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