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