bauiv1lib.gettickets

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
  7from typing import TYPE_CHECKING
  8
  9from efro.util import utc_now
 10
 11import bauiv1 as bui
 12
 13if TYPE_CHECKING:
 14    from typing import Any
 15
 16
 17class GetTicketsWindow(bui.Window):
 18    """Window for purchasing/acquiring classic tickets."""
 19
 20    def __init__(
 21        self,
 22        transition: str = 'in_right',
 23        from_modal_store: bool = False,
 24        modal: bool = False,
 25        origin_widget: bui.Widget | None = None,
 26        store_back_location: str | None = None,
 27    ):
 28        # pylint: disable=too-many-statements
 29        # pylint: disable=too-many-locals
 30
 31        plus = bui.app.plus
 32        assert plus is not None
 33
 34        bui.set_analytics_screen('Get Tickets Window')
 35
 36        self._transitioning_out = False
 37        self._store_back_location = store_back_location  # ew.
 38
 39        self._ad_button_greyed = False
 40        self._smooth_update_timer: bui.AppTimer | None = None
 41        self._ad_button = None
 42        self._ad_label = None
 43        self._ad_image = None
 44        self._ad_time_text = None
 45
 46        # If they provided an origin-widget, scale up from that.
 47        scale_origin: tuple[float, float] | None
 48        if origin_widget is not None:
 49            self._transition_out = 'out_scale'
 50            scale_origin = origin_widget.get_screen_space_center()
 51            transition = 'in_scale'
 52        else:
 53            self._transition_out = 'out_right'
 54            scale_origin = None
 55
 56        assert bui.app.classic is not None
 57        uiscale = bui.app.ui_v1.uiscale
 58        self._width = 1000.0 if uiscale is bui.UIScale.SMALL else 800.0
 59        x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0
 60        self._height = 480.0
 61
 62        self._modal = modal
 63        self._from_modal_store = from_modal_store
 64        self._r = 'getTicketsWindow'
 65
 66        top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
 67
 68        super().__init__(
 69            root_widget=bui.containerwidget(
 70                size=(self._width, self._height + top_extra),
 71                transition=transition,
 72                scale_origin_stack_offset=scale_origin,
 73                color=(0.4, 0.37, 0.55),
 74                scale=(
 75                    1.63
 76                    if uiscale is bui.UIScale.SMALL
 77                    else 1.2 if uiscale is bui.UIScale.MEDIUM else 1.0
 78                ),
 79                stack_offset=(
 80                    (0, -3) if uiscale is bui.UIScale.SMALL else (0, 0)
 81                ),
 82            )
 83        )
 84
 85        btn = bui.buttonwidget(
 86            parent=self._root_widget,
 87            position=(55 + x_inset, self._height - 79),
 88            size=(140, 60),
 89            scale=1.0,
 90            autoselect=True,
 91            label=bui.Lstr(resource='doneText' if modal else 'backText'),
 92            button_type='regular' if modal else 'back',
 93            on_activate_call=self._back,
 94        )
 95
 96        bui.containerwidget(edit=self._root_widget, cancel_button=btn)
 97
 98        bui.textwidget(
 99            parent=self._root_widget,
100            position=(self._width * 0.5 - 15, self._height - 47),
101            size=(0, 0),
102            color=bui.app.ui_v1.title_color,
103            scale=1.2,
104            h_align='right',
105            v_align='center',
106            text=bui.Lstr(resource=f'{self._r}.titleText'),
107            # text='Testing really long text here blah blah',
108            maxwidth=260,
109        )
110
111        # Get Tokens button
112        bui.buttonwidget(
113            parent=self._root_widget,
114            position=(self._width * 0.5, self._height - 72),
115            color=(0.65, 0.5, 0.7),
116            textcolor=bui.app.ui_v1.title_color,
117            size=(190, 50),
118            autoselect=True,
119            label=bui.Lstr(resource='tokens.getTokensText'),
120            on_activate_call=self._get_tokens_press,
121        )
122
123        # 'New!' by tokens button
124        bui.textwidget(
125            parent=self._root_widget,
126            text=bui.Lstr(resource='newExclaimText'),
127            position=(self._width * 0.5 + 25, self._height - 32),
128            size=(0, 0),
129            color=(1, 1, 0, 1.0),
130            rotate=22,
131            shadow=1.0,
132            maxwidth=150,
133            h_align='center',
134            v_align='center',
135            scale=0.7,
136        )
137
138        if not modal:
139            bui.buttonwidget(
140                edit=btn,
141                button_type='backSmall',
142                size=(60, 60),
143                label=bui.charstr(bui.SpecialChar.BACK),
144            )
145
146        b_size = (220.0, 180.0)
147        v = self._height - b_size[1] - 80
148        spacing = 1
149
150        self._ad_button = None
151
152        def _add_button(
153            item: str,
154            position: tuple[float, float],
155            size: tuple[float, float],
156            label: bui.Lstr,
157            price: str | None = None,
158            tex_name: str | None = None,
159            tex_opacity: float = 1.0,
160            tex_scale: float = 1.0,
161            enabled: bool = True,
162            text_scale: float = 1.0,
163        ) -> bui.Widget:
164            btn2 = bui.buttonwidget(
165                parent=self._root_widget,
166                position=position,
167                button_type='square',
168                size=size,
169                label='',
170                autoselect=True,
171                color=None if enabled else (0.5, 0.5, 0.5),
172                on_activate_call=(
173                    bui.Call(self._purchase, item)
174                    if enabled
175                    else self._disabled_press
176                ),
177            )
178            txt = bui.textwidget(
179                parent=self._root_widget,
180                text=label,
181                position=(
182                    position[0] + size[0] * 0.5,
183                    position[1] + size[1] * 0.3,
184                ),
185                scale=text_scale,
186                maxwidth=size[0] * 0.75,
187                size=(0, 0),
188                h_align='center',
189                v_align='center',
190                draw_controller=btn2,
191                color=(0.7, 0.9, 0.7, 1.0 if enabled else 0.2),
192            )
193            if price is not None and enabled:
194                bui.textwidget(
195                    parent=self._root_widget,
196                    text=price,
197                    position=(
198                        position[0] + size[0] * 0.5,
199                        position[1] + size[1] * 0.17,
200                    ),
201                    scale=0.7,
202                    maxwidth=size[0] * 0.75,
203                    size=(0, 0),
204                    h_align='center',
205                    v_align='center',
206                    draw_controller=btn2,
207                    color=(0.4, 0.9, 0.4, 1.0),
208                )
209            i = None
210            if tex_name is not None:
211                tex_size = 90.0 * tex_scale
212                i = bui.imagewidget(
213                    parent=self._root_widget,
214                    texture=bui.gettexture(tex_name),
215                    position=(
216                        position[0] + size[0] * 0.5 - tex_size * 0.5,
217                        position[1] + size[1] * 0.66 - tex_size * 0.5,
218                    ),
219                    size=(tex_size, tex_size),
220                    draw_controller=btn2,
221                    opacity=tex_opacity * (1.0 if enabled else 0.25),
222                )
223            if item == 'ad':
224                self._ad_button = btn2
225                self._ad_label = txt
226                assert i is not None
227                self._ad_image = i
228                self._ad_time_text = bui.textwidget(
229                    parent=self._root_widget,
230                    text='1m 10s',
231                    position=(
232                        position[0] + size[0] * 0.5,
233                        position[1] + size[1] * 0.5,
234                    ),
235                    scale=text_scale * 1.2,
236                    maxwidth=size[0] * 0.85,
237                    size=(0, 0),
238                    h_align='center',
239                    v_align='center',
240                    draw_controller=btn2,
241                    color=(0.4, 0.9, 0.4, 1.0),
242                )
243            return btn2
244
245        rsrc = f'{self._r}.ticketsText'
246
247        c2txt = bui.Lstr(
248            resource=rsrc,
249            subs=[
250                (
251                    '${COUNT}',
252                    str(
253                        plus.get_v1_account_misc_read_val('tickets2Amount', 500)
254                    ),
255                )
256            ],
257        )
258        c3txt = bui.Lstr(
259            resource=rsrc,
260            subs=[
261                (
262                    '${COUNT}',
263                    str(
264                        plus.get_v1_account_misc_read_val(
265                            'tickets3Amount', 1500
266                        )
267                    ),
268                )
269            ],
270        )
271        c4txt = bui.Lstr(
272            resource=rsrc,
273            subs=[
274                (
275                    '${COUNT}',
276                    str(
277                        plus.get_v1_account_misc_read_val(
278                            'tickets4Amount', 5000
279                        )
280                    ),
281                )
282            ],
283        )
284        c5txt = bui.Lstr(
285            resource=rsrc,
286            subs=[
287                (
288                    '${COUNT}',
289                    str(
290                        plus.get_v1_account_misc_read_val(
291                            'tickets5Amount', 15000
292                        )
293                    ),
294                )
295            ],
296        )
297
298        h = 110.0
299
300        # Enable buttons if we have prices.
301        tickets2_price = plus.get_price('tickets2')
302        tickets3_price = plus.get_price('tickets3')
303        tickets4_price = plus.get_price('tickets4')
304        tickets5_price = plus.get_price('tickets5')
305
306        # TEMP
307        # tickets1_price = '$0.99'
308        # tickets2_price = '$4.99'
309        # tickets3_price = '$9.99'
310        # tickets4_price = '$19.99'
311        # tickets5_price = '$49.99'
312
313        _add_button(
314            'tickets2',
315            enabled=(tickets2_price is not None),
316            position=(
317                self._width * 0.5 - spacing * 1.5 - b_size[0] * 2.0 + h,
318                v,
319            ),
320            size=b_size,
321            label=c2txt,
322            price=tickets2_price,
323            tex_name='ticketsMore',
324        )  # 0.99-ish
325        _add_button(
326            'tickets3',
327            enabled=(tickets3_price is not None),
328            position=(
329                self._width * 0.5 - spacing * 0.5 - b_size[0] * 1.0 + h,
330                v,
331            ),
332            size=b_size,
333            label=c3txt,
334            price=tickets3_price,
335            tex_name='ticketRoll',
336        )  # 4.99-ish
337        v -= b_size[1] - 5
338        _add_button(
339            'tickets4',
340            enabled=(tickets4_price is not None),
341            position=(
342                self._width * 0.5 - spacing * 1.5 - b_size[0] * 2.0 + h,
343                v,
344            ),
345            size=b_size,
346            label=c4txt,
347            price=tickets4_price,
348            tex_name='ticketRollBig',
349            tex_scale=1.2,
350        )  # 9.99-ish
351        _add_button(
352            'tickets5',
353            enabled=(tickets5_price is not None),
354            position=(
355                self._width * 0.5 - spacing * 0.5 - b_size[0] * 1.0 + h,
356                v,
357            ),
358            size=b_size,
359            label=c5txt,
360            price=tickets5_price,
361            tex_name='ticketRolls',
362            tex_scale=1.2,
363        )  # 19.99-ish
364
365        self._enable_ad_button = plus.has_video_ads()
366        h = self._width * 0.5 + 110.0
367        v = self._height - b_size[1] - 115.0
368
369        if self._enable_ad_button:
370            h_offs = 35
371            b_size_3 = (150, 120)
372            cdb = _add_button(
373                'ad',
374                position=(h + h_offs, v),
375                size=b_size_3,
376                label=bui.Lstr(
377                    resource=f'{self._r}.ticketsFromASponsorText',
378                    subs=[
379                        (
380                            '${COUNT}',
381                            str(
382                                plus.get_v1_account_misc_read_val(
383                                    'sponsorTickets', 5
384                                )
385                            ),
386                        )
387                    ],
388                ),
389                tex_name='ticketsMore',
390                enabled=self._enable_ad_button,
391                tex_opacity=0.6,
392                tex_scale=0.7,
393                text_scale=0.7,
394            )
395            bui.buttonwidget(edit=cdb, color=(0.65, 0.5, 0.7))
396
397            self._ad_free_text = bui.textwidget(
398                parent=self._root_widget,
399                text=bui.Lstr(resource=f'{self._r}.freeText'),
400                position=(
401                    h + h_offs + b_size_3[0] * 0.5,
402                    v + b_size_3[1] * 0.5 + 25,
403                ),
404                size=(0, 0),
405                color=(1, 1, 0, 1.0),
406                draw_controller=cdb,
407                rotate=15,
408                shadow=1.0,
409                maxwidth=150,
410                h_align='center',
411                v_align='center',
412                scale=1.0,
413            )
414            v -= 125
415        else:
416            v -= 20
417
418        if bool(True):
419            h_offs = 35
420            b_size_3 = (150, 120)
421            cdb = _add_button(
422                'app_invite',
423                position=(h + h_offs, v),
424                size=b_size_3,
425                label=bui.Lstr(
426                    resource='gatherWindow.earnTicketsForRecommendingText',
427                    subs=[
428                        (
429                            '${COUNT}',
430                            str(
431                                plus.get_v1_account_misc_read_val(
432                                    'sponsorTickets', 5
433                                )
434                            ),
435                        )
436                    ],
437                ),
438                tex_name='ticketsMore',
439                enabled=True,
440                tex_opacity=0.6,
441                tex_scale=0.7,
442                text_scale=0.7,
443            )
444            bui.buttonwidget(edit=cdb, color=(0.65, 0.5, 0.7))
445
446            bui.textwidget(
447                parent=self._root_widget,
448                text=bui.Lstr(resource=f'{self._r}.freeText'),
449                position=(
450                    h + h_offs + b_size_3[0] * 0.5,
451                    v + b_size_3[1] * 0.5 + 25,
452                ),
453                size=(0, 0),
454                color=(1, 1, 0, 1.0),
455                draw_controller=cdb,
456                rotate=15,
457                shadow=1.0,
458                maxwidth=150,
459                h_align='center',
460                v_align='center',
461                scale=1.0,
462            )
463            tc_y_offs = 0
464        else:
465            tc_y_offs = 0
466
467        h = self._width - (185 + x_inset)
468        v = self._height - 105 + tc_y_offs
469
470        txt1 = (
471            bui.Lstr(resource=f'{self._r}.youHaveText')
472            .evaluate()
473            .partition('${COUNT}')[0]
474            .strip()
475        )
476        txt2 = (
477            bui.Lstr(resource=f'{self._r}.youHaveText')
478            .evaluate()
479            .rpartition('${COUNT}')[-1]
480            .strip()
481        )
482
483        bui.textwidget(
484            parent=self._root_widget,
485            text=txt1,
486            position=(h, v),
487            size=(0, 0),
488            color=(0.5, 0.5, 0.6),
489            maxwidth=200,
490            h_align='center',
491            v_align='center',
492            scale=0.8,
493        )
494        v -= 30
495        self._ticket_count_text = bui.textwidget(
496            parent=self._root_widget,
497            position=(h, v),
498            size=(0, 0),
499            color=(0.2, 1.0, 0.2),
500            maxwidth=200,
501            h_align='center',
502            v_align='center',
503            scale=1.6,
504        )
505        v -= 30
506        bui.textwidget(
507            parent=self._root_widget,
508            text=txt2,
509            position=(h, v),
510            size=(0, 0),
511            color=(0.5, 0.5, 0.6),
512            maxwidth=200,
513            h_align='center',
514            v_align='center',
515            scale=0.8,
516        )
517
518        self._ticking_sound: bui.Sound | None = None
519        self._smooth_ticket_count: float | None = None
520        self._ticket_count = 0
521        self._update()
522        self._update_timer = bui.AppTimer(
523            1.0, bui.WeakCall(self._update), repeat=True
524        )
525        self._smooth_increase_speed = 1.0
526
527    def __del__(self) -> None:
528        if self._ticking_sound is not None:
529            self._ticking_sound.stop()
530            self._ticking_sound = None
531
532    def _smooth_update(self) -> None:
533        if not self._ticket_count_text:
534            self._smooth_update_timer = None
535            return
536
537        finished = False
538
539        # If we're going down, do it immediately.
540        assert self._smooth_ticket_count is not None
541        if int(self._smooth_ticket_count) >= self._ticket_count:
542            self._smooth_ticket_count = float(self._ticket_count)
543            finished = True
544        else:
545            # We're going up; start a sound if need be.
546            self._smooth_ticket_count = min(
547                self._smooth_ticket_count + 1.0 * self._smooth_increase_speed,
548                self._ticket_count,
549            )
550            if int(self._smooth_ticket_count) >= self._ticket_count:
551                finished = True
552                self._smooth_ticket_count = float(self._ticket_count)
553            elif self._ticking_sound is None:
554                self._ticking_sound = bui.getsound('scoreIncrease')
555                self._ticking_sound.play()
556
557        bui.textwidget(
558            edit=self._ticket_count_text,
559            text=str(int(self._smooth_ticket_count)),
560        )
561
562        # If we've reached the target, kill the timer/sound/etc.
563        if finished:
564            self._smooth_update_timer = None
565            if self._ticking_sound is not None:
566                self._ticking_sound.stop()
567                self._ticking_sound = None
568                bui.getsound('cashRegister2').play()
569
570    def _update(self) -> None:
571        import datetime
572
573        plus = bui.app.plus
574        assert plus is not None
575
576        # If we somehow get signed out, just die.
577        if plus.get_v1_account_state() != 'signed_in':
578            self._back()
579            return
580
581        self._ticket_count = plus.get_v1_account_ticket_count()
582
583        # Update our incentivized ad button depending on whether ads are
584        # available.
585        if self._ad_button is not None:
586            next_reward_ad_time = plus.get_v1_account_misc_read_val_2(
587                'nextRewardAdTime', None
588            )
589            if next_reward_ad_time is not None:
590                next_reward_ad_time = datetime.datetime.fromtimestamp(
591                    next_reward_ad_time, datetime.UTC
592                )
593            now = utc_now()
594            if plus.have_incentivized_ad() and (
595                next_reward_ad_time is None or next_reward_ad_time <= now
596            ):
597                self._ad_button_greyed = False
598                bui.buttonwidget(edit=self._ad_button, color=(0.65, 0.5, 0.7))
599                bui.textwidget(edit=self._ad_label, color=(0.7, 0.9, 0.7, 1.0))
600                bui.textwidget(edit=self._ad_free_text, color=(1, 1, 0, 1))
601                bui.imagewidget(edit=self._ad_image, opacity=0.6)
602                bui.textwidget(edit=self._ad_time_text, text='')
603            else:
604                self._ad_button_greyed = True
605                bui.buttonwidget(edit=self._ad_button, color=(0.5, 0.5, 0.5))
606                bui.textwidget(edit=self._ad_label, color=(0.7, 0.9, 0.7, 0.2))
607                bui.textwidget(edit=self._ad_free_text, color=(1, 1, 0, 0.2))
608                bui.imagewidget(edit=self._ad_image, opacity=0.6 * 0.25)
609                sval: str | bui.Lstr
610                if (
611                    next_reward_ad_time is not None
612                    and next_reward_ad_time > now
613                ):
614                    sval = bui.timestring(
615                        (next_reward_ad_time - now).total_seconds(), centi=False
616                    )
617                else:
618                    sval = ''
619                bui.textwidget(edit=self._ad_time_text, text=sval)
620
621        # If this is our first update, assign immediately; otherwise kick
622        # off a smooth transition if the value has changed.
623        if self._smooth_ticket_count is None:
624            self._smooth_ticket_count = float(self._ticket_count)
625            self._smooth_update()  # will set the text widget
626
627        elif (
628            self._ticket_count != int(self._smooth_ticket_count)
629            and self._smooth_update_timer is None
630        ):
631            self._smooth_update_timer = bui.AppTimer(
632                0.05, bui.WeakCall(self._smooth_update), repeat=True
633            )
634            diff = abs(float(self._ticket_count) - self._smooth_ticket_count)
635            self._smooth_increase_speed = (
636                diff / 100.0
637                if diff >= 5000
638                else (
639                    diff / 50.0
640                    if diff >= 1500
641                    else diff / 30.0 if diff >= 500 else diff / 15.0
642                )
643            )
644
645    def _disabled_press(self) -> None:
646        plus = bui.app.plus
647        assert plus is not None
648
649        # If we're on a platform without purchases, inform the user they
650        # can link their accounts and buy stuff elsewhere.
651        app = bui.app
652        assert app.classic is not None
653        if (
654            app.env.test
655            or (
656                app.classic.platform == 'android'
657                and app.classic.subplatform in ['oculus', 'cardboard']
658            )
659        ) and plus.get_v1_account_misc_read_val('allowAccountLinking2', False):
660            bui.screenmessage(
661                bui.Lstr(resource=f'{self._r}.unavailableLinkAccountText'),
662                color=(1, 0.5, 0),
663            )
664        else:
665            bui.screenmessage(
666                bui.Lstr(resource=f'{self._r}.unavailableText'),
667                color=(1, 0.5, 0),
668            )
669        bui.getsound('error').play()
670
671    def _purchase(self, item: str) -> None:
672        from bauiv1lib import account
673        from bauiv1lib import appinvite
674
675        plus = bui.app.plus
676        assert plus is not None
677
678        if bui.app.classic is None:
679            raise RuntimeError('This requires classic support.')
680
681        if item == 'app_invite':
682            if plus.get_v1_account_state() != 'signed_in':
683                account.show_sign_in_prompt()
684                return
685            appinvite.handle_app_invites_press()
686            return
687
688        # Here we ping the server to ask if it's valid for us to
689        # purchase this.. (better to fail now than after we've paid
690        # locally).
691        app = bui.app
692        assert app.classic is not None
693        bui.app.classic.master_server_v1_get(
694            'bsAccountPurchaseCheck',
695            {
696                'item': item,
697                'platform': app.classic.platform,
698                'subplatform': app.classic.subplatform,
699                'version': app.env.engine_version,
700                'buildNumber': app.env.engine_build_number,
701            },
702            callback=bui.WeakCall(self._purchase_check_result, item),
703        )
704
705    def _purchase_check_result(
706        self, item: str, result: dict[str, Any] | None
707    ) -> None:
708        if result is None:
709            bui.getsound('error').play()
710            bui.screenmessage(
711                bui.Lstr(resource='internal.unavailableNoConnectionText'),
712                color=(1, 0, 0),
713            )
714        else:
715            if result['allow']:
716                self._do_purchase(item)
717            else:
718                if result['reason'] == 'versionTooOld':
719                    bui.getsound('error').play()
720                    bui.screenmessage(
721                        bui.Lstr(resource='getTicketsWindow.versionTooOldText'),
722                        color=(1, 0, 0),
723                    )
724                else:
725                    bui.getsound('error').play()
726                    bui.screenmessage(
727                        bui.Lstr(resource='getTicketsWindow.unavailableText'),
728                        color=(1, 0, 0),
729                    )
730
731    # Actually start the purchase locally.
732    def _do_purchase(self, item: str) -> None:
733        plus = bui.app.plus
734        assert plus is not None
735
736        if item == 'ad':
737            import datetime
738
739            # If ads are disabled until some time, error.
740            next_reward_ad_time = plus.get_v1_account_misc_read_val_2(
741                'nextRewardAdTime', None
742            )
743            if next_reward_ad_time is not None:
744                next_reward_ad_time = datetime.datetime.fromtimestamp(
745                    next_reward_ad_time, datetime.UTC
746                )
747            now = utc_now()
748            if (
749                next_reward_ad_time is not None and next_reward_ad_time > now
750            ) or self._ad_button_greyed:
751                bui.getsound('error').play()
752                bui.screenmessage(
753                    bui.Lstr(
754                        resource='getTicketsWindow.unavailableTemporarilyText'
755                    ),
756                    color=(1, 0, 0),
757                )
758            elif self._enable_ad_button:
759                assert bui.app.classic is not None
760                bui.app.classic.ads.show_ad('tickets')
761        else:
762            plus.purchase(item)
763
764    def _get_tokens_press(self) -> None:
765        from functools import partial
766
767        from bauiv1lib.gettokens import GetTokensWindow
768
769        # No-op if our underlying widget is dead or on its way out.
770        if not self._root_widget or self._root_widget.transitioning_out:
771            return
772
773        if self._transitioning_out:
774            return
775
776        bui.containerwidget(edit=self._root_widget, transition='out_left')
777
778        # Note: Make sure we don't pass anything here that would
779        # capture 'self'. (a lambda would implicitly do this by capturing
780        # the stack frame).
781        restorecall = partial(
782            _restore_get_tickets_window,
783            self._modal,
784            self._from_modal_store,
785            self._store_back_location,
786        )
787
788        window = GetTokensWindow(
789            transition='in_right',
790            restore_previous_call=restorecall,
791        ).get_root_widget()
792        if not self._modal and not self._from_modal_store:
793            assert bui.app.classic is not None
794            bui.app.ui_v1.set_main_menu_window(
795                window, from_window=self._root_widget
796            )
797        self._transitioning_out = True
798
799    def _back(self) -> None:
800        from bauiv1lib.store import browser
801
802        # No-op if our underlying widget is dead or on its way out.
803        if not self._root_widget or self._root_widget.transitioning_out:
804            return
805
806        if self._transitioning_out:
807            return
808
809        bui.containerwidget(
810            edit=self._root_widget, transition=self._transition_out
811        )
812        if not self._modal:
813            window = browser.StoreBrowserWindow(
814                transition='in_left',
815                modal=self._from_modal_store,
816                back_location=self._store_back_location,
817            ).get_root_widget()
818            if not self._from_modal_store:
819                assert bui.app.classic is not None
820                bui.app.ui_v1.set_main_menu_window(
821                    window, from_window=self._root_widget
822                )
823        self._transitioning_out = True
824
825
826# A call we can bundle up and pass to windows we open; allows them to
827# get back to us without having to explicitly know about us.
828def _restore_get_tickets_window(
829    modal: bool,
830    from_modal_store: bool,
831    store_back_location: str | None,
832    from_window: bui.Widget,
833) -> None:
834    restored = GetTicketsWindow(
835        transition='in_left',
836        modal=modal,
837        from_modal_store=from_modal_store,
838        store_back_location=store_back_location,
839    )
840    if not modal and not from_modal_store:
841        assert bui.app.classic is not None
842        bui.app.ui_v1.set_main_menu_window(
843            restored.get_root_widget(), from_window=from_window
844        )
845
846
847def show_get_tickets_prompt() -> None:
848    """Show a 'not enough tickets' prompt with an option to purchase more.
849
850    Note that the purchase option may not always be available
851    depending on the build of the game.
852    """
853    from bauiv1lib.confirm import ConfirmWindow
854
855    assert bui.app.classic is not None
856
857    if bui.app.classic.allow_ticket_purchases:
858        ConfirmWindow(
859            bui.Lstr(
860                translate=(
861                    'serverResponses',
862                    'You don\'t have enough tickets for this!',
863                )
864            ),
865            lambda: GetTicketsWindow(modal=True),
866            ok_text=bui.Lstr(resource='getTicketsWindow.titleText'),
867            width=460,
868            height=130,
869        )
870    else:
871        ConfirmWindow(
872            bui.Lstr(
873                translate=(
874                    'serverResponses',
875                    'You don\'t have enough tickets for this!',
876                )
877            ),
878            cancel_button=False,
879            width=460,
880            height=130,
881        )
class GetTicketsWindow(bauiv1._uitypes.Window):
 18class GetTicketsWindow(bui.Window):
 19    """Window for purchasing/acquiring classic tickets."""
 20
 21    def __init__(
 22        self,
 23        transition: str = 'in_right',
 24        from_modal_store: bool = False,
 25        modal: bool = False,
 26        origin_widget: bui.Widget | None = None,
 27        store_back_location: str | None = None,
 28    ):
 29        # pylint: disable=too-many-statements
 30        # pylint: disable=too-many-locals
 31
 32        plus = bui.app.plus
 33        assert plus is not None
 34
 35        bui.set_analytics_screen('Get Tickets Window')
 36
 37        self._transitioning_out = False
 38        self._store_back_location = store_back_location  # ew.
 39
 40        self._ad_button_greyed = False
 41        self._smooth_update_timer: bui.AppTimer | None = None
 42        self._ad_button = None
 43        self._ad_label = None
 44        self._ad_image = None
 45        self._ad_time_text = None
 46
 47        # If they provided an origin-widget, scale up from that.
 48        scale_origin: tuple[float, float] | None
 49        if origin_widget is not None:
 50            self._transition_out = 'out_scale'
 51            scale_origin = origin_widget.get_screen_space_center()
 52            transition = 'in_scale'
 53        else:
 54            self._transition_out = 'out_right'
 55            scale_origin = None
 56
 57        assert bui.app.classic is not None
 58        uiscale = bui.app.ui_v1.uiscale
 59        self._width = 1000.0 if uiscale is bui.UIScale.SMALL else 800.0
 60        x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0
 61        self._height = 480.0
 62
 63        self._modal = modal
 64        self._from_modal_store = from_modal_store
 65        self._r = 'getTicketsWindow'
 66
 67        top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
 68
 69        super().__init__(
 70            root_widget=bui.containerwidget(
 71                size=(self._width, self._height + top_extra),
 72                transition=transition,
 73                scale_origin_stack_offset=scale_origin,
 74                color=(0.4, 0.37, 0.55),
 75                scale=(
 76                    1.63
 77                    if uiscale is bui.UIScale.SMALL
 78                    else 1.2 if uiscale is bui.UIScale.MEDIUM else 1.0
 79                ),
 80                stack_offset=(
 81                    (0, -3) if uiscale is bui.UIScale.SMALL else (0, 0)
 82                ),
 83            )
 84        )
 85
 86        btn = bui.buttonwidget(
 87            parent=self._root_widget,
 88            position=(55 + x_inset, self._height - 79),
 89            size=(140, 60),
 90            scale=1.0,
 91            autoselect=True,
 92            label=bui.Lstr(resource='doneText' if modal else 'backText'),
 93            button_type='regular' if modal else 'back',
 94            on_activate_call=self._back,
 95        )
 96
 97        bui.containerwidget(edit=self._root_widget, cancel_button=btn)
 98
 99        bui.textwidget(
100            parent=self._root_widget,
101            position=(self._width * 0.5 - 15, self._height - 47),
102            size=(0, 0),
103            color=bui.app.ui_v1.title_color,
104            scale=1.2,
105            h_align='right',
106            v_align='center',
107            text=bui.Lstr(resource=f'{self._r}.titleText'),
108            # text='Testing really long text here blah blah',
109            maxwidth=260,
110        )
111
112        # Get Tokens button
113        bui.buttonwidget(
114            parent=self._root_widget,
115            position=(self._width * 0.5, self._height - 72),
116            color=(0.65, 0.5, 0.7),
117            textcolor=bui.app.ui_v1.title_color,
118            size=(190, 50),
119            autoselect=True,
120            label=bui.Lstr(resource='tokens.getTokensText'),
121            on_activate_call=self._get_tokens_press,
122        )
123
124        # 'New!' by tokens button
125        bui.textwidget(
126            parent=self._root_widget,
127            text=bui.Lstr(resource='newExclaimText'),
128            position=(self._width * 0.5 + 25, self._height - 32),
129            size=(0, 0),
130            color=(1, 1, 0, 1.0),
131            rotate=22,
132            shadow=1.0,
133            maxwidth=150,
134            h_align='center',
135            v_align='center',
136            scale=0.7,
137        )
138
139        if not modal:
140            bui.buttonwidget(
141                edit=btn,
142                button_type='backSmall',
143                size=(60, 60),
144                label=bui.charstr(bui.SpecialChar.BACK),
145            )
146
147        b_size = (220.0, 180.0)
148        v = self._height - b_size[1] - 80
149        spacing = 1
150
151        self._ad_button = None
152
153        def _add_button(
154            item: str,
155            position: tuple[float, float],
156            size: tuple[float, float],
157            label: bui.Lstr,
158            price: str | None = None,
159            tex_name: str | None = None,
160            tex_opacity: float = 1.0,
161            tex_scale: float = 1.0,
162            enabled: bool = True,
163            text_scale: float = 1.0,
164        ) -> bui.Widget:
165            btn2 = bui.buttonwidget(
166                parent=self._root_widget,
167                position=position,
168                button_type='square',
169                size=size,
170                label='',
171                autoselect=True,
172                color=None if enabled else (0.5, 0.5, 0.5),
173                on_activate_call=(
174                    bui.Call(self._purchase, item)
175                    if enabled
176                    else self._disabled_press
177                ),
178            )
179            txt = bui.textwidget(
180                parent=self._root_widget,
181                text=label,
182                position=(
183                    position[0] + size[0] * 0.5,
184                    position[1] + size[1] * 0.3,
185                ),
186                scale=text_scale,
187                maxwidth=size[0] * 0.75,
188                size=(0, 0),
189                h_align='center',
190                v_align='center',
191                draw_controller=btn2,
192                color=(0.7, 0.9, 0.7, 1.0 if enabled else 0.2),
193            )
194            if price is not None and enabled:
195                bui.textwidget(
196                    parent=self._root_widget,
197                    text=price,
198                    position=(
199                        position[0] + size[0] * 0.5,
200                        position[1] + size[1] * 0.17,
201                    ),
202                    scale=0.7,
203                    maxwidth=size[0] * 0.75,
204                    size=(0, 0),
205                    h_align='center',
206                    v_align='center',
207                    draw_controller=btn2,
208                    color=(0.4, 0.9, 0.4, 1.0),
209                )
210            i = None
211            if tex_name is not None:
212                tex_size = 90.0 * tex_scale
213                i = bui.imagewidget(
214                    parent=self._root_widget,
215                    texture=bui.gettexture(tex_name),
216                    position=(
217                        position[0] + size[0] * 0.5 - tex_size * 0.5,
218                        position[1] + size[1] * 0.66 - tex_size * 0.5,
219                    ),
220                    size=(tex_size, tex_size),
221                    draw_controller=btn2,
222                    opacity=tex_opacity * (1.0 if enabled else 0.25),
223                )
224            if item == 'ad':
225                self._ad_button = btn2
226                self._ad_label = txt
227                assert i is not None
228                self._ad_image = i
229                self._ad_time_text = bui.textwidget(
230                    parent=self._root_widget,
231                    text='1m 10s',
232                    position=(
233                        position[0] + size[0] * 0.5,
234                        position[1] + size[1] * 0.5,
235                    ),
236                    scale=text_scale * 1.2,
237                    maxwidth=size[0] * 0.85,
238                    size=(0, 0),
239                    h_align='center',
240                    v_align='center',
241                    draw_controller=btn2,
242                    color=(0.4, 0.9, 0.4, 1.0),
243                )
244            return btn2
245
246        rsrc = f'{self._r}.ticketsText'
247
248        c2txt = bui.Lstr(
249            resource=rsrc,
250            subs=[
251                (
252                    '${COUNT}',
253                    str(
254                        plus.get_v1_account_misc_read_val('tickets2Amount', 500)
255                    ),
256                )
257            ],
258        )
259        c3txt = bui.Lstr(
260            resource=rsrc,
261            subs=[
262                (
263                    '${COUNT}',
264                    str(
265                        plus.get_v1_account_misc_read_val(
266                            'tickets3Amount', 1500
267                        )
268                    ),
269                )
270            ],
271        )
272        c4txt = bui.Lstr(
273            resource=rsrc,
274            subs=[
275                (
276                    '${COUNT}',
277                    str(
278                        plus.get_v1_account_misc_read_val(
279                            'tickets4Amount', 5000
280                        )
281                    ),
282                )
283            ],
284        )
285        c5txt = bui.Lstr(
286            resource=rsrc,
287            subs=[
288                (
289                    '${COUNT}',
290                    str(
291                        plus.get_v1_account_misc_read_val(
292                            'tickets5Amount', 15000
293                        )
294                    ),
295                )
296            ],
297        )
298
299        h = 110.0
300
301        # Enable buttons if we have prices.
302        tickets2_price = plus.get_price('tickets2')
303        tickets3_price = plus.get_price('tickets3')
304        tickets4_price = plus.get_price('tickets4')
305        tickets5_price = plus.get_price('tickets5')
306
307        # TEMP
308        # tickets1_price = '$0.99'
309        # tickets2_price = '$4.99'
310        # tickets3_price = '$9.99'
311        # tickets4_price = '$19.99'
312        # tickets5_price = '$49.99'
313
314        _add_button(
315            'tickets2',
316            enabled=(tickets2_price is not None),
317            position=(
318                self._width * 0.5 - spacing * 1.5 - b_size[0] * 2.0 + h,
319                v,
320            ),
321            size=b_size,
322            label=c2txt,
323            price=tickets2_price,
324            tex_name='ticketsMore',
325        )  # 0.99-ish
326        _add_button(
327            'tickets3',
328            enabled=(tickets3_price is not None),
329            position=(
330                self._width * 0.5 - spacing * 0.5 - b_size[0] * 1.0 + h,
331                v,
332            ),
333            size=b_size,
334            label=c3txt,
335            price=tickets3_price,
336            tex_name='ticketRoll',
337        )  # 4.99-ish
338        v -= b_size[1] - 5
339        _add_button(
340            'tickets4',
341            enabled=(tickets4_price is not None),
342            position=(
343                self._width * 0.5 - spacing * 1.5 - b_size[0] * 2.0 + h,
344                v,
345            ),
346            size=b_size,
347            label=c4txt,
348            price=tickets4_price,
349            tex_name='ticketRollBig',
350            tex_scale=1.2,
351        )  # 9.99-ish
352        _add_button(
353            'tickets5',
354            enabled=(tickets5_price is not None),
355            position=(
356                self._width * 0.5 - spacing * 0.5 - b_size[0] * 1.0 + h,
357                v,
358            ),
359            size=b_size,
360            label=c5txt,
361            price=tickets5_price,
362            tex_name='ticketRolls',
363            tex_scale=1.2,
364        )  # 19.99-ish
365
366        self._enable_ad_button = plus.has_video_ads()
367        h = self._width * 0.5 + 110.0
368        v = self._height - b_size[1] - 115.0
369
370        if self._enable_ad_button:
371            h_offs = 35
372            b_size_3 = (150, 120)
373            cdb = _add_button(
374                'ad',
375                position=(h + h_offs, v),
376                size=b_size_3,
377                label=bui.Lstr(
378                    resource=f'{self._r}.ticketsFromASponsorText',
379                    subs=[
380                        (
381                            '${COUNT}',
382                            str(
383                                plus.get_v1_account_misc_read_val(
384                                    'sponsorTickets', 5
385                                )
386                            ),
387                        )
388                    ],
389                ),
390                tex_name='ticketsMore',
391                enabled=self._enable_ad_button,
392                tex_opacity=0.6,
393                tex_scale=0.7,
394                text_scale=0.7,
395            )
396            bui.buttonwidget(edit=cdb, color=(0.65, 0.5, 0.7))
397
398            self._ad_free_text = bui.textwidget(
399                parent=self._root_widget,
400                text=bui.Lstr(resource=f'{self._r}.freeText'),
401                position=(
402                    h + h_offs + b_size_3[0] * 0.5,
403                    v + b_size_3[1] * 0.5 + 25,
404                ),
405                size=(0, 0),
406                color=(1, 1, 0, 1.0),
407                draw_controller=cdb,
408                rotate=15,
409                shadow=1.0,
410                maxwidth=150,
411                h_align='center',
412                v_align='center',
413                scale=1.0,
414            )
415            v -= 125
416        else:
417            v -= 20
418
419        if bool(True):
420            h_offs = 35
421            b_size_3 = (150, 120)
422            cdb = _add_button(
423                'app_invite',
424                position=(h + h_offs, v),
425                size=b_size_3,
426                label=bui.Lstr(
427                    resource='gatherWindow.earnTicketsForRecommendingText',
428                    subs=[
429                        (
430                            '${COUNT}',
431                            str(
432                                plus.get_v1_account_misc_read_val(
433                                    'sponsorTickets', 5
434                                )
435                            ),
436                        )
437                    ],
438                ),
439                tex_name='ticketsMore',
440                enabled=True,
441                tex_opacity=0.6,
442                tex_scale=0.7,
443                text_scale=0.7,
444            )
445            bui.buttonwidget(edit=cdb, color=(0.65, 0.5, 0.7))
446
447            bui.textwidget(
448                parent=self._root_widget,
449                text=bui.Lstr(resource=f'{self._r}.freeText'),
450                position=(
451                    h + h_offs + b_size_3[0] * 0.5,
452                    v + b_size_3[1] * 0.5 + 25,
453                ),
454                size=(0, 0),
455                color=(1, 1, 0, 1.0),
456                draw_controller=cdb,
457                rotate=15,
458                shadow=1.0,
459                maxwidth=150,
460                h_align='center',
461                v_align='center',
462                scale=1.0,
463            )
464            tc_y_offs = 0
465        else:
466            tc_y_offs = 0
467
468        h = self._width - (185 + x_inset)
469        v = self._height - 105 + tc_y_offs
470
471        txt1 = (
472            bui.Lstr(resource=f'{self._r}.youHaveText')
473            .evaluate()
474            .partition('${COUNT}')[0]
475            .strip()
476        )
477        txt2 = (
478            bui.Lstr(resource=f'{self._r}.youHaveText')
479            .evaluate()
480            .rpartition('${COUNT}')[-1]
481            .strip()
482        )
483
484        bui.textwidget(
485            parent=self._root_widget,
486            text=txt1,
487            position=(h, v),
488            size=(0, 0),
489            color=(0.5, 0.5, 0.6),
490            maxwidth=200,
491            h_align='center',
492            v_align='center',
493            scale=0.8,
494        )
495        v -= 30
496        self._ticket_count_text = bui.textwidget(
497            parent=self._root_widget,
498            position=(h, v),
499            size=(0, 0),
500            color=(0.2, 1.0, 0.2),
501            maxwidth=200,
502            h_align='center',
503            v_align='center',
504            scale=1.6,
505        )
506        v -= 30
507        bui.textwidget(
508            parent=self._root_widget,
509            text=txt2,
510            position=(h, v),
511            size=(0, 0),
512            color=(0.5, 0.5, 0.6),
513            maxwidth=200,
514            h_align='center',
515            v_align='center',
516            scale=0.8,
517        )
518
519        self._ticking_sound: bui.Sound | None = None
520        self._smooth_ticket_count: float | None = None
521        self._ticket_count = 0
522        self._update()
523        self._update_timer = bui.AppTimer(
524            1.0, bui.WeakCall(self._update), repeat=True
525        )
526        self._smooth_increase_speed = 1.0
527
528    def __del__(self) -> None:
529        if self._ticking_sound is not None:
530            self._ticking_sound.stop()
531            self._ticking_sound = None
532
533    def _smooth_update(self) -> None:
534        if not self._ticket_count_text:
535            self._smooth_update_timer = None
536            return
537
538        finished = False
539
540        # If we're going down, do it immediately.
541        assert self._smooth_ticket_count is not None
542        if int(self._smooth_ticket_count) >= self._ticket_count:
543            self._smooth_ticket_count = float(self._ticket_count)
544            finished = True
545        else:
546            # We're going up; start a sound if need be.
547            self._smooth_ticket_count = min(
548                self._smooth_ticket_count + 1.0 * self._smooth_increase_speed,
549                self._ticket_count,
550            )
551            if int(self._smooth_ticket_count) >= self._ticket_count:
552                finished = True
553                self._smooth_ticket_count = float(self._ticket_count)
554            elif self._ticking_sound is None:
555                self._ticking_sound = bui.getsound('scoreIncrease')
556                self._ticking_sound.play()
557
558        bui.textwidget(
559            edit=self._ticket_count_text,
560            text=str(int(self._smooth_ticket_count)),
561        )
562
563        # If we've reached the target, kill the timer/sound/etc.
564        if finished:
565            self._smooth_update_timer = None
566            if self._ticking_sound is not None:
567                self._ticking_sound.stop()
568                self._ticking_sound = None
569                bui.getsound('cashRegister2').play()
570
571    def _update(self) -> None:
572        import datetime
573
574        plus = bui.app.plus
575        assert plus is not None
576
577        # If we somehow get signed out, just die.
578        if plus.get_v1_account_state() != 'signed_in':
579            self._back()
580            return
581
582        self._ticket_count = plus.get_v1_account_ticket_count()
583
584        # Update our incentivized ad button depending on whether ads are
585        # available.
586        if self._ad_button is not None:
587            next_reward_ad_time = plus.get_v1_account_misc_read_val_2(
588                'nextRewardAdTime', None
589            )
590            if next_reward_ad_time is not None:
591                next_reward_ad_time = datetime.datetime.fromtimestamp(
592                    next_reward_ad_time, datetime.UTC
593                )
594            now = utc_now()
595            if plus.have_incentivized_ad() and (
596                next_reward_ad_time is None or next_reward_ad_time <= now
597            ):
598                self._ad_button_greyed = False
599                bui.buttonwidget(edit=self._ad_button, color=(0.65, 0.5, 0.7))
600                bui.textwidget(edit=self._ad_label, color=(0.7, 0.9, 0.7, 1.0))
601                bui.textwidget(edit=self._ad_free_text, color=(1, 1, 0, 1))
602                bui.imagewidget(edit=self._ad_image, opacity=0.6)
603                bui.textwidget(edit=self._ad_time_text, text='')
604            else:
605                self._ad_button_greyed = True
606                bui.buttonwidget(edit=self._ad_button, color=(0.5, 0.5, 0.5))
607                bui.textwidget(edit=self._ad_label, color=(0.7, 0.9, 0.7, 0.2))
608                bui.textwidget(edit=self._ad_free_text, color=(1, 1, 0, 0.2))
609                bui.imagewidget(edit=self._ad_image, opacity=0.6 * 0.25)
610                sval: str | bui.Lstr
611                if (
612                    next_reward_ad_time is not None
613                    and next_reward_ad_time > now
614                ):
615                    sval = bui.timestring(
616                        (next_reward_ad_time - now).total_seconds(), centi=False
617                    )
618                else:
619                    sval = ''
620                bui.textwidget(edit=self._ad_time_text, text=sval)
621
622        # If this is our first update, assign immediately; otherwise kick
623        # off a smooth transition if the value has changed.
624        if self._smooth_ticket_count is None:
625            self._smooth_ticket_count = float(self._ticket_count)
626            self._smooth_update()  # will set the text widget
627
628        elif (
629            self._ticket_count != int(self._smooth_ticket_count)
630            and self._smooth_update_timer is None
631        ):
632            self._smooth_update_timer = bui.AppTimer(
633                0.05, bui.WeakCall(self._smooth_update), repeat=True
634            )
635            diff = abs(float(self._ticket_count) - self._smooth_ticket_count)
636            self._smooth_increase_speed = (
637                diff / 100.0
638                if diff >= 5000
639                else (
640                    diff / 50.0
641                    if diff >= 1500
642                    else diff / 30.0 if diff >= 500 else diff / 15.0
643                )
644            )
645
646    def _disabled_press(self) -> None:
647        plus = bui.app.plus
648        assert plus is not None
649
650        # If we're on a platform without purchases, inform the user they
651        # can link their accounts and buy stuff elsewhere.
652        app = bui.app
653        assert app.classic is not None
654        if (
655            app.env.test
656            or (
657                app.classic.platform == 'android'
658                and app.classic.subplatform in ['oculus', 'cardboard']
659            )
660        ) and plus.get_v1_account_misc_read_val('allowAccountLinking2', False):
661            bui.screenmessage(
662                bui.Lstr(resource=f'{self._r}.unavailableLinkAccountText'),
663                color=(1, 0.5, 0),
664            )
665        else:
666            bui.screenmessage(
667                bui.Lstr(resource=f'{self._r}.unavailableText'),
668                color=(1, 0.5, 0),
669            )
670        bui.getsound('error').play()
671
672    def _purchase(self, item: str) -> None:
673        from bauiv1lib import account
674        from bauiv1lib import appinvite
675
676        plus = bui.app.plus
677        assert plus is not None
678
679        if bui.app.classic is None:
680            raise RuntimeError('This requires classic support.')
681
682        if item == 'app_invite':
683            if plus.get_v1_account_state() != 'signed_in':
684                account.show_sign_in_prompt()
685                return
686            appinvite.handle_app_invites_press()
687            return
688
689        # Here we ping the server to ask if it's valid for us to
690        # purchase this.. (better to fail now than after we've paid
691        # locally).
692        app = bui.app
693        assert app.classic is not None
694        bui.app.classic.master_server_v1_get(
695            'bsAccountPurchaseCheck',
696            {
697                'item': item,
698                'platform': app.classic.platform,
699                'subplatform': app.classic.subplatform,
700                'version': app.env.engine_version,
701                'buildNumber': app.env.engine_build_number,
702            },
703            callback=bui.WeakCall(self._purchase_check_result, item),
704        )
705
706    def _purchase_check_result(
707        self, item: str, result: dict[str, Any] | None
708    ) -> None:
709        if result is None:
710            bui.getsound('error').play()
711            bui.screenmessage(
712                bui.Lstr(resource='internal.unavailableNoConnectionText'),
713                color=(1, 0, 0),
714            )
715        else:
716            if result['allow']:
717                self._do_purchase(item)
718            else:
719                if result['reason'] == 'versionTooOld':
720                    bui.getsound('error').play()
721                    bui.screenmessage(
722                        bui.Lstr(resource='getTicketsWindow.versionTooOldText'),
723                        color=(1, 0, 0),
724                    )
725                else:
726                    bui.getsound('error').play()
727                    bui.screenmessage(
728                        bui.Lstr(resource='getTicketsWindow.unavailableText'),
729                        color=(1, 0, 0),
730                    )
731
732    # Actually start the purchase locally.
733    def _do_purchase(self, item: str) -> None:
734        plus = bui.app.plus
735        assert plus is not None
736
737        if item == 'ad':
738            import datetime
739
740            # If ads are disabled until some time, error.
741            next_reward_ad_time = plus.get_v1_account_misc_read_val_2(
742                'nextRewardAdTime', None
743            )
744            if next_reward_ad_time is not None:
745                next_reward_ad_time = datetime.datetime.fromtimestamp(
746                    next_reward_ad_time, datetime.UTC
747                )
748            now = utc_now()
749            if (
750                next_reward_ad_time is not None and next_reward_ad_time > now
751            ) or self._ad_button_greyed:
752                bui.getsound('error').play()
753                bui.screenmessage(
754                    bui.Lstr(
755                        resource='getTicketsWindow.unavailableTemporarilyText'
756                    ),
757                    color=(1, 0, 0),
758                )
759            elif self._enable_ad_button:
760                assert bui.app.classic is not None
761                bui.app.classic.ads.show_ad('tickets')
762        else:
763            plus.purchase(item)
764
765    def _get_tokens_press(self) -> None:
766        from functools import partial
767
768        from bauiv1lib.gettokens import GetTokensWindow
769
770        # No-op if our underlying widget is dead or on its way out.
771        if not self._root_widget or self._root_widget.transitioning_out:
772            return
773
774        if self._transitioning_out:
775            return
776
777        bui.containerwidget(edit=self._root_widget, transition='out_left')
778
779        # Note: Make sure we don't pass anything here that would
780        # capture 'self'. (a lambda would implicitly do this by capturing
781        # the stack frame).
782        restorecall = partial(
783            _restore_get_tickets_window,
784            self._modal,
785            self._from_modal_store,
786            self._store_back_location,
787        )
788
789        window = GetTokensWindow(
790            transition='in_right',
791            restore_previous_call=restorecall,
792        ).get_root_widget()
793        if not self._modal and not self._from_modal_store:
794            assert bui.app.classic is not None
795            bui.app.ui_v1.set_main_menu_window(
796                window, from_window=self._root_widget
797            )
798        self._transitioning_out = True
799
800    def _back(self) -> None:
801        from bauiv1lib.store import browser
802
803        # No-op if our underlying widget is dead or on its way out.
804        if not self._root_widget or self._root_widget.transitioning_out:
805            return
806
807        if self._transitioning_out:
808            return
809
810        bui.containerwidget(
811            edit=self._root_widget, transition=self._transition_out
812        )
813        if not self._modal:
814            window = browser.StoreBrowserWindow(
815                transition='in_left',
816                modal=self._from_modal_store,
817                back_location=self._store_back_location,
818            ).get_root_widget()
819            if not self._from_modal_store:
820                assert bui.app.classic is not None
821                bui.app.ui_v1.set_main_menu_window(
822                    window, from_window=self._root_widget
823                )
824        self._transitioning_out = True

