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