bauiv1lib.chest
Provides chest related ui.
1# Released under the MIT License. See LICENSE for details. 2# 3# pylint: disable=too-many-lines 4"""Provides chest related ui.""" 5 6from __future__ import annotations 7 8import math 9import random 10from typing import override, TYPE_CHECKING 11 12from efro.util import strict_partial 13import bacommon.bs 14import bauiv1 as bui 15 16if TYPE_CHECKING: 17 import datetime 18 19 import baclassic 20 21_g_open_voices: list[tuple[float, str, float]] = [] 22 23 24class ChestWindow(bui.MainWindow): 25 """Allows viewing and performing operations on a chest.""" 26 27 def __init__( 28 self, 29 index: int, 30 transition: str | None = 'in_right', 31 origin_widget: bui.Widget | None = None, 32 ): 33 self._index = index 34 35 assert bui.app.classic is not None 36 uiscale = bui.app.ui_v1.uiscale 37 self._width = 1200 if uiscale is bui.UIScale.SMALL else 650 38 self._height = 800 if uiscale is bui.UIScale.SMALL else 450 39 self._action_in_flight = False 40 self._open_now_button: bui.Widget | None = None 41 self._open_now_spinner: bui.Widget | None = None 42 self._open_now_texts: list[bui.Widget] = [] 43 self._open_now_images: list[bui.Widget] = [] 44 self._watch_ad_button: bui.Widget | None = None 45 self._time_string_timer: bui.AppTimer | None = None 46 self._time_string_text: bui.Widget | None = None 47 self._prizesets: list[bacommon.bs.ChestInfoResponse.Chest.PrizeSet] = [] 48 self._prizeindex = -1 49 self._prizesettxts: dict[int, list[bui.Widget]] = {} 50 self._prizesetimgs: dict[int, list[bui.Widget]] = {} 51 self._chestdisplayinfo: baclassic.ChestAppearanceDisplayInfo | None = ( 52 None 53 ) 54 55 # The set of widgets we keep when doing a clear. 56 self._core_widgets: list[bui.Widget] = [] 57 58 # Do some fancy math to fill all available screen area up to the 59 # size of our backing container. This lets us fit to the exact 60 # screen shape at small ui scale. 61 screensize = bui.get_virtual_screen_size() 62 scale = ( 63 1.8 64 if uiscale is bui.UIScale.SMALL 65 else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.9 66 ) 67 # Calc screen size in our local container space and clamp to a 68 # bit smaller than our container size. 69 target_height = min(self._height - 120, screensize[1] / scale) 70 71 # To get top/left coords, go to the center of our window and 72 # offset by half the width/height of our target area. 73 self._yoffstop = 0.5 * self._height + 0.5 * target_height + 18 74 75 # Offset for stuff we want centered. 76 self._yoffs = 0.5 * self._height + ( 77 220 if uiscale is bui.UIScale.SMALL else 190 78 ) 79 80 self._chest_yoffs = self._yoffs - 223 81 82 super().__init__( 83 root_widget=bui.containerwidget( 84 size=(self._width, self._height), 85 toolbar_visibility='menu_full', 86 scale=scale, 87 ), 88 transition=transition, 89 origin_widget=origin_widget, 90 # We're affected by screen size only at small ui-scale. 91 refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL, 92 ) 93 94 # Tell the root-ui to stop updating toolbar values immediately; 95 # this allows it to run animations based on the results of our 96 # chest opening. 97 bui.root_ui_pause_updates() 98 self._root_ui_updates_paused = True 99 100 self._title_text = bui.textwidget( 101 parent=self._root_widget, 102 position=( 103 self._width * 0.5, 104 self._yoffstop - (36 if uiscale is bui.UIScale.SMALL else 10), 105 ), 106 size=(0, 0), 107 text=bui.Lstr( 108 resource='chests.slotText', 109 subs=[('${NUM}', str(index + 1))], 110 ), 111 color=bui.app.ui_v1.title_color, 112 maxwidth=110.0 if uiscale is bui.UIScale.SMALL else 200, 113 scale=0.9 if uiscale is bui.UIScale.SMALL else 1.1, 114 h_align='center', 115 v_align='center', 116 ) 117 self._core_widgets.append(self._title_text) 118 119 if uiscale is bui.UIScale.SMALL: 120 bui.containerwidget( 121 edit=self._root_widget, on_cancel_call=self.main_window_back 122 ) 123 else: 124 btn = bui.buttonwidget( 125 parent=self._root_widget, 126 position=(50, self._yoffs - 44), 127 size=(60, 55), 128 scale=0.8, 129 label=bui.charstr(bui.SpecialChar.BACK), 130 button_type='backSmall', 131 extra_touch_border_scale=2.0, 132 autoselect=True, 133 on_activate_call=self.main_window_back, 134 ) 135 bui.containerwidget(edit=self._root_widget, cancel_button=btn) 136 self._core_widgets.append(btn) 137 138 # Note: Don't need to explicitly clean this up. Just not adding 139 # it to core_widgets so it will go away on next reset. 140 self._loadingspinner = bui.spinnerwidget( 141 parent=self._root_widget, 142 position=(self._width * 0.5, self._height * 0.5), 143 size=48, 144 style='bomb', 145 ) 146 147 self._infotext = bui.textwidget( 148 parent=self._root_widget, 149 position=(self._width * 0.5, self._yoffs - 200), 150 size=(0, 0), 151 text='', 152 maxwidth=700, 153 scale=0.8, 154 color=(0.6, 0.5, 0.6), 155 h_align='center', 156 v_align='center', 157 ) 158 self._core_widgets.append(self._infotext) 159 160 plus = bui.app.plus 161 if plus is None: 162 self._error('Plus feature-set is not present.') 163 return 164 165 if plus.accounts.primary is None: 166 self._error(bui.Lstr(resource='notSignedInText')) 167 return 168 169 # Start by showing info/options for our target chest. Note that 170 # we always ask the server for these values even though we may 171 # have them through our appmode subscription which updates the 172 # chest UI. This is because the wait_for_connectivity() 173 # mechanism will often bring our window up a split second before 174 # the chest subscription receives its first values which would 175 # lead us to incorrectly think there is no chest there. If we 176 # want to optimize this in the future we could perhaps use local 177 # values only if there is a chest present in them. 178 assert not self._action_in_flight 179 self._action_in_flight = True 180 with plus.accounts.primary: 181 plus.cloud.send_message_cb( 182 bacommon.bs.ChestInfoMessage(chest_id=str(self._index)), 183 on_response=bui.WeakCall(self._on_chest_info_response), 184 ) 185 186 def __del__(self) -> None: 187 188 # Make sure UI updates are resumed if we haven't done so. 189 if self._root_ui_updates_paused: 190 bui.root_ui_resume_updates() 191 192 @override 193 def get_main_window_state(self) -> bui.MainWindowState: 194 # Support recreating our window for back/refresh purposes. 195 cls = type(self) 196 197 # Pull anything we need from self out here; if we do it in the 198 # lambda we keep self alive which is bad. 199 index = self._index 200 201 return bui.BasicMainWindowState( 202 create_call=lambda transition, origin_widget: cls( 203 index=index, transition=transition, origin_widget=origin_widget 204 ) 205 ) 206 207 def _update_time_display(self, unlock_time: datetime.datetime) -> None: 208 # Once text disappears, kill our timer. 209 if not self._time_string_text: 210 self._time_string_timer = None 211 return 212 now = bui.utc_now_cloud() 213 secs_till_open = max(0.0, (unlock_time - now).total_seconds()) 214 tstr = ( 215 bui.timestring(secs_till_open, centi=False) 216 if secs_till_open > 0 217 else '' 218 ) 219 bui.textwidget(edit=self._time_string_text, text=tstr) 220 221 def _on_chest_info_response( 222 self, response: bacommon.bs.ChestInfoResponse | Exception 223 ) -> None: 224 assert self._action_in_flight # Should be us. 225 self._action_in_flight = False 226 227 if isinstance(response, Exception): 228 self._error( 229 bui.Lstr(resource='internal.unableToCompleteTryAgainText'), 230 minor=True, 231 ) 232 return 233 234 if response.chest is None: 235 self._show_about_chest_slots() 236 return 237 238 assert response.user_tokens is not None 239 self._show_chest_actions(response.user_tokens, response.chest) 240 241 def _on_chest_action_response( 242 self, response: bacommon.bs.ChestActionResponse | Exception 243 ) -> None: 244 assert self._action_in_flight # Should be us. 245 self._action_in_flight = False 246 247 # Communication/local error: 248 if isinstance(response, Exception): 249 self._error( 250 bui.Lstr(resource='internal.unableToCompleteTryAgainText'), 251 minor=True, 252 ) 253 return 254 255 # Server-side error: 256 if response.error is not None: 257 self._error(bui.Lstr(translate=('serverResponses', response.error))) 258 return 259 260 # Show any bundled success message. 261 if response.success_msg is not None: 262 bui.screenmessage( 263 bui.Lstr(translate=('serverResponses', response.success_msg)), 264 color=(0, 1.0, 0), 265 ) 266 bui.getsound('cashRegister').play() 267 268 # Show any bundled warning. 269 if response.warning is not None: 270 bui.screenmessage( 271 bui.Lstr(translate=('serverResponses', response.warning)), 272 color=(1, 0.5, 0), 273 ) 274 bui.getsound('error').play() 275 276 # If we just paid for something, make a sound accordingly. 277 if bool(False): # Hmm maybe this feels odd. 278 if response.tokens_charged > 0: 279 bui.getsound('cashRegister').play() 280 281 # If there's contents listed in the response, show them. 282 if response.contents is not None: 283 self._show_chest_contents(response) 284 else: 285 # Otherwise we're done here; just close out our UI. 286 self.main_window_back() 287 288 def _show_chest_actions( 289 self, user_tokens: int, chest: bacommon.bs.ChestInfoResponse.Chest 290 ) -> None: 291 """Show state for our chest.""" 292 # pylint: disable=too-many-locals 293 # pylint: disable=cyclic-import 294 from baclassic import ( 295 ClassicAppMode, 296 CHEST_APPEARANCE_DISPLAY_INFOS, 297 CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT, 298 ) 299 300 plus = bui.app.plus 301 assert plus is not None 302 303 # We expect to be run under classic app mode. 304 mode = bui.app.mode 305 if not isinstance(mode, ClassicAppMode): 306 self._error('Classic app mode not active.') 307 return 308 309 self._reset() 310 311 self._chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get( 312 chest.appearance, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT 313 ) 314 315 bui.textwidget( 316 edit=self._title_text, 317 text=bui.Lstr( 318 translate=('displayItemNames', chest.appearance.pretty_name) 319 ), 320 ) 321 322 imgsize = 145 323 bui.imagewidget( 324 parent=self._root_widget, 325 position=( 326 self._width * 0.5 - imgsize * 0.5, 327 # self._height - 223 + self._yoffs, 328 self._chest_yoffs, 329 ), 330 color=self._chestdisplayinfo.color, 331 size=(imgsize, imgsize), 332 texture=bui.gettexture(self._chestdisplayinfo.texclosed), 333 tint_texture=bui.gettexture(self._chestdisplayinfo.texclosedtint), 334 tint_color=self._chestdisplayinfo.tint, 335 tint2_color=self._chestdisplayinfo.tint2, 336 ) 337 338 # Store the prize-sets so we can display odds/etc. Sort them 339 # with largest weights first. 340 self._prizesets = sorted( 341 chest.prizesets, key=lambda s: s.weight, reverse=True 342 ) 343 344 if chest.unlock_tokens > 0: 345 lsize = 30 346 bui.imagewidget( 347 parent=self._root_widget, 348 position=( 349 self._width * 0.5 - imgsize * 0.4 - lsize * 0.5, 350 # self._height - 223 + 27.0 + self._yoffs, 351 self._chest_yoffs + 27.0, 352 ), 353 size=(lsize, lsize), 354 texture=bui.gettexture('lock'), 355 ) 356 357 # Time string. 358 if chest.unlock_tokens != 0: 359 self._time_string_text = bui.textwidget( 360 parent=self._root_widget, 361 position=( 362 self._width * 0.5, 363 # self._height - 85 + self._yoffs 364 self._yoffs - 85, 365 ), 366 size=(0, 0), 367 text='', 368 maxwidth=700, 369 scale=0.6, 370 color=(0.6, 1.0, 0.6), 371 h_align='center', 372 v_align='center', 373 ) 374 self._update_time_display(chest.unlock_time) 375 self._time_string_timer = bui.AppTimer( 376 1.0, 377 repeat=True, 378 call=bui.WeakCall(self._update_time_display, chest.unlock_time), 379 ) 380 381 # Allow watching an ad IF the server tells us we can AND we have 382 # an ad ready to show. 383 show_ad_button = ( 384 chest.unlock_tokens > 0 385 and chest.ad_allow 386 and plus.have_incentivized_ad() 387 ) 388 389 bwidth = 130 390 bheight = 90 391 bposy = -330 if chest.unlock_tokens == 0 else -340 392 hspace = 20 393 boffsx = (hspace * -0.5 - bwidth * 0.5) if show_ad_button else 0.0 394 395 self._open_now_button = bui.buttonwidget( 396 parent=self._root_widget, 397 position=( 398 self._width * 0.5 - bwidth * 0.5 + boffsx, 399 self._yoffs + bposy, 400 ), 401 size=(bwidth, bheight), 402 label='', 403 button_type='square', 404 autoselect=True, 405 on_activate_call=bui.WeakCall( 406 self._open_press, user_tokens, chest.unlock_tokens 407 ), 408 enable_sound=False, 409 ) 410 self._open_now_images = [] 411 self._open_now_texts = [] 412 413 iconsize = 50 414 if chest.unlock_tokens == 0: 415 self._open_now_texts.append( 416 bui.textwidget( 417 parent=self._root_widget, 418 text=bui.Lstr(resource='openText'), 419 position=( 420 self._width * 0.5 + boffsx, 421 self._yoffs + bposy + bheight * 0.5, 422 ), 423 color=(0, 1, 0), 424 draw_controller=self._open_now_button, 425 scale=0.7, 426 maxwidth=bwidth * 0.8, 427 size=(0, 0), 428 h_align='center', 429 v_align='center', 430 ) 431 ) 432 else: 433 self._open_now_texts.append( 434 bui.textwidget( 435 parent=self._root_widget, 436 text=bui.Lstr(resource='openNowText'), 437 position=( 438 self._width * 0.5 + boffsx, 439 self._yoffs + bposy + bheight * 1.15, 440 ), 441 maxwidth=bwidth * 0.8, 442 scale=0.7, 443 color=(0.7, 1, 0.7), 444 size=(0, 0), 445 h_align='center', 446 v_align='center', 447 ) 448 ) 449 self._open_now_images.append( 450 bui.imagewidget( 451 parent=self._root_widget, 452 size=(iconsize, iconsize), 453 position=( 454 self._width * 0.5 - iconsize * 0.5 + boffsx, 455 self._yoffs + bposy + bheight * 0.35, 456 ), 457 draw_controller=self._open_now_button, 458 texture=bui.gettexture('coin'), 459 ) 460 ) 461 self._open_now_texts.append( 462 bui.textwidget( 463 parent=self._root_widget, 464 text=bui.Lstr( 465 resource='tokens.numTokensText', 466 subs=[('${COUNT}', str(chest.unlock_tokens))], 467 ), 468 position=( 469 self._width * 0.5 + boffsx, 470 self._yoffs + bposy + bheight * 0.25, 471 ), 472 scale=0.65, 473 color=(0, 1, 0), 474 draw_controller=self._open_now_button, 475 maxwidth=bwidth * 0.8, 476 size=(0, 0), 477 h_align='center', 478 v_align='center', 479 ) 480 ) 481 self._open_now_spinner = bui.spinnerwidget( 482 parent=self._root_widget, 483 position=( 484 self._width * 0.5 + boffsx, 485 self._yoffs + bposy + 0.5 * bheight, 486 ), 487 visible=False, 488 ) 489 490 if show_ad_button: 491 bui.textwidget( 492 parent=self._root_widget, 493 text=bui.Lstr(resource='chests.reduceWaitText'), 494 position=( 495 self._width * 0.5 + hspace * 0.5 + bwidth * 0.5, 496 self._yoffs + bposy + bheight * 1.15, 497 ), 498 maxwidth=bwidth * 0.8, 499 scale=0.7, 500 color=(0.7, 1, 0.7), 501 size=(0, 0), 502 h_align='center', 503 v_align='center', 504 ) 505 self._watch_ad_button = bui.buttonwidget( 506 parent=self._root_widget, 507 position=( 508 self._width * 0.5 + hspace * 0.5, 509 self._yoffs + bposy, 510 ), 511 size=(bwidth, bheight), 512 label='', 513 button_type='square', 514 autoselect=True, 515 on_activate_call=bui.WeakCall(self._watch_ad_press), 516 enable_sound=False, 517 ) 518 bui.imagewidget( 519 parent=self._root_widget, 520 size=(iconsize, iconsize), 521 position=( 522 self._width * 0.5 523 + hspace * 0.5 524 + bwidth * 0.5 525 - iconsize * 0.5, 526 self._yoffs + bposy + bheight * 0.35, 527 ), 528 draw_controller=self._watch_ad_button, 529 color=(1.5, 1.0, 2.0), 530 texture=bui.gettexture('tv'), 531 ) 532 # Note to self: AdMob requires rewarded ad usage 533 # specifically says 'Ad' in it. 534 bui.textwidget( 535 parent=self._root_widget, 536 text=bui.Lstr(resource='watchAnAdText'), 537 position=( 538 self._width * 0.5 + hspace * 0.5 + bwidth * 0.5, 539 self._yoffs + bposy + bheight * 0.25, 540 ), 541 scale=0.65, 542 color=(0, 1, 0), 543 draw_controller=self._watch_ad_button, 544 maxwidth=bwidth * 0.8, 545 size=(0, 0), 546 h_align='center', 547 v_align='center', 548 ) 549 550 self._show_odds(initial_highlighted_row=-1) 551 552 def _highlight_odds_row(self, row: int, extra: bool = False) -> None: 553 554 for rindex, imgs in self._prizesetimgs.items(): 555 opacity = ( 556 (0.9 if extra else 0.75) 557 if rindex == row 558 else (0.4 if extra else 0.5) 559 ) 560 for img in imgs: 561 if img: 562 bui.imagewidget(edit=img, opacity=opacity) 563 564 for rindex, txts in self._prizesettxts.items(): 565 opacity = ( 566 (0.9 if extra else 0.75) 567 if rindex == row 568 else (0.4 if extra else 0.5) 569 ) 570 for txt in txts: 571 if txt: 572 bui.textwidget(edit=txt, color=(0.7, 0.65, 1, opacity)) 573 574 def _show_odds( 575 self, 576 *, 577 initial_highlighted_row: int, 578 initial_highlighted_extra: bool = False, 579 ) -> None: 580 # pylint: disable=too-many-locals 581 xoffs = 110 582 583 totalweight = max(0.001, sum(t.weight for t in self._prizesets)) 584 585 rowheight = 25 586 totalheight = (len(self._prizesets) + 1) * rowheight 587 x = self._width * 0.5 + xoffs 588 y = self._yoffs - 150.0 + totalheight * 0.5 589 590 # Title. 591 bui.textwidget( 592 parent=self._root_widget, 593 text=bui.Lstr(resource='chests.prizeOddsText'), 594 color=(0.7, 0.65, 1, 0.5), 595 flatness=1.0, 596 shadow=1.0, 597 position=(x, y), 598 scale=0.55, 599 size=(0, 0), 600 h_align='left', 601 v_align='center', 602 ) 603 y -= 5.0 604 605 prizesettxts: list[bui.Widget] 606 prizesetimgs: list[bui.Widget] 607 608 def _mkicon(img: str) -> None: 609 iconsize = 20.0 610 nonlocal x 611 nonlocal prizesetimgs 612 prizesetimgs.append( 613 bui.imagewidget( 614 parent=self._root_widget, 615 size=(iconsize, iconsize), 616 position=(x, y - iconsize * 0.5), 617 texture=bui.gettexture(img), 618 opacity=0.4, 619 ) 620 ) 621 x += iconsize 622 623 def _mktxt(txt: str, advance: bool = True) -> None: 624 tscale = 0.45 625 nonlocal x 626 nonlocal prizesettxts 627 prizesettxts.append( 628 bui.textwidget( 629 parent=self._root_widget, 630 text=txt, 631 flatness=1.0, 632 shadow=1.0, 633 position=(x, y), 634 scale=tscale, 635 size=(0, 0), 636 h_align='left', 637 v_align='center', 638 ) 639 ) 640 if advance: 641 x += (bui.get_string_width(txt, suppress_warning=True)) * tscale 642 643 self._prizesettxts = {} 644 self._prizesetimgs = {} 645 646 for i, p in enumerate(self._prizesets): 647 prizesettxts = self._prizesettxts.setdefault(i, []) 648 prizesetimgs = self._prizesetimgs.setdefault(i, []) 649 x = self._width * 0.5 + xoffs 650 y -= rowheight 651 percent = 100.0 * p.weight / totalweight 652 653 # Show decimals only if we get very small percentages (looks 654 # better than rounding as '0%'). 655 percenttxt = ( 656 f'{percent:.2f}%:' 657 if percent < 0.095 658 else ( 659 f'{percent:.1f}%:' 660 if percent < 0.95 661 else f'{round(percent)}%:' 662 ) 663 ) 664 665 # We advance manually here to keep values lined up 666 # (otherwise single digit percent rows don't line up with 667 # double digit ones). 668 _mktxt(percenttxt, advance=False) 669 x += 35.0 670 671 for item in p.contents: 672 x += 5.0 673 if isinstance(item.item, bacommon.bs.TicketsDisplayItem): 674 _mktxt(str(item.item.count)) 675 _mkicon('tickets') 676 elif isinstance(item.item, bacommon.bs.TokensDisplayItem): 677 _mktxt(str(item.item.count)) 678 _mkicon('coin') 679 else: 680 # For other cases just fall back on text desc. 681 # 682 # Translate the wrapper description and apply any subs. 683 descfin = bui.Lstr( 684 translate=('serverResponses', item.description) 685 ).evaluate() 686 subs = ( 687 [] 688 if item.description_subs is None 689 else item.description_subs 690 ) 691 assert len(subs) % 2 == 0 # Should always be even. 692 for j in range(0, len(subs) - 1, 2): 693 descfin = descfin.replace(subs[j], subs[j + 1]) 694 _mktxt(descfin) 695 self._highlight_odds_row( 696 initial_highlighted_row, extra=initial_highlighted_extra 697 ) 698 699 def _open_press(self, user_tokens: int, token_payment: int) -> None: 700 from bauiv1lib.gettokens import show_get_tokens_prompt 701 702 bui.getsound('click01').play() 703 704 # Allow only one in-flight action at once. 705 if self._action_in_flight: 706 bui.screenmessage( 707 bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0) 708 ) 709 bui.getsound('error').play() 710 return 711 712 plus = bui.app.plus 713 assert plus is not None 714 715 if plus.accounts.primary is None: 716 self._error(bui.Lstr(resource='notSignedInText')) 717 return 718 719 # Offer to purchase tokens if they don't have enough. 720 if user_tokens < token_payment: 721 # Hack: We disable normal swish for the open button and it 722 # seems weird without a swish here, so explicitly do one. 723 bui.getsound('swish').play() 724 show_get_tokens_prompt() 725 return 726 727 self._action_in_flight = True 728 with plus.accounts.primary: 729 plus.cloud.send_message_cb( 730 bacommon.bs.ChestActionMessage( 731 chest_id=str(self._index), 732 action=bacommon.bs.ChestActionMessage.Action.UNLOCK, 733 token_payment=token_payment, 734 ), 735 on_response=bui.WeakCall(self._on_chest_action_response), 736 ) 737 738 # Convey that something is in progress. 739 if self._open_now_button: 740 bui.spinnerwidget(edit=self._open_now_spinner, visible=True) 741 for twidget in self._open_now_texts: 742 bui.textwidget(edit=twidget, color=(1, 1, 1, 0.2)) 743 for iwidget in self._open_now_images: 744 bui.imagewidget(edit=iwidget, opacity=0.2) 745 746 def _watch_ad_press(self) -> None: 747 748 bui.getsound('click01').play() 749 750 # Allow only one in-flight action at once. 751 if self._action_in_flight: 752 bui.screenmessage( 753 bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0) 754 ) 755 bui.getsound('error').play() 756 return 757 758 assert bui.app.classic is not None 759 760 self._action_in_flight = True 761 bui.app.classic.ads.show_ad_2( 762 'reduce_chest_wait', 763 on_completion_call=bui.WeakCall(self._watch_ad_complete), 764 ) 765 766 # Convey that something is in progress. 767 if self._watch_ad_button: 768 bui.buttonwidget(edit=self._watch_ad_button, color=(0.4, 0.4, 0.4)) 769 770 def _watch_ad_complete(self, actually_showed: bool) -> None: 771 772 assert self._action_in_flight # Should be ad view. 773 self._action_in_flight = False 774 775 if not actually_showed: 776 return 777 778 # Allow only one in-flight action at once. 779 if self._action_in_flight: 780 bui.screenmessage( 781 bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0) 782 ) 783 bui.getsound('error').play() 784 return 785 786 plus = bui.app.plus 787 assert plus is not None 788 789 if plus.accounts.primary is None: 790 self._error(bui.Lstr(resource='notSignedInText')) 791 return 792 793 self._action_in_flight = True 794 with plus.accounts.primary: 795 plus.cloud.send_message_cb( 796 bacommon.bs.ChestActionMessage( 797 chest_id=str(self._index), 798 action=bacommon.bs.ChestActionMessage.Action.AD, 799 token_payment=0, 800 ), 801 on_response=bui.WeakCall(self._on_chest_action_response), 802 ) 803 804 def _reset(self) -> None: 805 """Clear all non-permanent widgets and clear infotext.""" 806 for widget in self._root_widget.get_children(): 807 if widget not in self._core_widgets: 808 widget.delete() 809 bui.textwidget(edit=self._infotext, text='', color=(1, 1, 1)) 810 811 def _error(self, msg: str | bui.Lstr, minor: bool = False) -> None: 812 """Put ourself in an error state with a visible error message.""" 813 self._reset() 814 bui.textwidget( 815 edit=self._infotext, 816 text=msg, 817 color=(1, 0.5, 0.5) if minor else (1, 0, 0), 818 ) 819 820 def _show_about_chest_slots(self) -> None: 821 # No-op if our ui is dead. 822 if not self._root_widget: 823 return 824 825 self._reset() 826 bui.textwidget( 827 edit=self._infotext, 828 text=bui.Lstr(resource='chests.slotDescriptionText'), 829 color=(1, 1, 1), 830 ) 831 832 def _show_chest_contents( 833 self, response: bacommon.bs.ChestActionResponse 834 ) -> None: 835 # pylint: disable=too-many-locals 836 837 from baclassic import show_display_item 838 839 # No-op if our ui is dead. 840 if not self._root_widget: 841 return 842 843 assert response.contents is not None 844 845 # Insert test items for testing. 846 if bool(False): 847 response.contents += [ 848 bacommon.bs.DisplayItemWrapper.for_display_item( 849 bacommon.bs.TestDisplayItem() 850 ) 851 ] 852 853 tincr = 0.4 854 tendoffs = tincr * 4.0 855 toffs = 0.0 856 857 bui.getsound('revUp').play(volume=2.0) 858 859 # Show nothing but the chest icon and animate it shaking. 860 self._reset() 861 imgsize = 145 862 assert self._chestdisplayinfo is not None 863 img = bui.imagewidget( 864 parent=self._root_widget, 865 color=self._chestdisplayinfo.color, 866 texture=bui.gettexture(self._chestdisplayinfo.texclosed), 867 tint_texture=bui.gettexture(self._chestdisplayinfo.texclosedtint), 868 tint_color=self._chestdisplayinfo.tint, 869 tint2_color=self._chestdisplayinfo.tint2, 870 ) 871 872 def _set_img(x: float, scale: float) -> None: 873 if not img: 874 return 875 bui.imagewidget( 876 edit=img, 877 position=( 878 self._width * 0.5 - imgsize * scale * 0.5 + x, 879 self._yoffs - 223 + imgsize * 0.5 - imgsize * scale * 0.5, 880 ), 881 size=(imgsize * scale, imgsize * scale), 882 ) 883 884 # Set initial place. 885 _set_img(0.0, 1.0) 886 887 sign = 1.0 888 while toffs < tendoffs: 889 toffs += 0.03 * random.uniform(0.5, 1.5) 890 sign = -sign 891 bui.apptimer( 892 toffs, 893 bui.Call( 894 _set_img, 895 x=( 896 20.0 897 * random.uniform(0.3, 1.0) 898 * math.pow(toffs / tendoffs, 2.0) 899 * sign 900 ), 901 scale=1.0 - 0.2 * math.pow(toffs / tendoffs, 2.0), 902 ), 903 ) 904 905 xspacing = 100 906 xoffs = -0.5 * (len(response.contents) - 1) * xspacing 907 bui.apptimer( 908 toffs - 0.2, lambda: bui.getsound('corkPop2').play(volume=4.0) 909 ) 910 # Play a variety of voice sounds. 911 912 # We keep a global list of voice options which we randomly pull 913 # from and refill when empty. This ensures everything gets 914 # played somewhat frequently and minimizes annoying repeats. 915 global _g_open_voices # pylint: disable=global-statement 916 if not _g_open_voices: 917 _g_open_voices = [ 918 (0.3, 'woo3', 2.5), 919 (0.1, 'gasp', 1.3), 920 (0.2, 'woo2', 2.0), 921 (0.2, 'wow', 2.0), 922 (0.2, 'kronk2', 2.0), 923 (0.2, 'mel03', 2.0), 924 (0.2, 'aww', 2.0), 925 (0.4, 'nice', 2.0), 926 (0.3, 'yeah', 1.5), 927 (0.2, 'woo', 1.0), 928 (0.5, 'ooh', 0.8), 929 ] 930 931 voicetimeoffs, voicename, volume = _g_open_voices.pop( 932 random.randrange(len(_g_open_voices)) 933 ) 934 bui.apptimer( 935 toffs + voicetimeoffs, 936 lambda: bui.getsound(voicename).play(volume=volume), 937 ) 938 939 toffsopen = toffs 940 bui.apptimer(toffs, bui.WeakCall(self._show_chest_opening)) 941 toffs += tincr * 1.0 942 width = xspacing * 0.95 943 944 for item in response.contents: 945 toffs += tincr 946 bui.apptimer( 947 toffs - 0.1, lambda: bui.getsound('cashRegister').play() 948 ) 949 bui.apptimer( 950 toffs, 951 strict_partial( 952 show_display_item, 953 item, 954 self._root_widget, 955 pos=( 956 self._width * 0.5 + xoffs, 957 self._yoffs - 250.0, 958 ), 959 width=width, 960 ), 961 ) 962 xoffs += xspacing 963 toffs += tincr 964 bui.apptimer(toffs, bui.WeakCall(self._show_done_button)) 965 966 self._show_odds(initial_highlighted_row=-1) 967 968 # Store this for later 969 self._prizeindex = response.prizeindex 970 971 # The final result was already randomly selected on the server, 972 # but we want to give the illusion of randomness here, so cycle 973 # through highlighting our options and stop on the winner when 974 # the chest opens. To do this, we start at the end at the prize 975 # and work backwards setting timers. 976 if self._prizesets: 977 toffs2 = toffsopen - 0.01 978 amt = 0.02 979 i = self._prizeindex 980 while toffs2 > 0.0: 981 bui.apptimer( 982 toffs2, 983 bui.WeakCall(self._highlight_odds_row, i), 984 ) 985 toffs2 -= amt 986 amt *= 1.05 * random.uniform(0.9, 1.1) 987 i = (i - 1) % len(self._prizesets) 988 989 def _show_chest_opening(self) -> None: 990 991 # No-op if our ui is dead. 992 if not self._root_widget: 993 return 994 995 self._reset() 996 imgsize = 145 997 bui.getsound('hiss').play() 998 assert self._chestdisplayinfo is not None 999 img = bui.imagewidget( 1000 parent=self._root_widget, 1001 color=self._chestdisplayinfo.color, 1002 texture=bui.gettexture(self._chestdisplayinfo.texopen), 1003 tint_texture=bui.gettexture(self._chestdisplayinfo.texopentint), 1004 tint_color=self._chestdisplayinfo.tint, 1005 tint2_color=self._chestdisplayinfo.tint2, 1006 ) 1007 tincr = 0.8 1008 tendoffs = tincr * 2.0 1009 toffs = 0.0 1010 1011 def _set_img(x: float, scale: float) -> None: 1012 if not img: 1013 return 1014 bui.imagewidget( 1015 edit=img, 1016 position=( 1017 self._width * 0.5 - imgsize * scale * 0.5 + x, 1018 self._yoffs - 223 + imgsize * 0.5 - imgsize * scale * 0.5, 1019 ), 1020 size=(imgsize * scale, imgsize * scale), 1021 ) 1022 1023 # Set initial place. 1024 _set_img(0.0, 1.0) 1025 1026 sign = 1.0 1027 while toffs < tendoffs: 1028 toffs += 0.03 * random.uniform(0.5, 1.5) 1029 sign = -sign 1030 # Note: we speed x along here (multing toffs) so position 1031 # comes to rest before scale. 1032 bui.apptimer( 1033 toffs, 1034 bui.Call( 1035 _set_img, 1036 x=( 1037 1.0 1038 * random.uniform(0.3, 1.0) 1039 * ( 1040 1.0 1041 - math.pow(min(1.0, 3.0 * toffs / tendoffs), 2.0) 1042 ) 1043 * sign 1044 ), 1045 scale=1.0 - 0.1 * math.pow(toffs / tendoffs, 0.5), 1046 ), 1047 ) 1048 1049 self._show_odds( 1050 initial_highlighted_row=self._prizeindex, 1051 initial_highlighted_extra=True, 1052 ) 1053 1054 def _show_done_button(self) -> None: 1055 # No-op if our ui is dead. 1056 if not self._root_widget: 1057 return 1058 1059 bwidth = 200 1060 bheight = 60 1061 1062 btn = bui.buttonwidget( 1063 parent=self._root_widget, 1064 position=( 1065 self._width * 0.5 - bwidth * 0.5, 1066 self._yoffs - 350, 1067 ), 1068 size=(bwidth, bheight), 1069 label=bui.Lstr(resource='doneText'), 1070 autoselect=True, 1071 on_activate_call=self.main_window_back, 1072 ) 1073 bui.containerwidget(edit=self._root_widget, start_button=btn) 1074 1075 1076# Slight hack: we define window different classes for our different 1077# chest slots so that the default UI behavior is to replace each other 1078# when different ones are pressed. If they are all the same window class 1079# then the default behavior for such presses is to toggle the existing 1080# one back off. 1081 1082 1083class ChestWindow0(ChestWindow): 1084 """Child class of ChestWindow for slighty hackish reasons.""" 1085 1086 1087class ChestWindow1(ChestWindow): 1088 """Child class of ChestWindow for slighty hackish reasons.""" 1089 1090 1091class ChestWindow2(ChestWindow): 1092 """Child class of ChestWindow for slighty hackish reasons.""" 1093 1094 1095class ChestWindow3(ChestWindow): 1096 """Child class of ChestWindow for slighty hackish reasons."""
class
ChestWindow(bauiv1._uitypes.MainWindow):
25class ChestWindow(bui.MainWindow): 26 """Allows viewing and performing operations on a chest.""" 27 28 def __init__( 29 self, 30 index: int, 31 transition: str | None = 'in_right', 32 origin_widget: bui.Widget | None = None, 33 ): 34 self._index = index 35 36 assert bui.app.classic is not None 37 uiscale = bui.app.ui_v1.uiscale 38 self._width = 1200 if uiscale is bui.UIScale.SMALL else 650 39 self._height = 800 if uiscale is bui.UIScale.SMALL else 450 40 self._action_in_flight = False 41 self._open_now_button: bui.Widget | None = None 42 self._open_now_spinner: bui.Widget | None = None 43 self._open_now_texts: list[bui.Widget] = [] 44 self._open_now_images: list[bui.Widget] = [] 45 self._watch_ad_button: bui.Widget | None = None 46 self._time_string_timer: bui.AppTimer | None = None 47 self._time_string_text: bui.Widget | None = None 48 self._prizesets: list[bacommon.bs.ChestInfoResponse.Chest.PrizeSet] = [] 49 self._prizeindex = -1 50 self._prizesettxts: dict[int, list[bui.Widget]] = {} 51 self._prizesetimgs: dict[int, list[bui.Widget]] = {} 52 self._chestdisplayinfo: baclassic.ChestAppearanceDisplayInfo | None = ( 53 None 54 ) 55 56 # The set of widgets we keep when doing a clear. 57 self._core_widgets: list[bui.Widget] = [] 58 59 # Do some fancy math to fill all available screen area up to the 60 # size of our backing container. This lets us fit to the exact 61 # screen shape at small ui scale. 62 screensize = bui.get_virtual_screen_size() 63 scale = ( 64 1.8 65 if uiscale is bui.UIScale.SMALL 66 else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.9 67 ) 68 # Calc screen size in our local container space and clamp to a 69 # bit smaller than our container size. 70 target_height = min(self._height - 120, screensize[1] / scale) 71 72 # To get top/left coords, go to the center of our window and 73 # offset by half the width/height of our target area. 74 self._yoffstop = 0.5 * self._height + 0.5 * target_height + 18 75 76 # Offset for stuff we want centered. 77 self._yoffs = 0.5 * self._height + ( 78 220 if uiscale is bui.UIScale.SMALL else 190 79 ) 80 81 self._chest_yoffs = self._yoffs - 223 82 83 super().__init__( 84 root_widget=bui.containerwidget( 85 size=(self._width, self._height), 86 toolbar_visibility='menu_full', 87 scale=scale, 88 ), 89 transition=transition, 90 origin_widget=origin_widget, 91 # We're affected by screen size only at small ui-scale. 92 refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL, 93 ) 94 95 # Tell the root-ui to stop updating toolbar values immediately; 96 # this allows it to run animations based on the results of our 97 # chest opening. 98 bui.root_ui_pause_updates() 99 self._root_ui_updates_paused = True 100 101 self._title_text = bui.textwidget( 102 parent=self._root_widget, 103 position=( 104 self._width * 0.5, 105 self._yoffstop - (36 if uiscale is bui.UIScale.SMALL else 10), 106 ), 107 size=(0, 0), 108 text=bui.Lstr( 109 resource='chests.slotText', 110 subs=[('${NUM}', str(index + 1))], 111 ), 112 color=bui.app.ui_v1.title_color, 113 maxwidth=110.0 if uiscale is bui.UIScale.SMALL else 200, 114 scale=0.9 if uiscale is bui.UIScale.SMALL else 1.1, 115 h_align='center', 116 v_align='center', 117 ) 118 self._core_widgets.append(self._title_text) 119 120 if uiscale is bui.UIScale.SMALL: 121 bui.containerwidget( 122 edit=self._root_widget, on_cancel_call=self.main_window_back 123 ) 124 else: 125 btn = bui.buttonwidget( 126 parent=self._root_widget, 127 position=(50, self._yoffs - 44), 128 size=(60, 55), 129 scale=0.8, 130 label=bui.charstr(bui.SpecialChar.BACK), 131 button_type='backSmall', 132 extra_touch_border_scale=2.0, 133 autoselect=True, 134 on_activate_call=self.main_window_back, 135 ) 136 bui.containerwidget(edit=self._root_widget, cancel_button=btn) 137 self._core_widgets.append(btn) 138 139 # Note: Don't need to explicitly clean this up. Just not adding 140 # it to core_widgets so it will go away on next reset. 141 self._loadingspinner = bui.spinnerwidget( 142 parent=self._root_widget, 143 position=(self._width * 0.5, self._height * 0.5), 144 size=48, 145 style='bomb', 146 ) 147 148 self._infotext = bui.textwidget( 149 parent=self._root_widget, 150 position=(self._width * 0.5, self._yoffs - 200), 151 size=(0, 0), 152 text='', 153 maxwidth=700, 154 scale=0.8, 155 color=(0.6, 0.5, 0.6), 156 h_align='center', 157 v_align='center', 158 ) 159 self._core_widgets.append(self._infotext) 160 161 plus = bui.app.plus 162 if plus is None: 163 self._error('Plus feature-set is not present.') 164 return 165 166 if plus.accounts.primary is None: 167 self._error(bui.Lstr(resource='notSignedInText')) 168 return 169 170 # Start by showing info/options for our target chest. Note that 171 # we always ask the server for these values even though we may 172 # have them through our appmode subscription which updates the 173 # chest UI. This is because the wait_for_connectivity() 174 # mechanism will often bring our window up a split second before 175 # the chest subscription receives its first values which would 176 # lead us to incorrectly think there is no chest there. If we 177 # want to optimize this in the future we could perhaps use local 178 # values only if there is a chest present in them. 179 assert not self._action_in_flight 180 self._action_in_flight = True 181 with plus.accounts.primary: 182 plus.cloud.send_message_cb( 183 bacommon.bs.ChestInfoMessage(chest_id=str(self._index)), 184 on_response=bui.WeakCall(self._on_chest_info_response), 185 ) 186 187 def __del__(self) -> None: 188 189 # Make sure UI updates are resumed if we haven't done so. 190 if self._root_ui_updates_paused: 191 bui.root_ui_resume_updates() 192 193 @override 194 def get_main_window_state(self) -> bui.MainWindowState: 195 # Support recreating our window for back/refresh purposes. 196 cls = type(self) 197 198 # Pull anything we need from self out here; if we do it in the 199 # lambda we keep self alive which is bad. 200 index = self._index 201 202 return bui.BasicMainWindowState( 203 create_call=lambda transition, origin_widget: cls( 204 index=index, transition=transition, origin_widget=origin_widget 205 ) 206 ) 207 208 def _update_time_display(self, unlock_time: datetime.datetime) -> None: 209 # Once text disappears, kill our timer. 210 if not self._time_string_text: 211 self._time_string_timer = None 212 return 213 now = bui.utc_now_cloud() 214 secs_till_open = max(0.0, (unlock_time - now).total_seconds()) 215 tstr = ( 216 bui.timestring(secs_till_open, centi=False) 217 if secs_till_open > 0 218 else '' 219 ) 220 bui.textwidget(edit=self._time_string_text, text=tstr) 221 222 def _on_chest_info_response( 223 self, response: bacommon.bs.ChestInfoResponse | Exception 224 ) -> None: 225 assert self._action_in_flight # Should be us. 226 self._action_in_flight = False 227 228 if isinstance(response, Exception): 229 self._error( 230 bui.Lstr(resource='internal.unableToCompleteTryAgainText'), 231 minor=True, 232 ) 233 return 234 235 if response.chest is None: 236 self._show_about_chest_slots() 237 return 238 239 assert response.user_tokens is not None 240 self._show_chest_actions(response.user_tokens, response.chest) 241 242 def _on_chest_action_response( 243 self, response: bacommon.bs.ChestActionResponse | Exception 244 ) -> None: 245 assert self._action_in_flight # Should be us. 246 self._action_in_flight = False 247 248 # Communication/local error: 249 if isinstance(response, Exception): 250 self._error( 251 bui.Lstr(resource='internal.unableToCompleteTryAgainText'), 252 minor=True, 253 ) 254 return 255 256 # Server-side error: 257 if response.error is not None: 258 self._error(bui.Lstr(translate=('serverResponses', response.error))) 259 return 260 261 # Show any bundled success message. 262 if response.success_msg is not None: 263 bui.screenmessage( 264 bui.Lstr(translate=('serverResponses', response.success_msg)), 265 color=(0, 1.0, 0), 266 ) 267 bui.getsound('cashRegister').play() 268 269 # Show any bundled warning. 270 if response.warning is not None: 271 bui.screenmessage( 272 bui.Lstr(translate=('serverResponses', response.warning)), 273 color=(1, 0.5, 0), 274 ) 275 bui.getsound('error').play() 276 277 # If we just paid for something, make a sound accordingly. 278 if bool(False): # Hmm maybe this feels odd. 279 if response.tokens_charged > 0: 280 bui.getsound('cashRegister').play() 281 282 # If there's contents listed in the response, show them. 283 if response.contents is not None: 284 self._show_chest_contents(response) 285 else: 286 # Otherwise we're done here; just close out our UI. 287 self.main_window_back() 288 289 def _show_chest_actions( 290 self, user_tokens: int, chest: bacommon.bs.ChestInfoResponse.Chest 291 ) -> None: 292 """Show state for our chest.""" 293 # pylint: disable=too-many-locals 294 # pylint: disable=cyclic-import 295 from baclassic import ( 296 ClassicAppMode, 297 CHEST_APPEARANCE_DISPLAY_INFOS, 298 CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT, 299 ) 300 301 plus = bui.app.plus 302 assert plus is not None 303 304 # We expect to be run under classic app mode. 305 mode = bui.app.mode 306 if not isinstance(mode, ClassicAppMode): 307 self._error('Classic app mode not active.') 308 return 309 310 self._reset() 311 312 self._chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get( 313 chest.appearance, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT 314 ) 315 316 bui.textwidget( 317 edit=self._title_text, 318 text=bui.Lstr( 319 translate=('displayItemNames', chest.appearance.pretty_name) 320 ), 321 ) 322 323 imgsize = 145 324 bui.imagewidget( 325 parent=self._root_widget, 326 position=( 327 self._width * 0.5 - imgsize * 0.5, 328 # self._height - 223 + self._yoffs, 329 self._chest_yoffs, 330 ), 331 color=self._chestdisplayinfo.color, 332 size=(imgsize, imgsize), 333 texture=bui.gettexture(self._chestdisplayinfo.texclosed), 334 tint_texture=bui.gettexture(self._chestdisplayinfo.texclosedtint), 335 tint_color=self._chestdisplayinfo.tint, 336 tint2_color=self._chestdisplayinfo.tint2, 337 ) 338 339 # Store the prize-sets so we can display odds/etc. Sort them 340 # with largest weights first. 341 self._prizesets = sorted( 342 chest.prizesets, key=lambda s: s.weight, reverse=True 343 ) 344 345 if chest.unlock_tokens > 0: 346 lsize = 30 347 bui.imagewidget( 348 parent=self._root_widget, 349 position=( 350 self._width * 0.5 - imgsize * 0.4 - lsize * 0.5, 351 # self._height - 223 + 27.0 + self._yoffs, 352 self._chest_yoffs + 27.0, 353 ), 354 size=(lsize, lsize), 355 texture=bui.gettexture('lock'), 356 ) 357 358 # Time string. 359 if chest.unlock_tokens != 0: 360 self._time_string_text = bui.textwidget( 361 parent=self._root_widget, 362 position=( 363 self._width * 0.5, 364 # self._height - 85 + self._yoffs 365 self._yoffs - 85, 366 ), 367 size=(0, 0), 368 text='', 369 maxwidth=700, 370 scale=0.6, 371 color=(0.6, 1.0, 0.6), 372 h_align='center', 373 v_align='center', 374 ) 375 self._update_time_display(chest.unlock_time) 376 self._time_string_timer = bui.AppTimer( 377 1.0, 378 repeat=True, 379 call=bui.WeakCall(self._update_time_display, chest.unlock_time), 380 ) 381 382 # Allow watching an ad IF the server tells us we can AND we have 383 # an ad ready to show. 384 show_ad_button = ( 385 chest.unlock_tokens > 0 386 and chest.ad_allow 387 and plus.have_incentivized_ad() 388 ) 389 390 bwidth = 130 391 bheight = 90 392 bposy = -330 if chest.unlock_tokens == 0 else -340 393 hspace = 20 394 boffsx = (hspace * -0.5 - bwidth * 0.5) if show_ad_button else 0.0 395 396 self._open_now_button = bui.buttonwidget( 397 parent=self._root_widget, 398 position=( 399 self._width * 0.5 - bwidth * 0.5 + boffsx, 400 self._yoffs + bposy, 401 ), 402 size=(bwidth, bheight), 403 label='', 404 button_type='square', 405 autoselect=True, 406 on_activate_call=bui.WeakCall( 407 self._open_press, user_tokens, chest.unlock_tokens 408 ), 409 enable_sound=False, 410 ) 411 self._open_now_images = [] 412 self._open_now_texts = [] 413 414 iconsize = 50 415 if chest.unlock_tokens == 0: 416 self._open_now_texts.append( 417 bui.textwidget( 418 parent=self._root_widget, 419 text=bui.Lstr(resource='openText'), 420 position=( 421 self._width * 0.5 + boffsx, 422 self._yoffs + bposy + bheight * 0.5, 423 ), 424 color=(0, 1, 0), 425 draw_controller=self._open_now_button, 426 scale=0.7, 427 maxwidth=bwidth * 0.8, 428 size=(0, 0), 429 h_align='center', 430 v_align='center', 431 ) 432 ) 433 else: 434 self._open_now_texts.append( 435 bui.textwidget( 436 parent=self._root_widget, 437 text=bui.Lstr(resource='openNowText'), 438 position=( 439 self._width * 0.5 + boffsx, 440 self._yoffs + bposy + bheight * 1.15, 441 ), 442 maxwidth=bwidth * 0.8, 443 scale=0.7, 444 color=(0.7, 1, 0.7), 445 size=(0, 0), 446 h_align='center', 447 v_align='center', 448 ) 449 ) 450 self._open_now_images.append( 451 bui.imagewidget( 452 parent=self._root_widget, 453 size=(iconsize, iconsize), 454 position=( 455 self._width * 0.5 - iconsize * 0.5 + boffsx, 456 self._yoffs + bposy + bheight * 0.35, 457 ), 458 draw_controller=self._open_now_button, 459 texture=bui.gettexture('coin'), 460 ) 461 ) 462 self._open_now_texts.append( 463 bui.textwidget( 464 parent=self._root_widget, 465 text=bui.Lstr( 466 resource='tokens.numTokensText', 467 subs=[('${COUNT}', str(chest.unlock_tokens))], 468 ), 469 position=( 470 self._width * 0.5 + boffsx, 471 self._yoffs + bposy + bheight * 0.25, 472 ), 473 scale=0.65, 474 color=(0, 1, 0), 475 draw_controller=self._open_now_button, 476 maxwidth=bwidth * 0.8, 477 size=(0, 0), 478 h_align='center', 479 v_align='center', 480 ) 481 ) 482 self._open_now_spinner = bui.spinnerwidget( 483 parent=self._root_widget, 484 position=( 485 self._width * 0.5 + boffsx, 486 self._yoffs + bposy + 0.5 * bheight, 487 ), 488 visible=False, 489 ) 490 491 if show_ad_button: 492 bui.textwidget( 493 parent=self._root_widget, 494 text=bui.Lstr(resource='chests.reduceWaitText'), 495 position=( 496 self._width * 0.5 + hspace * 0.5 + bwidth * 0.5, 497 self._yoffs + bposy + bheight * 1.15, 498 ), 499 maxwidth=bwidth * 0.8, 500 scale=0.7, 501 color=(0.7, 1, 0.7), 502 size=(0, 0), 503 h_align='center', 504 v_align='center', 505 ) 506 self._watch_ad_button = bui.buttonwidget( 507 parent=self._root_widget, 508 position=( 509 self._width * 0.5 + hspace * 0.5, 510 self._yoffs + bposy, 511 ), 512 size=(bwidth, bheight), 513 label='', 514 button_type='square', 515 autoselect=True, 516 on_activate_call=bui.WeakCall(self._watch_ad_press), 517 enable_sound=False, 518 ) 519 bui.imagewidget( 520 parent=self._root_widget, 521 size=(iconsize, iconsize), 522 position=( 523 self._width * 0.5 524 + hspace * 0.5 525 + bwidth * 0.5 526 - iconsize * 0.5, 527 self._yoffs + bposy + bheight * 0.35, 528 ), 529 draw_controller=self._watch_ad_button, 530 color=(1.5, 1.0, 2.0), 531 texture=bui.gettexture('tv'), 532 ) 533 # Note to self: AdMob requires rewarded ad usage 534 # specifically says 'Ad' in it. 535 bui.textwidget( 536 parent=self._root_widget, 537 text=bui.Lstr(resource='watchAnAdText'), 538 position=( 539 self._width * 0.5 + hspace * 0.5 + bwidth * 0.5, 540 self._yoffs + bposy + bheight * 0.25, 541 ), 542 scale=0.65, 543 color=(0, 1, 0), 544 draw_controller=self._watch_ad_button, 545 maxwidth=bwidth * 0.8, 546 size=(0, 0), 547 h_align='center', 548 v_align='center', 549 ) 550 551 self._show_odds(initial_highlighted_row=-1) 552 553 def _highlight_odds_row(self, row: int, extra: bool = False) -> None: 554 555 for rindex, imgs in self._prizesetimgs.items(): 556 opacity = ( 557 (0.9 if extra else 0.75) 558 if rindex == row 559 else (0.4 if extra else 0.5) 560 ) 561 for img in imgs: 562 if img: 563 bui.imagewidget(edit=img, opacity=opacity) 564 565 for rindex, txts in self._prizesettxts.items(): 566 opacity = ( 567 (0.9 if extra else 0.75) 568 if rindex == row 569 else (0.4 if extra else 0.5) 570 ) 571 for txt in txts: 572 if txt: 573 bui.textwidget(edit=txt, color=(0.7, 0.65, 1, opacity)) 574 575 def _show_odds( 576 self, 577 *, 578 initial_highlighted_row: int, 579 initial_highlighted_extra: bool = False, 580 ) -> None: 581 # pylint: disable=too-many-locals 582 xoffs = 110 583 584 totalweight = max(0.001, sum(t.weight for t in self._prizesets)) 585 586 rowheight = 25 587 totalheight = (len(self._prizesets) + 1) * rowheight 588 x = self._width * 0.5 + xoffs 589 y = self._yoffs - 150.0 + totalheight * 0.5 590 591 # Title. 592 bui.textwidget( 593 parent=self._root_widget, 594 text=bui.Lstr(resource='chests.prizeOddsText'), 595 color=(0.7, 0.65, 1, 0.5), 596 flatness=1.0, 597 shadow=1.0, 598 position=(x, y), 599 scale=0.55, 600 size=(0, 0), 601 h_align='left', 602 v_align='center', 603 ) 604 y -= 5.0 605 606 prizesettxts: list[bui.Widget] 607 prizesetimgs: list[bui.Widget] 608 609 def _mkicon(img: str) -> None: 610 iconsize = 20.0 611 nonlocal x 612 nonlocal prizesetimgs 613 prizesetimgs.append( 614 bui.imagewidget( 615 parent=self._root_widget, 616 size=(iconsize, iconsize), 617 position=(x, y - iconsize * 0.5), 618 texture=bui.gettexture(img), 619 opacity=0.4, 620 ) 621 ) 622 x += iconsize 623 624 def _mktxt(txt: str, advance: bool = True) -> None: 625 tscale = 0.45 626 nonlocal x 627 nonlocal prizesettxts 628 prizesettxts.append( 629 bui.textwidget( 630 parent=self._root_widget, 631 text=txt, 632 flatness=1.0, 633 shadow=1.0, 634 position=(x, y), 635 scale=tscale, 636 size=(0, 0), 637 h_align='left', 638 v_align='center', 639 ) 640 ) 641 if advance: 642 x += (bui.get_string_width(txt, suppress_warning=True)) * tscale 643 644 self._prizesettxts = {} 645 self._prizesetimgs = {} 646 647 for i, p in enumerate(self._prizesets): 648 prizesettxts = self._prizesettxts.setdefault(i, []) 649 prizesetimgs = self._prizesetimgs.setdefault(i, []) 650 x = self._width * 0.5 + xoffs 651 y -= rowheight 652 percent = 100.0 * p.weight / totalweight 653 654 # Show decimals only if we get very small percentages (looks 655 # better than rounding as '0%'). 656 percenttxt = ( 657 f'{percent:.2f}%:' 658 if percent < 0.095 659 else ( 660 f'{percent:.1f}%:' 661 if percent < 0.95 662 else f'{round(percent)}%:' 663 ) 664 ) 665 666 # We advance manually here to keep values lined up 667 # (otherwise single digit percent rows don't line up with 668 # double digit ones). 669 _mktxt(percenttxt, advance=False) 670 x += 35.0 671 672 for item in p.contents: 673 x += 5.0 674 if isinstance(item.item, bacommon.bs.TicketsDisplayItem): 675 _mktxt(str(item.item.count)) 676 _mkicon('tickets') 677 elif isinstance(item.item, bacommon.bs.TokensDisplayItem): 678 _mktxt(str(item.item.count)) 679 _mkicon('coin') 680 else: 681 # For other cases just fall back on text desc. 682 # 683 # Translate the wrapper description and apply any subs. 684 descfin = bui.Lstr( 685 translate=('serverResponses', item.description) 686 ).evaluate() 687 subs = ( 688 [] 689 if item.description_subs is None 690 else item.description_subs 691 ) 692 assert len(subs) % 2 == 0 # Should always be even. 693 for j in range(0, len(subs) - 1, 2): 694 descfin = descfin.replace(subs[j], subs[j + 1]) 695 _mktxt(descfin) 696 self._highlight_odds_row( 697 initial_highlighted_row, extra=initial_highlighted_extra 698 ) 699 700 def _open_press(self, user_tokens: int, token_payment: int) -> None: 701 from bauiv1lib.gettokens import show_get_tokens_prompt 702 703 bui.getsound('click01').play() 704 705 # Allow only one in-flight action at once. 706 if self._action_in_flight: 707 bui.screenmessage( 708 bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0) 709 ) 710 bui.getsound('error').play() 711 return 712 713 plus = bui.app.plus 714 assert plus is not None 715 716 if plus.accounts.primary is None: 717 self._error(bui.Lstr(resource='notSignedInText')) 718 return 719 720 # Offer to purchase tokens if they don't have enough. 721 if user_tokens < token_payment: 722 # Hack: We disable normal swish for the open button and it 723 # seems weird without a swish here, so explicitly do one. 724 bui.getsound('swish').play() 725 show_get_tokens_prompt() 726 return 727 728 self._action_in_flight = True 729 with plus.accounts.primary: 730 plus.cloud.send_message_cb( 731 bacommon.bs.ChestActionMessage( 732 chest_id=str(self._index), 733 action=bacommon.bs.ChestActionMessage.Action.UNLOCK, 734 token_payment=token_payment, 735 ), 736 on_response=bui.WeakCall(self._on_chest_action_response), 737 ) 738 739 # Convey that something is in progress. 740 if self._open_now_button: 741 bui.spinnerwidget(edit=self._open_now_spinner, visible=True) 742 for twidget in self._open_now_texts: 743 bui.textwidget(edit=twidget, color=(1, 1, 1, 0.2)) 744 for iwidget in self._open_now_images: 745 bui.imagewidget(edit=iwidget, opacity=0.2) 746 747 def _watch_ad_press(self) -> None: 748 749 bui.getsound('click01').play() 750 751 # Allow only one in-flight action at once. 752 if self._action_in_flight: 753 bui.screenmessage( 754 bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0) 755 ) 756 bui.getsound('error').play() 757 return 758 759 assert bui.app.classic is not None 760 761 self._action_in_flight = True 762 bui.app.classic.ads.show_ad_2( 763 'reduce_chest_wait', 764 on_completion_call=bui.WeakCall(self._watch_ad_complete), 765 ) 766 767 # Convey that something is in progress. 768 if self._watch_ad_button: 769 bui.buttonwidget(edit=self._watch_ad_button, color=(0.4, 0.4, 0.4)) 770 771 def _watch_ad_complete(self, actually_showed: bool) -> None: 772 773 assert self._action_in_flight # Should be ad view. 774 self._action_in_flight = False 775 776 if not actually_showed: 777 return 778 779 # Allow only one in-flight action at once. 780 if self._action_in_flight: 781 bui.screenmessage( 782 bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0) 783 ) 784 bui.getsound('error').play() 785 return 786 787 plus = bui.app.plus 788 assert plus is not None 789 790 if plus.accounts.primary is None: 791 self._error(bui.Lstr(resource='notSignedInText')) 792 return 793 794 self._action_in_flight = True 795 with plus.accounts.primary: 796 plus.cloud.send_message_cb( 797 bacommon.bs.ChestActionMessage( 798 chest_id=str(self._index), 799 action=bacommon.bs.ChestActionMessage.Action.AD, 800 token_payment=0, 801 ), 802 on_response=bui.WeakCall(self._on_chest_action_response), 803 ) 804 805 def _reset(self) -> None: 806 """Clear all non-permanent widgets and clear infotext.""" 807 for widget in self._root_widget.get_children(): 808 if widget not in self._core_widgets: 809 widget.delete() 810 bui.textwidget(edit=self._infotext, text='', color=(1, 1, 1)) 811 812 def _error(self, msg: str | bui.Lstr, minor: bool = False) -> None: 813 """Put ourself in an error state with a visible error message.""" 814 self._reset() 815 bui.textwidget( 816 edit=self._infotext, 817 text=msg, 818 color=(1, 0.5, 0.5) if minor else (1, 0, 0), 819 ) 820 821 def _show_about_chest_slots(self) -> None: 822 # No-op if our ui is dead. 823 if not self._root_widget: 824 return 825 826 self._reset() 827 bui.textwidget( 828 edit=self._infotext, 829 text=bui.Lstr(resource='chests.slotDescriptionText'), 830 color=(1, 1, 1), 831 ) 832 833 def _show_chest_contents( 834 self, response: bacommon.bs.ChestActionResponse 835 ) -> None: 836 # pylint: disable=too-many-locals 837 838 from baclassic import show_display_item 839 840 # No-op if our ui is dead. 841 if not self._root_widget: 842 return 843 844 assert response.contents is not None 845 846 # Insert test items for testing. 847 if bool(False): 848 response.contents += [ 849 bacommon.bs.DisplayItemWrapper.for_display_item( 850 bacommon.bs.TestDisplayItem() 851 ) 852 ] 853 854 tincr = 0.4 855 tendoffs = tincr * 4.0 856 toffs = 0.0 857 858 bui.getsound('revUp').play(volume=2.0) 859 860 # Show nothing but the chest icon and animate it shaking. 861 self._reset() 862 imgsize = 145 863 assert self._chestdisplayinfo is not None 864 img = bui.imagewidget( 865 parent=self._root_widget, 866 color=self._chestdisplayinfo.color, 867 texture=bui.gettexture(self._chestdisplayinfo.texclosed), 868 tint_texture=bui.gettexture(self._chestdisplayinfo.texclosedtint), 869 tint_color=self._chestdisplayinfo.tint, 870 tint2_color=self._chestdisplayinfo.tint2, 871 ) 872 873 def _set_img(x: float, scale: float) -> None: 874 if not img: 875 return 876 bui.imagewidget( 877 edit=img, 878 position=( 879 self._width * 0.5 - imgsize * scale * 0.5 + x, 880 self._yoffs - 223 + imgsize * 0.5 - imgsize * scale * 0.5, 881 ), 882 size=(imgsize * scale, imgsize * scale), 883 ) 884 885 # Set initial place. 886 _set_img(0.0, 1.0) 887 888 sign = 1.0 889 while toffs < tendoffs: 890 toffs += 0.03 * random.uniform(0.5, 1.5) 891 sign = -sign 892 bui.apptimer( 893 toffs, 894 bui.Call( 895 _set_img, 896 x=( 897 20.0 898 * random.uniform(0.3, 1.0) 899 * math.pow(toffs / tendoffs, 2.0) 900 * sign 901 ), 902 scale=1.0 - 0.2 * math.pow(toffs / tendoffs, 2.0), 903 ), 904 ) 905 906 xspacing = 100 907 xoffs = -0.5 * (len(response.contents) - 1) * xspacing 908 bui.apptimer( 909 toffs - 0.2, lambda: bui.getsound('corkPop2').play(volume=4.0) 910 ) 911 # Play a variety of voice sounds. 912 913 # We keep a global list of voice options which we randomly pull 914 # from and refill when empty. This ensures everything gets 915 # played somewhat frequently and minimizes annoying repeats. 916 global _g_open_voices # pylint: disable=global-statement 917 if not _g_open_voices: 918 _g_open_voices = [ 919 (0.3, 'woo3', 2.5), 920 (0.1, 'gasp', 1.3), 921 (0.2, 'woo2', 2.0), 922 (0.2, 'wow', 2.0), 923 (0.2, 'kronk2', 2.0), 924 (0.2, 'mel03', 2.0), 925 (0.2, 'aww', 2.0), 926 (0.4, 'nice', 2.0), 927 (0.3, 'yeah', 1.5), 928 (0.2, 'woo', 1.0), 929 (0.5, 'ooh', 0.8), 930 ] 931 932 voicetimeoffs, voicename, volume = _g_open_voices.pop( 933 random.randrange(len(_g_open_voices)) 934 ) 935 bui.apptimer( 936 toffs + voicetimeoffs, 937 lambda: bui.getsound(voicename).play(volume=volume), 938 ) 939 940 toffsopen = toffs 941 bui.apptimer(toffs, bui.WeakCall(self._show_chest_opening)) 942 toffs += tincr * 1.0 943 width = xspacing * 0.95 944 945 for item in response.contents: 946 toffs += tincr 947 bui.apptimer( 948 toffs - 0.1, lambda: bui.getsound('cashRegister').play() 949 ) 950 bui.apptimer( 951 toffs, 952 strict_partial( 953 show_display_item, 954 item, 955 self._root_widget, 956 pos=( 957 self._width * 0.5 + xoffs, 958 self._yoffs - 250.0, 959 ), 960 width=width, 961 ), 962 ) 963 xoffs += xspacing 964 toffs += tincr 965 bui.apptimer(toffs, bui.WeakCall(self._show_done_button)) 966 967 self._show_odds(initial_highlighted_row=-1) 968 969 # Store this for later 970 self._prizeindex = response.prizeindex 971 972 # The final result was already randomly selected on the server, 973 # but we want to give the illusion of randomness here, so cycle 974 # through highlighting our options and stop on the winner when 975 # the chest opens. To do this, we start at the end at the prize 976 # and work backwards setting timers. 977 if self._prizesets: 978 toffs2 = toffsopen - 0.01 979 amt = 0.02 980 i = self._prizeindex 981 while toffs2 > 0.0: 982 bui.apptimer( 983 toffs2, 984 bui.WeakCall(self._highlight_odds_row, i), 985 ) 986 toffs2 -= amt 987 amt *= 1.05 * random.uniform(0.9, 1.1) 988 i = (i - 1) % len(self._prizesets) 989 990 def _show_chest_opening(self) -> None: 991 992 # No-op if our ui is dead. 993 if not self._root_widget: 994 return 995 996 self._reset() 997 imgsize = 145 998 bui.getsound('hiss').play() 999 assert self._chestdisplayinfo is not None 1000 img = bui.imagewidget( 1001 parent=self._root_widget, 1002 color=self._chestdisplayinfo.color, 1003 texture=bui.gettexture(self._chestdisplayinfo.texopen), 1004 tint_texture=bui.gettexture(self._chestdisplayinfo.texopentint), 1005 tint_color=self._chestdisplayinfo.tint, 1006 tint2_color=self._chestdisplayinfo.tint2, 1007 ) 1008 tincr = 0.8 1009 tendoffs = tincr * 2.0 1010 toffs = 0.0 1011 1012 def _set_img(x: float, scale: float) -> None: 1013 if not img: 1014 return 1015 bui.imagewidget( 1016 edit=img, 1017 position=( 1018 self._width * 0.5 - imgsize * scale * 0.5 + x, 1019 self._yoffs - 223 + imgsize * 0.5 - imgsize * scale * 0.5, 1020 ), 1021 size=(imgsize * scale, imgsize * scale), 1022 ) 1023 1024 # Set initial place. 1025 _set_img(0.0, 1.0) 1026 1027 sign = 1.0 1028 while toffs < tendoffs: 1029 toffs += 0.03 * random.uniform(0.5, 1.5) 1030 sign = -sign 1031 # Note: we speed x along here (multing toffs) so position 1032 # comes to rest before scale. 1033 bui.apptimer( 1034 toffs, 1035 bui.Call( 1036 _set_img, 1037 x=( 1038 1.0 1039 * random.uniform(0.3, 1.0) 1040 * ( 1041 1.0 1042 - math.pow(min(1.0, 3.0 * toffs / tendoffs), 2.0) 1043 ) 1044 * sign 1045 ), 1046 scale=1.0 - 0.1 * math.pow(toffs / tendoffs, 0.5), 1047 ), 1048 ) 1049 1050 self._show_odds( 1051 initial_highlighted_row=self._prizeindex, 1052 initial_highlighted_extra=True, 1053 ) 1054 1055 def _show_done_button(self) -> None: 1056 # No-op if our ui is dead. 1057 if not self._root_widget: 1058 return 1059 1060 bwidth = 200 1061 bheight = 60 1062 1063 btn = bui.buttonwidget( 1064 parent=self._root_widget, 1065 position=( 1066 self._width * 0.5 - bwidth * 0.5, 1067 self._yoffs - 350, 1068 ), 1069 size=(bwidth, bheight), 1070 label=bui.Lstr(resource='doneText'), 1071 autoselect=True, 1072 on_activate_call=self.main_window_back, 1073 ) 1074 bui.containerwidget(edit=self._root_widget, start_button=btn)
Allows viewing and performing operations on a chest.
ChestWindow( index: int, transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None)
28 def __init__( 29 self, 30 index: int, 31 transition: str | None = 'in_right', 32 origin_widget: bui.Widget | None = None, 33 ): 34 self._index = index 35 36 assert bui.app.classic is not None 37 uiscale = bui.app.ui_v1.uiscale 38 self._width = 1200 if uiscale is bui.UIScale.SMALL else 650 39 self._height = 800 if uiscale is bui.UIScale.SMALL else 450 40 self._action_in_flight = False 41 self._open_now_button: bui.Widget | None = None 42 self._open_now_spinner: bui.Widget | None = None 43 self._open_now_texts: list[bui.Widget] = [] 44 self._open_now_images: list[bui.Widget] = [] 45 self._watch_ad_button: bui.Widget | None = None 46 self._time_string_timer: bui.AppTimer | None = None 47 self._time_string_text: bui.Widget | None = None 48 self._prizesets: list[bacommon.bs.ChestInfoResponse.Chest.PrizeSet] = [] 49 self._prizeindex = -1 50 self._prizesettxts: dict[int, list[bui.Widget]] = {} 51 self._prizesetimgs: dict[int, list[bui.Widget]] = {} 52 self._chestdisplayinfo: baclassic.ChestAppearanceDisplayInfo | None = ( 53 None 54 ) 55 56 # The set of widgets we keep when doing a clear. 57 self._core_widgets: list[bui.Widget] = [] 58 59 # Do some fancy math to fill all available screen area up to the 60 # size of our backing container. This lets us fit to the exact 61 # screen shape at small ui scale. 62 screensize = bui.get_virtual_screen_size() 63 scale = ( 64 1.8 65 if uiscale is bui.UIScale.SMALL 66 else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.9 67 ) 68 # Calc screen size in our local container space and clamp to a 69 # bit smaller than our container size. 70 target_height = min(self._height - 120, screensize[1] / scale) 71 72 # To get top/left coords, go to the center of our window and 73 # offset by half the width/height of our target area. 74 self._yoffstop = 0.5 * self._height + 0.5 * target_height + 18 75 76 # Offset for stuff we want centered. 77 self._yoffs = 0.5 * self._height + ( 78 220 if uiscale is bui.UIScale.SMALL else 190 79 ) 80 81 self._chest_yoffs = self._yoffs - 223 82 83 super().__init__( 84 root_widget=bui.containerwidget( 85 size=(self._width, self._height), 86 toolbar_visibility='menu_full', 87 scale=scale, 88 ), 89 transition=transition, 90 origin_widget=origin_widget, 91 # We're affected by screen size only at small ui-scale. 92 refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL, 93 ) 94 95 # Tell the root-ui to stop updating toolbar values immediately; 96 # this allows it to run animations based on the results of our 97 # chest opening. 98 bui.root_ui_pause_updates() 99 self._root_ui_updates_paused = True 100 101 self._title_text = bui.textwidget( 102 parent=self._root_widget, 103 position=( 104 self._width * 0.5, 105 self._yoffstop - (36 if uiscale is bui.UIScale.SMALL else 10), 106 ), 107 size=(0, 0), 108 text=bui.Lstr( 109 resource='chests.slotText', 110 subs=[('${NUM}', str(index + 1))], 111 ), 112 color=bui.app.ui_v1.title_color, 113 maxwidth=110.0 if uiscale is bui.UIScale.SMALL else 200, 114 scale=0.9 if uiscale is bui.UIScale.SMALL else 1.1, 115 h_align='center', 116 v_align='center', 117 ) 118 self._core_widgets.append(self._title_text) 119 120 if uiscale is bui.UIScale.SMALL: 121 bui.containerwidget( 122 edit=self._root_widget, on_cancel_call=self.main_window_back 123 ) 124 else: 125 btn = bui.buttonwidget( 126 parent=self._root_widget, 127 position=(50, self._yoffs - 44), 128 size=(60, 55), 129 scale=0.8, 130 label=bui.charstr(bui.SpecialChar.BACK), 131 button_type='backSmall', 132 extra_touch_border_scale=2.0, 133 autoselect=True, 134 on_activate_call=self.main_window_back, 135 ) 136 bui.containerwidget(edit=self._root_widget, cancel_button=btn) 137 self._core_widgets.append(btn) 138 139 # Note: Don't need to explicitly clean this up. Just not adding 140 # it to core_widgets so it will go away on next reset. 141 self._loadingspinner = bui.spinnerwidget( 142 parent=self._root_widget, 143 position=(self._width * 0.5, self._height * 0.5), 144 size=48, 145 style='bomb', 146 ) 147 148 self._infotext = bui.textwidget( 149 parent=self._root_widget, 150 position=(self._width * 0.5, self._yoffs - 200), 151 size=(0, 0), 152 text='', 153 maxwidth=700, 154 scale=0.8, 155 color=(0.6, 0.5, 0.6), 156 h_align='center', 157 v_align='center', 158 ) 159 self._core_widgets.append(self._infotext) 160 161 plus = bui.app.plus 162 if plus is None: 163 self._error('Plus feature-set is not present.') 164 return 165 166 if plus.accounts.primary is None: 167 self._error(bui.Lstr(resource='notSignedInText')) 168 return 169 170 # Start by showing info/options for our target chest. Note that 171 # we always ask the server for these values even though we may 172 # have them through our appmode subscription which updates the 173 # chest UI. This is because the wait_for_connectivity() 174 # mechanism will often bring our window up a split second before 175 # the chest subscription receives its first values which would 176 # lead us to incorrectly think there is no chest there. If we 177 # want to optimize this in the future we could perhaps use local 178 # values only if there is a chest present in them. 179 assert not self._action_in_flight 180 self._action_in_flight = True 181 with plus.accounts.primary: 182 plus.cloud.send_message_cb( 183 bacommon.bs.ChestInfoMessage(chest_id=str(self._index)), 184 on_response=bui.WeakCall(self._on_chest_info_response), 185 )
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.
193 @override 194 def get_main_window_state(self) -> bui.MainWindowState: 195 # Support recreating our window for back/refresh purposes. 196 cls = type(self) 197 198 # Pull anything we need from self out here; if we do it in the 199 # lambda we keep self alive which is bad. 200 index = self._index 201 202 return bui.BasicMainWindowState( 203 create_call=lambda transition, origin_widget: cls( 204 index=index, transition=transition, origin_widget=origin_widget 205 ) 206 )
Return a WindowState to recreate this window, if supported.
1084class ChestWindow0(ChestWindow): 1085 """Child class of ChestWindow for slighty hackish reasons."""
Child class of ChestWindow for slighty hackish reasons.
Inherited Members
1088class ChestWindow1(ChestWindow): 1089 """Child class of ChestWindow for slighty hackish reasons."""
Child class of ChestWindow for slighty hackish reasons.
Inherited Members
1092class ChestWindow2(ChestWindow): 1093 """Child class of ChestWindow for slighty hackish reasons."""
Child class of ChestWindow for slighty hackish reasons.
Inherited Members
1096class ChestWindow3(ChestWindow): 1097 """Child class of ChestWindow for slighty hackish reasons."""
Child class of ChestWindow for slighty hackish reasons.