bastd.ui.getcurrency

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

Window for purchasing/acquiring currency.

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

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.