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