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