Window for purchasing/acquiring classic tickets.

GetTicketsWindow( transition: str = 'in_right', from_modal_store: bool = False, modal: bool = False, origin_widget: _bauiv1.Widget | None = None, store_back_location: str | None = None)
 21    def __init__(
 22        self,
 23        transition: str = 'in_right',
 24        from_modal_store: bool = False,
 25        modal: bool = False,
 26        origin_widget: bui.Widget | None = None,
 27        store_back_location: str | None = None,
 28    ):
 29        # pylint: disable=too-many-statements
 30        # pylint: disable=too-many-locals
 31
 32        plus = bui.app.plus
 33        assert plus is not None
 34
 35        bui.set_analytics_screen('Get Tickets Window')
 36
 37        self._transitioning_out = False
 38        self._store_back_location = store_back_location  # ew.
 39
 40        self._ad_button_greyed = False
 41        self._smooth_update_timer: bui.AppTimer | None = None
 42        self._ad_button = None
 43        self._ad_label = None
 44        self._ad_image = None
 45        self._ad_time_text = None
 46
 47        # If they provided an origin-widget, scale up from that.
 48        scale_origin: tuple[float, float] | None
 49        if origin_widget is not None:
 50            self._transition_out = 'out_scale'
 51            scale_origin = origin_widget.get_screen_space_center()
 52            transition = 'in_scale'
 53        else:
 54            self._transition_out = 'out_right'
 55            scale_origin = None
 56
 57        assert bui.app.classic is not None
 58        uiscale = bui.app.ui_v1.uiscale
 59        self._width = 1000.0 if uiscale is bui.UIScale.SMALL else 800.0
 60        x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0
 61        self._height = 480.0
 62
 63        self._modal = modal
 64        self._from_modal_store = from_modal_store
 65        self._r = 'getTicketsWindow'
 66
 67        top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
 68
 69        super().__init__(
 70            root_widget=bui.containerwidget(
 71                size=(self._width, self._height + top_extra),
 72                transition=transition,
 73                scale_origin_stack_offset=scale_origin,
 74                color=(0.4, 0.37, 0.55),
 75                scale=(
 76                    1.63
 77                    if uiscale is bui.UIScale.SMALL
 78                    else 1.2 if uiscale is bui.UIScale.MEDIUM else 1.0
 79                ),
 80                stack_offset=(
 81                    (0, -3) if uiscale is bui.UIScale.SMALL else (0, 0)
 82                ),
 83            )
 84        )
 85
 86        btn = bui.buttonwidget(
 87            parent=self._root_widget,
 88            position=(55 + x_inset, self._height - 79),
 89            size=(140, 60),
 90            scale=1.0,
 91            autoselect=True,
 92            label=bui.Lstr(resource='doneText' if modal else 'backText'),
 93            button_type='regular' if modal else 'back',
 94            on_activate_call=self._back,
 95        )
 96
 97        bui.containerwidget(edit=self._root_widget, cancel_button=btn)
 98
 99        bui.textwidget(
100            parent=self._root_widget,
101            position=(self._width * 0.5 - 15, self._height - 47),
102            size=(0, 0),
103            color=bui.app.ui_v1.title_color,
104            scale=1.2,
105            h_align='right',
106            v_align='center',
107            text=bui.Lstr(resource=f'{self._r}.titleText'),
108            # text='Testing really long text here blah blah',
109            maxwidth=260,
110        )
111
112        # Get Tokens button
113        bui.buttonwidget(
114            parent=self._root_widget,
115            position=(self._width * 0.5, self._height - 72),
116            color=(0.65, 0.5, 0.7),
117            textcolor=bui.app.ui_v1.title_color,
118            size=(190, 50),
119            autoselect=True,
120            label=bui.Lstr(resource='tokens.getTokensText'),
121            on_activate_call=self._get_tokens_press,
122        )
123
124        # 'New!' by tokens button
125        bui.textwidget(
126            parent=self._root_widget,
127            text=bui.Lstr(resource='newExclaimText'),
128            position=(self._width * 0.5 + 25, self._height - 32),
129            size=(0, 0),
130            color=(1, 1, 0, 1.0),
131            rotate=22,
132            shadow=1.0,
133            maxwidth=150,
134            h_align='center',
135            v_align='center',
136            scale=0.7,
137        )
138
139        if not modal:
140            bui.buttonwidget(
141                edit=btn,
142                button_type='backSmall',
143                size=(60, 60),
144                label=bui.charstr(bui.SpecialChar.BACK),
145            )
146
147        b_size = (220.0, 180.0)
148        v = self._height - b_size[1] - 80
149        spacing = 1
150
151        self._ad_button = None
152
153        def _add_button(
154            item: str,
155            position: tuple[float, float],
156            size: tuple[float, float],
157            label: bui.Lstr,
158            price: str | None = None,
159            tex_name: str | None = None,
160            tex_opacity: float = 1.0,
161            tex_scale: float = 1.0,
162            enabled: bool = True,
163            text_scale: float = 1.0,
164        ) -> bui.Widget:
165            btn2 = bui.buttonwidget(
166                parent=self._root_widget,
167                position=position,
168                button_type='square',
169                size=size,
170                label='',
171                autoselect=True,
172                color=None if enabled else (0.5, 0.5, 0.5),
173                on_activate_call=(
174                    bui.Call(self._purchase, item)
175                    if enabled
176                    else self._disabled_press
177                ),
178            )
179            txt = bui.textwidget(
180                parent=self._root_widget,
181                text=label,
182                position=(
183                    position[0] + size[0] * 0.5,
184                    position[1] + size[1] * 0.3,
185                ),
186                scale=text_scale,
187                maxwidth=size[0] * 0.75,
188                size=(0, 0),
189                h_align='center',
190                v_align='center',
191                draw_controller=btn2,
192                color=(0.7, 0.9, 0.7, 1.0 if enabled else 0.2),
193            )
194            if price is not None and enabled:
195                bui.textwidget(
196                    parent=self._root_widget,
197                    text=price,
198                    position=(
199                        position[0] + size[0] * 0.5,
200                        position[1] + size[1] * 0.17,
201                    ),
202                    scale=0.7,
203                    maxwidth=size[0] * 0.75,
204                    size=(0, 0),
205                    h_align='center',
206                    v_align='center',
207                    draw_controller=btn2,
208                    color=(0.4, 0.9, 0.4, 1.0),
209                )
210            i = None
211            if tex_name is not None:
212                tex_size = 90.0 * tex_scale
213                i = bui.imagewidget(
214                    parent=self._root_widget,
215                    texture=bui.gettexture(tex_name),
216                    position=(
217                        position[0] + size[0] * 0.5 - tex_size * 0.5,
218                        position[1] + size[1] * 0.66 - tex_size * 0.5,
219                    ),
220                    size=(tex_size, tex_size),
221                    draw_controller=btn2,
222                    opacity=tex_opacity * (1.0 if enabled else 0.25),
223                )
224            if item == 'ad':
225                self._ad_button = btn2
226                self._ad_label = txt
227                assert i is not None
228                self._ad_image = i
229                self._ad_time_text = bui.textwidget(
230                    parent=self._root_widget,
231                    text='1m 10s',
232                    position=(
233                        position[0] + size[0] * 0.5,
234                        position[1] + size[1] * 0.5,
235                    ),
236                    scale=text_scale * 1.2,
237                    maxwidth=size[0] * 0.85,
238                    size=(0, 0),
239                    h_align='center',
240                    v_align='center',
241                    draw_controller=btn2,
242                    color=(0.4, 0.9, 0.4, 1.0),
243                )
244            return btn2
245
246        rsrc = f'{self._r}.ticketsText'
247
248        c2txt = bui.Lstr(
249            resource=rsrc,
250            subs=[
251                (
252                    '${COUNT}',
253                    str(
254                        plus.get_v1_account_misc_read_val('tickets2Amount', 500)
255                    ),
256                )
257            ],
258        )
259        c3txt = bui.Lstr(
260            resource=rsrc,
261            subs=[
262                (
263                    '${COUNT}',
264                    str(
265                        plus.get_v1_account_misc_read_val(
266                            'tickets3Amount', 1500
267                        )
268                    ),
269                )
270            ],
271        )
272        c4txt = bui.Lstr(
273            resource=rsrc,
274            subs=[
275                (
276                    '${COUNT}',
277                    str(
278                        plus.get_v1_account_misc_read_val(
279                            'tickets4Amount', 5000
280                        )
281                    ),
282                )
283            ],
284        )
285        c5txt = bui.Lstr(
286            resource=rsrc,
287            subs=[
288                (
289                    '${COUNT}',
290                    str(
291                        plus.get_v1_account_misc_read_val(
292                            'tickets5Amount', 15000
293                        )
294                    ),
295                )
296            ],
297        )
298
299        h = 110.0
300
301        # Enable buttons if we have prices.
302        tickets2_price = plus.get_price('tickets2')
303        tickets3_price = plus.get_price('tickets3')
304        tickets4_price = plus.get_price('tickets4')
305        tickets5_price = plus.get_price('tickets5')
306
307        # TEMP
308        # tickets1_price = '$0.99'
309        # tickets2_price = '$4.99'
310        # tickets3_price = '$9.99'
311        # tickets4_price = '$19.99'
312        # tickets5_price = '$49.99'
313
314        _add_button(
315            'tickets2',
316            enabled=(tickets2_price is not None),
317            position=(
318                self._width * 0.5 - spacing * 1.5 - b_size[0] * 2.0 + h,
319                v,
320            ),
321            size=b_size,
322            label=c2txt,
323            price=tickets2_price,
324            tex_name='ticketsMore',
325        )  # 0.99-ish
326        _add_button(
327            'tickets3',
328            enabled=(tickets3_price is not None),
329            position=(
330                self._width * 0.5 - spacing * 0.5 - b_size[0] * 1.0 + h,
331                v,
332            ),
333            size=b_size,
334            label=c3txt,
335            price=tickets3_price,
336            tex_name='ticketRoll',
337        )  # 4.99-ish
338        v -= b_size[1] - 5
339        _add_button(
340            'tickets4',
341            enabled=(tickets4_price is not None),
342            position=(
343                self._width * 0.5 - spacing * 1.5 - b_size[0] * 2.0 + h,
344                v,
345            ),
346            size=b_size,
347            label=c4txt,
348            price=tickets4_price,
349            tex_name='ticketRollBig',
350            tex_scale=1.2,
351        )  # 9.99-ish
352        _add_button(
353            'tickets5',
354            enabled=(tickets5_price is not None),
355            position=(
356                self._width * 0.5 - spacing * 0.5 - b_size[0] * 1.0 + h,
357                v,
358            ),
359            size=b_size,
360            label=c5txt,
361            price=tickets5_price,
362            tex_name='ticketRolls',
363            tex_scale=1.2,
364        )  # 19.99-ish
365
366        self._enable_ad_button = plus.has_video_ads()
367        h = self._width * 0.5 + 110.0
368        v = self._height - b_size[1] - 115.0
369
370        if self._enable_ad_button:
371            h_offs = 35
372            b_size_3 = (150, 120)
373            cdb = _add_button(
374                'ad',
375                position=(h + h_offs, v),
376                size=b_size_3,
377                label=bui.Lstr(
378                    resource=f'{self._r}.ticketsFromASponsorText',
379                    subs=[
380                        (
381                            '${COUNT}',
382                            str(
383                                plus.get_v1_account_misc_read_val(
384                                    'sponsorTickets', 5
385                                )
386                            ),
387                        )
388                    ],
389                ),
390                tex_name='ticketsMore',
391                enabled=self._enable_ad_button,
392                tex_opacity=0.6,
393                tex_scale=0.7,
394                text_scale=0.7,
395            )
396            bui.buttonwidget(edit=cdb, color=(0.65, 0.5, 0.7))
397
398            self._ad_free_text = bui.textwidget(
399                parent=self._root_widget,
400                text=bui.Lstr(resource=f'{self._r}.freeText'),
401                position=(
402                    h + h_offs + b_size_3[0] * 0.5,
403                    v + b_size_3[1] * 0.5 + 25,
404                ),
405                size=(0, 0),
406                color=(1, 1, 0, 1.0),
407                draw_controller=cdb,
408                rotate=15,
409                shadow=1.0,
410                maxwidth=150,
411                h_align='center',
412                v_align='center',
413                scale=1.0,
414            )
415            v -= 125
416        else:
417            v -= 20
418
419        if bool(True):
420            h_offs = 35
421            b_size_3 = (150, 120)
422            cdb = _add_button(
423                'app_invite',
424                position=(h + h_offs, v),
425                size=b_size_3,
426                label=bui.Lstr(
427                    resource='gatherWindow.earnTicketsForRecommendingText',
428                    subs=[
429                        (
430                            '${COUNT}',
431                            str(
432                                plus.get_v1_account_misc_read_val(
433                                    'sponsorTickets', 5
434                                )
435                            ),
436                        )
437                    ],
438                ),
439                tex_name='ticketsMore',
440                enabled=True,
441                tex_opacity=0.6,
442                tex_scale=0.7,
443                text_scale=0.7,
444            )
445            bui.buttonwidget(edit=cdb, color=(0.65, 0.5, 0.7))
446
447            bui.textwidget(
448                parent=self._root_widget,
449                text=bui.Lstr(resource=f'{self._r}.freeText'),
450                position=(
451                    h + h_offs + b_size_3[0] * 0.5,
452                    v + b_size_3[1] * 0.5 + 25,
453                ),
454                size=(0, 0),
455                color=(1, 1, 0, 1.0),
456                draw_controller=cdb,
457                rotate=15,
458                shadow=1.0,
459                maxwidth=150,
460                h_align='center',
461                v_align='center',
462                scale=1.0,
463            )
464            tc_y_offs = 0
465        else:
466            tc_y_offs = 0
467
468        h = self._width - (185 + x_inset)
469        v = self._height - 105 + tc_y_offs
470
471        txt1 = (
472            bui.Lstr(resource=f'{self._r}.youHaveText')
473            .evaluate()
474            .partition('${COUNT}')[0]
475            .strip()
476        )
477        txt2 = (
478            bui.Lstr(resource=f'{self._r}.youHaveText')
479            .evaluate()
480            .rpartition('${COUNT}')[-1]
481            .strip()
482        )
483
484        bui.textwidget(
485            parent=self._root_widget,
486            text=txt1,
487            position=(h, v),
488            size=(0, 0),
489            color=(0.5, 0.5, 0.6),
490            maxwidth=200,
491            h_align='center',
492            v_align='center',
493            scale=0.8,
494        )
495        v -= 30
496        self._ticket_count_text = bui.textwidget(
497            parent=self._root_widget,
498            position=(h, v),
499            size=(0, 0),
500            color=(0.2, 1.0, 0.2),
501            maxwidth=200,
502            h_align='center',
503            v_align='center',
504            scale=1.6,
505        )
506        v -= 30
507        bui.textwidget(
508            parent=self._root_widget,
509            text=txt2,
510            position=(h, v),
511            size=(0, 0),
512            color=(0.5, 0.5, 0.6),
513            maxwidth=200,
514            h_align='center',
515            v_align='center',
516            scale=0.8,
517        )
518
519        self._ticking_sound: bui.Sound | None = None
520        self._smooth_ticket_count: float | None = None
521        self._ticket_count = 0
522        self._update()
523        self._update_timer = bui.AppTimer(
524            1.0, bui.WeakCall(self._update), repeat=True
525        )
526        self._smooth_increase_speed = 1.0
Inherited Members
bauiv1._uitypes.Window
get_root_widget
def show_get_tickets_prompt() -> None:
848def show_get_tickets_prompt() -> None:
849    """Show a 'not enough tickets' prompt with an option to purchase more.
850
851    Note that the purchase option may not always be available
852    depending on the build of the game.
853    """
854    from bauiv1lib.confirm import ConfirmWindow
855
856    assert bui.app.classic is not None
857
858    if bui.app.classic.allow_ticket_purchases:
859        ConfirmWindow(
860            bui.Lstr(
861                translate=(
862                    'serverResponses',
863                    'You don\'t have enough tickets for this!',
864                )
865            ),
866            lambda: GetTicketsWindow(modal=True),
867            ok_text=bui.Lstr(resource='getTicketsWindow.titleText'),
868            width=460,
869            height=130,
870        )
871    else:
872        ConfirmWindow(
873            bui.Lstr(
874                translate=(
875                    'serverResponses',
876                    'You don\'t have enough tickets for this!',
877                )
878            ),
879            cancel_button=False,
880            width=460,
881            height=130,
882        )

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

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