bauiv1lib.tournamententry
Defines a popup window for entering tournaments.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Defines a popup window for entering tournaments.""" 4 5from __future__ import annotations 6 7import logging 8from typing import TYPE_CHECKING 9 10from bauiv1lib.popup import PopupWindow 11import bauiv1 as bui 12 13if TYPE_CHECKING: 14 from typing import Any, Callable 15 16 import bascenev1 as bs 17 18 19class TournamentEntryWindow(PopupWindow): 20 """Popup window for entering tournaments.""" 21 22 def __init__( 23 self, 24 tournament_id: str, 25 tournament_activity: bs.Activity | None = None, 26 position: tuple[float, float] = (0.0, 0.0), 27 delegate: Any = None, 28 scale: float | None = None, 29 offset: tuple[float, float] = (0.0, 0.0), 30 on_close_call: Callable[[], Any] | None = None, 31 ): 32 # Needs some tidying. 33 # pylint: disable=too-many-branches 34 # pylint: disable=too-many-statements 35 36 assert bui.app.classic is not None 37 bui.set_analytics_screen('Tournament Entry Window') 38 39 self._tournament_id = tournament_id 40 self._tournament_info = bui.app.classic.accounts.tournament_info[ 41 self._tournament_id 42 ] 43 44 # Set a few vars depending on the tourney fee. 45 self._fee = self._tournament_info['fee'] 46 self._allow_ads = self._tournament_info['allowAds'] 47 if self._fee == 4: 48 self._purchase_name = 'tournament_entry_4' 49 self._purchase_price_name = 'price.tournament_entry_4' 50 elif self._fee == 3: 51 self._purchase_name = 'tournament_entry_3' 52 self._purchase_price_name = 'price.tournament_entry_3' 53 elif self._fee == 2: 54 self._purchase_name = 'tournament_entry_2' 55 self._purchase_price_name = 'price.tournament_entry_2' 56 elif self._fee == 1: 57 self._purchase_name = 'tournament_entry_1' 58 self._purchase_price_name = 'price.tournament_entry_1' 59 else: 60 if self._fee != 0: 61 raise ValueError('invalid fee: ' + str(self._fee)) 62 self._purchase_name = 'tournament_entry_0' 63 self._purchase_price_name = 'price.tournament_entry_0' 64 65 self._purchase_price: int | None = None 66 67 self._on_close_call = on_close_call 68 if scale is None: 69 uiscale = bui.app.ui_v1.uiscale 70 scale = ( 71 2.3 72 if uiscale is bui.UIScale.SMALL 73 else 1.65 74 if uiscale is bui.UIScale.MEDIUM 75 else 1.23 76 ) 77 self._delegate = delegate 78 self._transitioning_out = False 79 80 self._tournament_activity = tournament_activity 81 82 self._width = 340 83 self._height = 225 84 85 bg_color = (0.5, 0.4, 0.6) 86 87 # Creates our root_widget. 88 super().__init__( 89 position=position, 90 size=(self._width, self._height), 91 scale=scale, 92 bg_color=bg_color, 93 offset=offset, 94 toolbar_visibility='menu_currency', 95 ) 96 97 self._last_ad_press_time = -9999.0 98 self._last_ticket_press_time = -9999.0 99 self._entering = False 100 self._launched = False 101 102 # Show the ad button only if we support ads *and* it has a level 1 fee. 103 self._do_ad_btn = bui.has_video_ads() and self._allow_ads 104 105 x_offs = 0 if self._do_ad_btn else 85 106 107 self._cancel_button = bui.buttonwidget( 108 parent=self.root_widget, 109 position=(20, self._height - 34), 110 size=(60, 60), 111 scale=0.5, 112 label='', 113 color=bg_color, 114 on_activate_call=self._on_cancel, 115 autoselect=True, 116 icon=bui.gettexture('crossOut'), 117 iconscale=1.2, 118 ) 119 120 self._title_text = bui.textwidget( 121 parent=self.root_widget, 122 position=(self._width * 0.5, self._height - 20), 123 size=(0, 0), 124 h_align='center', 125 v_align='center', 126 scale=0.6, 127 text=bui.Lstr(resource='tournamentEntryText'), 128 maxwidth=180, 129 color=(1, 1, 1, 0.4), 130 ) 131 132 btn = self._pay_with_tickets_button = bui.buttonwidget( 133 parent=self.root_widget, 134 position=(30 + x_offs, 60), 135 autoselect=True, 136 button_type='square', 137 size=(120, 120), 138 label='', 139 on_activate_call=self._on_pay_with_tickets_press, 140 ) 141 self._ticket_img_pos = (50 + x_offs, 94) 142 self._ticket_img_pos_free = (50 + x_offs, 80) 143 self._ticket_img = bui.imagewidget( 144 parent=self.root_widget, 145 draw_controller=btn, 146 size=(80, 80), 147 position=self._ticket_img_pos, 148 texture=bui.gettexture('tickets'), 149 ) 150 self._ticket_cost_text_position = (87 + x_offs, 88) 151 self._ticket_cost_text_position_free = (87 + x_offs, 120) 152 self._ticket_cost_text = bui.textwidget( 153 parent=self.root_widget, 154 draw_controller=btn, 155 position=self._ticket_cost_text_position, 156 size=(0, 0), 157 h_align='center', 158 v_align='center', 159 scale=0.6, 160 text='', 161 maxwidth=95, 162 color=(0, 1, 0), 163 ) 164 self._free_plays_remaining_text = bui.textwidget( 165 parent=self.root_widget, 166 draw_controller=btn, 167 position=(87 + x_offs, 78), 168 size=(0, 0), 169 h_align='center', 170 v_align='center', 171 scale=0.33, 172 text='', 173 maxwidth=95, 174 color=(0, 0.8, 0), 175 ) 176 self._pay_with_ad_btn: bui.Widget | None 177 if self._do_ad_btn: 178 btn = self._pay_with_ad_btn = bui.buttonwidget( 179 parent=self.root_widget, 180 position=(190, 60), 181 autoselect=True, 182 button_type='square', 183 size=(120, 120), 184 label='', 185 on_activate_call=self._on_pay_with_ad_press, 186 ) 187 self._pay_with_ad_img = bui.imagewidget( 188 parent=self.root_widget, 189 draw_controller=btn, 190 size=(80, 80), 191 position=(210, 94), 192 texture=bui.gettexture('tv'), 193 ) 194 195 self._ad_text_position = (251, 88) 196 self._ad_text_position_remaining = (251, 92) 197 have_ad_tries_remaining = ( 198 self._tournament_info['adTriesRemaining'] is not None 199 ) 200 self._ad_text = bui.textwidget( 201 parent=self.root_widget, 202 draw_controller=btn, 203 position=self._ad_text_position_remaining 204 if have_ad_tries_remaining 205 else self._ad_text_position, 206 size=(0, 0), 207 h_align='center', 208 v_align='center', 209 scale=0.6, 210 # Note: AdMob now requires rewarded ad usage 211 # specifically says 'Ad' in it. 212 text=bui.Lstr(resource='watchAnAdText'), 213 maxwidth=95, 214 color=(0, 1, 0), 215 ) 216 ad_plays_remaining_text = ( 217 '' 218 if not have_ad_tries_remaining 219 else '' + str(self._tournament_info['adTriesRemaining']) 220 ) 221 self._ad_plays_remaining_text = bui.textwidget( 222 parent=self.root_widget, 223 draw_controller=btn, 224 position=(251, 78), 225 size=(0, 0), 226 h_align='center', 227 v_align='center', 228 scale=0.33, 229 text=ad_plays_remaining_text, 230 maxwidth=95, 231 color=(0, 0.8, 0), 232 ) 233 234 bui.textwidget( 235 parent=self.root_widget, 236 position=(self._width * 0.5, 120), 237 size=(0, 0), 238 h_align='center', 239 v_align='center', 240 scale=0.6, 241 text=bui.Lstr( 242 resource='orText', subs=[('${A}', ''), ('${B}', '')] 243 ), 244 maxwidth=35, 245 color=(1, 1, 1, 0.5), 246 ) 247 else: 248 self._pay_with_ad_btn = None 249 250 self._get_tickets_button: bui.Widget | None = None 251 self._ticket_count_text: bui.Widget | None = None 252 if not bui.app.ui_v1.use_toolbars: 253 if bui.app.classic.allow_ticket_purchases: 254 self._get_tickets_button = bui.buttonwidget( 255 parent=self.root_widget, 256 position=(self._width - 190 + 125, self._height - 34), 257 autoselect=True, 258 scale=0.5, 259 size=(120, 60), 260 textcolor=(0.2, 1, 0.2), 261 label=bui.charstr(bui.SpecialChar.TICKET), 262 color=(0.65, 0.5, 0.8), 263 on_activate_call=self._on_get_tickets_press, 264 ) 265 else: 266 self._ticket_count_text = bui.textwidget( 267 parent=self.root_widget, 268 scale=0.5, 269 position=(self._width - 190 + 125, self._height - 34), 270 color=(0.2, 1, 0.2), 271 h_align='center', 272 v_align='center', 273 ) 274 275 self._seconds_remaining = None 276 277 bui.containerwidget( 278 edit=self.root_widget, cancel_button=self._cancel_button 279 ) 280 281 # Let's also ask the server for info about this tournament 282 # (time remaining, etc) so we can show the user time remaining, 283 # disallow entry if time has run out, etc. 284 # xoffs = 104 if bui.app.ui.use_toolbars else 0 285 self._time_remaining_text = bui.textwidget( 286 parent=self.root_widget, 287 position=(self._width / 2, 28), 288 size=(0, 0), 289 h_align='center', 290 v_align='center', 291 text='-', 292 scale=0.65, 293 maxwidth=100, 294 flatness=1.0, 295 color=(0.7, 0.7, 0.7), 296 ) 297 self._time_remaining_label_text = bui.textwidget( 298 parent=self.root_widget, 299 position=(self._width / 2, 45), 300 size=(0, 0), 301 h_align='center', 302 v_align='center', 303 text=bui.Lstr(resource='coopSelectWindow.timeRemainingText'), 304 scale=0.45, 305 flatness=1.0, 306 maxwidth=100, 307 color=(0.7, 0.7, 0.7), 308 ) 309 310 self._last_query_time: float | None = None 311 312 # If there seems to be a relatively-recent valid cached info for this 313 # tournament, use it. Otherwise we'll kick off a query ourselves. 314 if ( 315 self._tournament_id in bui.app.classic.accounts.tournament_info 316 and bui.app.classic.accounts.tournament_info[self._tournament_id][ 317 'valid' 318 ] 319 and ( 320 bui.apptime() 321 - bui.app.classic.accounts.tournament_info[self._tournament_id][ 322 'timeReceived' 323 ] 324 < 60 * 5 325 ) 326 ): 327 try: 328 info = bui.app.classic.accounts.tournament_info[ 329 self._tournament_id 330 ] 331 self._seconds_remaining = max( 332 0, 333 info['timeRemaining'] 334 - int((bui.apptime() - info['timeReceived'])), 335 ) 336 self._have_valid_data = True 337 self._last_query_time = bui.apptime() 338 except Exception: 339 logging.exception('Error using valid tourney data.') 340 self._have_valid_data = False 341 else: 342 self._have_valid_data = False 343 344 self._fg_state = bui.app.fg_state 345 self._running_query = False 346 self._update_timer = bui.AppTimer( 347 1.0, bui.WeakCall(self._update), repeat=True 348 ) 349 self._update() 350 self._restore_state() 351 352 def _on_tournament_query_response( 353 self, data: dict[str, Any] | None 354 ) -> None: 355 assert bui.app.classic is not None 356 accounts = bui.app.classic.accounts 357 self._running_query = False 358 if data is not None: 359 data = data['t'] # This used to be the whole payload. 360 accounts.cache_tournament_info(data) 361 self._seconds_remaining = accounts.tournament_info[ 362 self._tournament_id 363 ]['timeRemaining'] 364 self._have_valid_data = True 365 366 def _save_state(self) -> None: 367 if not self.root_widget: 368 return 369 sel = self.root_widget.get_selected_child() 370 if sel == self._pay_with_ad_btn: 371 sel_name = 'Ad' 372 else: 373 sel_name = 'Tickets' 374 cfg = bui.app.config 375 cfg['Tournament Pay Selection'] = sel_name 376 cfg.commit() 377 378 def _restore_state(self) -> None: 379 sel_name = bui.app.config.get('Tournament Pay Selection', 'Tickets') 380 if sel_name == 'Ad' and self._pay_with_ad_btn is not None: 381 sel = self._pay_with_ad_btn 382 else: 383 sel = self._pay_with_tickets_button 384 bui.containerwidget(edit=self.root_widget, selected_child=sel) 385 386 def _update(self) -> None: 387 plus = bui.app.plus 388 assert plus is not None 389 390 # We may outlive our widgets. 391 if not self.root_widget: 392 return 393 394 # If we've been foregrounded/backgrounded we need to re-grab data. 395 if self._fg_state != bui.app.fg_state: 396 self._fg_state = bui.app.fg_state 397 self._have_valid_data = False 398 399 # If we need to run another tournament query, do so. 400 if not self._running_query and ( 401 (self._last_query_time is None) 402 or (not self._have_valid_data) 403 or (bui.apptime() - self._last_query_time > 30.0) 404 ): 405 plus.tournament_query( 406 args={ 407 'source': 'entry window' 408 if self._tournament_activity is None 409 else 'retry entry window' 410 }, 411 callback=bui.WeakCall(self._on_tournament_query_response), 412 ) 413 self._last_query_time = bui.apptime() 414 self._running_query = True 415 416 # Grab the latest info on our tourney. 417 assert bui.app.classic is not None 418 self._tournament_info = bui.app.classic.accounts.tournament_info[ 419 self._tournament_id 420 ] 421 422 # If we don't have valid data always show a '-' for time. 423 if not self._have_valid_data: 424 bui.textwidget(edit=self._time_remaining_text, text='-') 425 else: 426 if self._seconds_remaining is not None: 427 self._seconds_remaining = max(0, self._seconds_remaining - 1) 428 bui.textwidget( 429 edit=self._time_remaining_text, 430 text=bui.timestring(self._seconds_remaining, centi=False), 431 ) 432 433 # Keep price up-to-date and update the button with it. 434 self._purchase_price = plus.get_v1_account_misc_read_val( 435 self._purchase_price_name, None 436 ) 437 438 bui.textwidget( 439 edit=self._ticket_cost_text, 440 text=( 441 bui.Lstr(resource='getTicketsWindow.freeText') 442 if self._purchase_price == 0 443 else bui.Lstr( 444 resource='getTicketsWindow.ticketsText', 445 subs=[ 446 ( 447 '${COUNT}', 448 str(self._purchase_price) 449 if self._purchase_price is not None 450 else '?', 451 ) 452 ], 453 ) 454 ), 455 position=self._ticket_cost_text_position_free 456 if self._purchase_price == 0 457 else self._ticket_cost_text_position, 458 scale=1.0 if self._purchase_price == 0 else 0.6, 459 ) 460 461 bui.textwidget( 462 edit=self._free_plays_remaining_text, 463 text='' 464 if ( 465 self._tournament_info['freeTriesRemaining'] in [None, 0] 466 or self._purchase_price != 0 467 ) 468 else '' + str(self._tournament_info['freeTriesRemaining']), 469 ) 470 471 bui.imagewidget( 472 edit=self._ticket_img, 473 opacity=0.2 if self._purchase_price == 0 else 1.0, 474 position=self._ticket_img_pos_free 475 if self._purchase_price == 0 476 else self._ticket_img_pos, 477 ) 478 479 if self._do_ad_btn: 480 enabled = bui.have_incentivized_ad() 481 have_ad_tries_remaining = ( 482 self._tournament_info['adTriesRemaining'] is not None 483 and self._tournament_info['adTriesRemaining'] > 0 484 ) 485 bui.textwidget( 486 edit=self._ad_text, 487 position=self._ad_text_position_remaining 488 if have_ad_tries_remaining 489 else self._ad_text_position, 490 color=(0, 1, 0) if enabled else (0.5, 0.5, 0.5), 491 ) 492 bui.imagewidget( 493 edit=self._pay_with_ad_img, opacity=1.0 if enabled else 0.2 494 ) 495 bui.buttonwidget( 496 edit=self._pay_with_ad_btn, 497 color=(0.5, 0.7, 0.2) if enabled else (0.5, 0.5, 0.5), 498 ) 499 ad_plays_remaining_text = ( 500 '' 501 if not have_ad_tries_remaining 502 else '' + str(self._tournament_info['adTriesRemaining']) 503 ) 504 bui.textwidget( 505 edit=self._ad_plays_remaining_text, 506 text=ad_plays_remaining_text, 507 color=(0, 0.8, 0) if enabled else (0.4, 0.4, 0.4), 508 ) 509 510 try: 511 t_str = str(plus.get_v1_account_ticket_count()) 512 except Exception: 513 t_str = '?' 514 if self._get_tickets_button: 515 bui.buttonwidget( 516 edit=self._get_tickets_button, 517 label=bui.charstr(bui.SpecialChar.TICKET) + t_str, 518 ) 519 if self._ticket_count_text: 520 bui.textwidget( 521 edit=self._ticket_count_text, 522 text=bui.charstr(bui.SpecialChar.TICKET) + t_str, 523 ) 524 525 def _launch(self) -> None: 526 assert bui.app.classic is not None 527 if self._launched: 528 return 529 self._launched = True 530 launched = False 531 532 # If they gave us an existing activity, just restart it. 533 if self._tournament_activity is not None: 534 try: 535 bui.apptimer(0.1, bui.getsound('cashRegister').play) 536 with self._tournament_activity.context: 537 self._tournament_activity.end( 538 {'outcome': 'restart'}, force=True 539 ) 540 bui.apptimer(0.3, self._transition_out) 541 launched = True 542 bui.screenmessage( 543 bui.Lstr( 544 translate=('serverResponses', 'Entering tournament...') 545 ), 546 color=(0, 1, 0), 547 ) 548 549 # We can hit exceptions here if _tournament_activity ends before 550 # our restart attempt happens. 551 # In this case we'll fall back to launching a new session. 552 # This is not ideal since players will have to rejoin, etc., 553 # but it works for now. 554 except Exception: 555 logging.exception('Error restarting tournament activity.') 556 557 # If we had no existing activity (or were unable to restart it) 558 # launch a new session. 559 if not launched: 560 bui.apptimer(0.1, bui.getsound('cashRegister').play) 561 bui.apptimer( 562 1.0, 563 lambda: bui.app.classic.launch_coop_game( 564 self._tournament_info['game'], 565 args={ 566 'min_players': self._tournament_info['minPlayers'], 567 'max_players': self._tournament_info['maxPlayers'], 568 'tournament_id': self._tournament_id, 569 }, 570 ) 571 if bui.app.classic is not None 572 else None, 573 ) 574 bui.apptimer(0.7, self._transition_out) 575 bui.screenmessage( 576 bui.Lstr( 577 translate=('serverResponses', 'Entering tournament...') 578 ), 579 color=(0, 1, 0), 580 ) 581 582 def _on_pay_with_tickets_press(self) -> None: 583 from bauiv1lib import getcurrency 584 585 plus = bui.app.plus 586 assert plus is not None 587 588 # If we're already entering, ignore. 589 if self._entering: 590 return 591 592 if not self._have_valid_data: 593 bui.screenmessage( 594 bui.Lstr(resource='tournamentCheckingStateText'), 595 color=(1, 0, 0), 596 ) 597 bui.getsound('error').play() 598 return 599 600 # If we don't have a price. 601 if self._purchase_price is None: 602 bui.screenmessage( 603 bui.Lstr(resource='tournamentCheckingStateText'), 604 color=(1, 0, 0), 605 ) 606 bui.getsound('error').play() 607 return 608 609 # Deny if it looks like the tourney has ended. 610 if self._seconds_remaining == 0: 611 bui.screenmessage( 612 bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) 613 ) 614 bui.getsound('error').play() 615 return 616 617 # Deny if we don't have enough tickets. 618 ticket_count: int | None 619 try: 620 ticket_count = plus.get_v1_account_ticket_count() 621 except Exception: 622 # FIXME: should add a bui.NotSignedInError we can use here. 623 ticket_count = None 624 ticket_cost = self._purchase_price 625 if ticket_count is not None and ticket_count < ticket_cost: 626 getcurrency.show_get_tickets_prompt() 627 bui.getsound('error').play() 628 self._transition_out() 629 return 630 631 cur_time = bui.apptime() 632 self._last_ticket_press_time = cur_time 633 assert isinstance(ticket_cost, int) 634 plus.in_game_purchase(self._purchase_name, ticket_cost) 635 636 self._entering = True 637 plus.add_v1_account_transaction( 638 { 639 'type': 'ENTER_TOURNAMENT', 640 'fee': self._fee, 641 'tournamentID': self._tournament_id, 642 } 643 ) 644 plus.run_v1_account_transactions() 645 self._launch() 646 647 def _on_pay_with_ad_press(self) -> None: 648 # If we're already entering, ignore. 649 if self._entering: 650 return 651 652 if not self._have_valid_data: 653 bui.screenmessage( 654 bui.Lstr(resource='tournamentCheckingStateText'), 655 color=(1, 0, 0), 656 ) 657 bui.getsound('error').play() 658 return 659 660 # Deny if it looks like the tourney has ended. 661 if self._seconds_remaining == 0: 662 bui.screenmessage( 663 bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) 664 ) 665 bui.getsound('error').play() 666 return 667 668 cur_time = bui.apptime() 669 if cur_time - self._last_ad_press_time > 5.0: 670 self._last_ad_press_time = cur_time 671 assert bui.app.classic is not None 672 bui.app.classic.ads.show_ad_2( 673 'tournament_entry', 674 on_completion_call=bui.WeakCall(self._on_ad_complete), 675 ) 676 677 def _on_ad_complete(self, actually_showed: bool) -> None: 678 plus = bui.app.plus 679 assert plus is not None 680 681 # Make sure any transactions the ad added got locally applied 682 # (rewards added, etc.). 683 plus.run_v1_account_transactions() 684 685 # If we're already entering the tourney, ignore. 686 if self._entering: 687 return 688 689 if not actually_showed: 690 return 691 692 # This should have awarded us the tournament_entry_ad purchase; 693 # make sure that's present. 694 # (otherwise the server will ignore our tournament entry anyway) 695 if not plus.get_purchased('tournament_entry_ad'): 696 print('no tournament_entry_ad purchase present in _on_ad_complete') 697 bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0)) 698 bui.getsound('error').play() 699 return 700 701 self._entering = True 702 plus.add_v1_account_transaction( 703 { 704 'type': 'ENTER_TOURNAMENT', 705 'fee': 'ad', 706 'tournamentID': self._tournament_id, 707 } 708 ) 709 plus.run_v1_account_transactions() 710 self._launch() 711 712 def _on_get_tickets_press(self) -> None: 713 from bauiv1lib import getcurrency 714 715 # If we're already entering, ignore presses. 716 if self._entering: 717 return 718 719 # Bring up get-tickets window and then kill ourself (we're on the 720 # overlay layer so we'd show up above it). 721 getcurrency.GetCurrencyWindow( 722 modal=True, origin_widget=self._get_tickets_button 723 ) 724 self._transition_out() 725 726 def _on_cancel(self) -> None: 727 plus = bui.app.plus 728 assert plus is not None 729 # Don't allow canceling for several seconds after poking an enter 730 # button if it looks like we're waiting on a purchase or entering 731 # the tournament. 732 if (bui.apptime() - self._last_ticket_press_time < 6.0) and ( 733 plus.have_outstanding_v1_account_transactions() 734 or plus.get_purchased(self._purchase_name) 735 or self._entering 736 ): 737 bui.getsound('error').play() 738 return 739 self._transition_out() 740 741 def _transition_out(self) -> None: 742 if not self.root_widget: 743 return 744 if not self._transitioning_out: 745 self._transitioning_out = True 746 self._save_state() 747 bui.containerwidget(edit=self.root_widget, transition='out_scale') 748 if self._on_close_call is not None: 749 self._on_close_call() 750 751 def on_popup_cancel(self) -> None: 752 bui.getsound('swish').play() 753 self._on_cancel()
20class TournamentEntryWindow(PopupWindow): 21 """Popup window for entering tournaments.""" 22 23 def __init__( 24 self, 25 tournament_id: str, 26 tournament_activity: bs.Activity | None = None, 27 position: tuple[float, float] = (0.0, 0.0), 28 delegate: Any = None, 29 scale: float | None = None, 30 offset: tuple[float, float] = (0.0, 0.0), 31 on_close_call: Callable[[], Any] | None = None, 32 ): 33 # Needs some tidying. 34 # pylint: disable=too-many-branches 35 # pylint: disable=too-many-statements 36 37 assert bui.app.classic is not None 38 bui.set_analytics_screen('Tournament Entry Window') 39 40 self._tournament_id = tournament_id 41 self._tournament_info = bui.app.classic.accounts.tournament_info[ 42 self._tournament_id 43 ] 44 45 # Set a few vars depending on the tourney fee. 46 self._fee = self._tournament_info['fee'] 47 self._allow_ads = self._tournament_info['allowAds'] 48 if self._fee == 4: 49 self._purchase_name = 'tournament_entry_4' 50 self._purchase_price_name = 'price.tournament_entry_4' 51 elif self._fee == 3: 52 self._purchase_name = 'tournament_entry_3' 53 self._purchase_price_name = 'price.tournament_entry_3' 54 elif self._fee == 2: 55 self._purchase_name = 'tournament_entry_2' 56 self._purchase_price_name = 'price.tournament_entry_2' 57 elif self._fee == 1: 58 self._purchase_name = 'tournament_entry_1' 59 self._purchase_price_name = 'price.tournament_entry_1' 60 else: 61 if self._fee != 0: 62 raise ValueError('invalid fee: ' + str(self._fee)) 63 self._purchase_name = 'tournament_entry_0' 64 self._purchase_price_name = 'price.tournament_entry_0' 65 66 self._purchase_price: int | None = None 67 68 self._on_close_call = on_close_call 69 if scale is None: 70 uiscale = bui.app.ui_v1.uiscale 71 scale = ( 72 2.3 73 if uiscale is bui.UIScale.SMALL 74 else 1.65 75 if uiscale is bui.UIScale.MEDIUM 76 else 1.23 77 ) 78 self._delegate = delegate 79 self._transitioning_out = False 80 81 self._tournament_activity = tournament_activity 82 83 self._width = 340 84 self._height = 225 85 86 bg_color = (0.5, 0.4, 0.6) 87 88 # Creates our root_widget. 89 super().__init__( 90 position=position, 91 size=(self._width, self._height), 92 scale=scale, 93 bg_color=bg_color, 94 offset=offset, 95 toolbar_visibility='menu_currency', 96 ) 97 98 self._last_ad_press_time = -9999.0 99 self._last_ticket_press_time = -9999.0 100 self._entering = False 101 self._launched = False 102 103 # Show the ad button only if we support ads *and* it has a level 1 fee. 104 self._do_ad_btn = bui.has_video_ads() and self._allow_ads 105 106 x_offs = 0 if self._do_ad_btn else 85 107 108 self._cancel_button = bui.buttonwidget( 109 parent=self.root_widget, 110 position=(20, self._height - 34), 111 size=(60, 60), 112 scale=0.5, 113 label='', 114 color=bg_color, 115 on_activate_call=self._on_cancel, 116 autoselect=True, 117 icon=bui.gettexture('crossOut'), 118 iconscale=1.2, 119 ) 120 121 self._title_text = bui.textwidget( 122 parent=self.root_widget, 123 position=(self._width * 0.5, self._height - 20), 124 size=(0, 0), 125 h_align='center', 126 v_align='center', 127 scale=0.6, 128 text=bui.Lstr(resource='tournamentEntryText'), 129 maxwidth=180, 130 color=(1, 1, 1, 0.4), 131 ) 132 133 btn = self._pay_with_tickets_button = bui.buttonwidget( 134 parent=self.root_widget, 135 position=(30 + x_offs, 60), 136 autoselect=True, 137 button_type='square', 138 size=(120, 120), 139 label='', 140 on_activate_call=self._on_pay_with_tickets_press, 141 ) 142 self._ticket_img_pos = (50 + x_offs, 94) 143 self._ticket_img_pos_free = (50 + x_offs, 80) 144 self._ticket_img = bui.imagewidget( 145 parent=self.root_widget, 146 draw_controller=btn, 147 size=(80, 80), 148 position=self._ticket_img_pos, 149 texture=bui.gettexture('tickets'), 150 ) 151 self._ticket_cost_text_position = (87 + x_offs, 88) 152 self._ticket_cost_text_position_free = (87 + x_offs, 120) 153 self._ticket_cost_text = bui.textwidget( 154 parent=self.root_widget, 155 draw_controller=btn, 156 position=self._ticket_cost_text_position, 157 size=(0, 0), 158 h_align='center', 159 v_align='center', 160 scale=0.6, 161 text='', 162 maxwidth=95, 163 color=(0, 1, 0), 164 ) 165 self._free_plays_remaining_text = bui.textwidget( 166 parent=self.root_widget, 167 draw_controller=btn, 168 position=(87 + x_offs, 78), 169 size=(0, 0), 170 h_align='center', 171 v_align='center', 172 scale=0.33, 173 text='', 174 maxwidth=95, 175 color=(0, 0.8, 0), 176 ) 177 self._pay_with_ad_btn: bui.Widget | None 178 if self._do_ad_btn: 179 btn = self._pay_with_ad_btn = bui.buttonwidget( 180 parent=self.root_widget, 181 position=(190, 60), 182 autoselect=True, 183 button_type='square', 184 size=(120, 120), 185 label='', 186 on_activate_call=self._on_pay_with_ad_press, 187 ) 188 self._pay_with_ad_img = bui.imagewidget( 189 parent=self.root_widget, 190 draw_controller=btn, 191 size=(80, 80), 192 position=(210, 94), 193 texture=bui.gettexture('tv'), 194 ) 195 196 self._ad_text_position = (251, 88) 197 self._ad_text_position_remaining = (251, 92) 198 have_ad_tries_remaining = ( 199 self._tournament_info['adTriesRemaining'] is not None 200 ) 201 self._ad_text = bui.textwidget( 202 parent=self.root_widget, 203 draw_controller=btn, 204 position=self._ad_text_position_remaining 205 if have_ad_tries_remaining 206 else self._ad_text_position, 207 size=(0, 0), 208 h_align='center', 209 v_align='center', 210 scale=0.6, 211 # Note: AdMob now requires rewarded ad usage 212 # specifically says 'Ad' in it. 213 text=bui.Lstr(resource='watchAnAdText'), 214 maxwidth=95, 215 color=(0, 1, 0), 216 ) 217 ad_plays_remaining_text = ( 218 '' 219 if not have_ad_tries_remaining 220 else '' + str(self._tournament_info['adTriesRemaining']) 221 ) 222 self._ad_plays_remaining_text = bui.textwidget( 223 parent=self.root_widget, 224 draw_controller=btn, 225 position=(251, 78), 226 size=(0, 0), 227 h_align='center', 228 v_align='center', 229 scale=0.33, 230 text=ad_plays_remaining_text, 231 maxwidth=95, 232 color=(0, 0.8, 0), 233 ) 234 235 bui.textwidget( 236 parent=self.root_widget, 237 position=(self._width * 0.5, 120), 238 size=(0, 0), 239 h_align='center', 240 v_align='center', 241 scale=0.6, 242 text=bui.Lstr( 243 resource='orText', subs=[('${A}', ''), ('${B}', '')] 244 ), 245 maxwidth=35, 246 color=(1, 1, 1, 0.5), 247 ) 248 else: 249 self._pay_with_ad_btn = None 250 251 self._get_tickets_button: bui.Widget | None = None 252 self._ticket_count_text: bui.Widget | None = None 253 if not bui.app.ui_v1.use_toolbars: 254 if bui.app.classic.allow_ticket_purchases: 255 self._get_tickets_button = bui.buttonwidget( 256 parent=self.root_widget, 257 position=(self._width - 190 + 125, self._height - 34), 258 autoselect=True, 259 scale=0.5, 260 size=(120, 60), 261 textcolor=(0.2, 1, 0.2), 262 label=bui.charstr(bui.SpecialChar.TICKET), 263 color=(0.65, 0.5, 0.8), 264 on_activate_call=self._on_get_tickets_press, 265 ) 266 else: 267 self._ticket_count_text = bui.textwidget( 268 parent=self.root_widget, 269 scale=0.5, 270 position=(self._width - 190 + 125, self._height - 34), 271 color=(0.2, 1, 0.2), 272 h_align='center', 273 v_align='center', 274 ) 275 276 self._seconds_remaining = None 277 278 bui.containerwidget( 279 edit=self.root_widget, cancel_button=self._cancel_button 280 ) 281 282 # Let's also ask the server for info about this tournament 283 # (time remaining, etc) so we can show the user time remaining, 284 # disallow entry if time has run out, etc. 285 # xoffs = 104 if bui.app.ui.use_toolbars else 0 286 self._time_remaining_text = bui.textwidget( 287 parent=self.root_widget, 288 position=(self._width / 2, 28), 289 size=(0, 0), 290 h_align='center', 291 v_align='center', 292 text='-', 293 scale=0.65, 294 maxwidth=100, 295 flatness=1.0, 296 color=(0.7, 0.7, 0.7), 297 ) 298 self._time_remaining_label_text = bui.textwidget( 299 parent=self.root_widget, 300 position=(self._width / 2, 45), 301 size=(0, 0), 302 h_align='center', 303 v_align='center', 304 text=bui.Lstr(resource='coopSelectWindow.timeRemainingText'), 305 scale=0.45, 306 flatness=1.0, 307 maxwidth=100, 308 color=(0.7, 0.7, 0.7), 309 ) 310 311 self._last_query_time: float | None = None 312 313 # If there seems to be a relatively-recent valid cached info for this 314 # tournament, use it. Otherwise we'll kick off a query ourselves. 315 if ( 316 self._tournament_id in bui.app.classic.accounts.tournament_info 317 and bui.app.classic.accounts.tournament_info[self._tournament_id][ 318 'valid' 319 ] 320 and ( 321 bui.apptime() 322 - bui.app.classic.accounts.tournament_info[self._tournament_id][ 323 'timeReceived' 324 ] 325 < 60 * 5 326 ) 327 ): 328 try: 329 info = bui.app.classic.accounts.tournament_info[ 330 self._tournament_id 331 ] 332 self._seconds_remaining = max( 333 0, 334 info['timeRemaining'] 335 - int((bui.apptime() - info['timeReceived'])), 336 ) 337 self._have_valid_data = True 338 self._last_query_time = bui.apptime() 339 except Exception: 340 logging.exception('Error using valid tourney data.') 341 self._have_valid_data = False 342 else: 343 self._have_valid_data = False 344 345 self._fg_state = bui.app.fg_state 346 self._running_query = False 347 self._update_timer = bui.AppTimer( 348 1.0, bui.WeakCall(self._update), repeat=True 349 ) 350 self._update() 351 self._restore_state() 352 353 def _on_tournament_query_response( 354 self, data: dict[str, Any] | None 355 ) -> None: 356 assert bui.app.classic is not None 357 accounts = bui.app.classic.accounts 358 self._running_query = False 359 if data is not None: 360 data = data['t'] # This used to be the whole payload. 361 accounts.cache_tournament_info(data) 362 self._seconds_remaining = accounts.tournament_info[ 363 self._tournament_id 364 ]['timeRemaining'] 365 self._have_valid_data = True 366 367 def _save_state(self) -> None: 368 if not self.root_widget: 369 return 370 sel = self.root_widget.get_selected_child() 371 if sel == self._pay_with_ad_btn: 372 sel_name = 'Ad' 373 else: 374 sel_name = 'Tickets' 375 cfg = bui.app.config 376 cfg['Tournament Pay Selection'] = sel_name 377 cfg.commit() 378 379 def _restore_state(self) -> None: 380 sel_name = bui.app.config.get('Tournament Pay Selection', 'Tickets') 381 if sel_name == 'Ad' and self._pay_with_ad_btn is not None: 382 sel = self._pay_with_ad_btn 383 else: 384 sel = self._pay_with_tickets_button 385 bui.containerwidget(edit=self.root_widget, selected_child=sel) 386 387 def _update(self) -> None: 388 plus = bui.app.plus 389 assert plus is not None 390 391 # We may outlive our widgets. 392 if not self.root_widget: 393 return 394 395 # If we've been foregrounded/backgrounded we need to re-grab data. 396 if self._fg_state != bui.app.fg_state: 397 self._fg_state = bui.app.fg_state 398 self._have_valid_data = False 399 400 # If we need to run another tournament query, do so. 401 if not self._running_query and ( 402 (self._last_query_time is None) 403 or (not self._have_valid_data) 404 or (bui.apptime() - self._last_query_time > 30.0) 405 ): 406 plus.tournament_query( 407 args={ 408 'source': 'entry window' 409 if self._tournament_activity is None 410 else 'retry entry window' 411 }, 412 callback=bui.WeakCall(self._on_tournament_query_response), 413 ) 414 self._last_query_time = bui.apptime() 415 self._running_query = True 416 417 # Grab the latest info on our tourney. 418 assert bui.app.classic is not None 419 self._tournament_info = bui.app.classic.accounts.tournament_info[ 420 self._tournament_id 421 ] 422 423 # If we don't have valid data always show a '-' for time. 424 if not self._have_valid_data: 425 bui.textwidget(edit=self._time_remaining_text, text='-') 426 else: 427 if self._seconds_remaining is not None: 428 self._seconds_remaining = max(0, self._seconds_remaining - 1) 429 bui.textwidget( 430 edit=self._time_remaining_text, 431 text=bui.timestring(self._seconds_remaining, centi=False), 432 ) 433 434 # Keep price up-to-date and update the button with it. 435 self._purchase_price = plus.get_v1_account_misc_read_val( 436 self._purchase_price_name, None 437 ) 438 439 bui.textwidget( 440 edit=self._ticket_cost_text, 441 text=( 442 bui.Lstr(resource='getTicketsWindow.freeText') 443 if self._purchase_price == 0 444 else bui.Lstr( 445 resource='getTicketsWindow.ticketsText', 446 subs=[ 447 ( 448 '${COUNT}', 449 str(self._purchase_price) 450 if self._purchase_price is not None 451 else '?', 452 ) 453 ], 454 ) 455 ), 456 position=self._ticket_cost_text_position_free 457 if self._purchase_price == 0 458 else self._ticket_cost_text_position, 459 scale=1.0 if self._purchase_price == 0 else 0.6, 460 ) 461 462 bui.textwidget( 463 edit=self._free_plays_remaining_text, 464 text='' 465 if ( 466 self._tournament_info['freeTriesRemaining'] in [None, 0] 467 or self._purchase_price != 0 468 ) 469 else '' + str(self._tournament_info['freeTriesRemaining']), 470 ) 471 472 bui.imagewidget( 473 edit=self._ticket_img, 474 opacity=0.2 if self._purchase_price == 0 else 1.0, 475 position=self._ticket_img_pos_free 476 if self._purchase_price == 0 477 else self._ticket_img_pos, 478 ) 479 480 if self._do_ad_btn: 481 enabled = bui.have_incentivized_ad() 482 have_ad_tries_remaining = ( 483 self._tournament_info['adTriesRemaining'] is not None 484 and self._tournament_info['adTriesRemaining'] > 0 485 ) 486 bui.textwidget( 487 edit=self._ad_text, 488 position=self._ad_text_position_remaining 489 if have_ad_tries_remaining 490 else self._ad_text_position, 491 color=(0, 1, 0) if enabled else (0.5, 0.5, 0.5), 492 ) 493 bui.imagewidget( 494 edit=self._pay_with_ad_img, opacity=1.0 if enabled else 0.2 495 ) 496 bui.buttonwidget( 497 edit=self._pay_with_ad_btn, 498 color=(0.5, 0.7, 0.2) if enabled else (0.5, 0.5, 0.5), 499 ) 500 ad_plays_remaining_text = ( 501 '' 502 if not have_ad_tries_remaining 503 else '' + str(self._tournament_info['adTriesRemaining']) 504 ) 505 bui.textwidget( 506 edit=self._ad_plays_remaining_text, 507 text=ad_plays_remaining_text, 508 color=(0, 0.8, 0) if enabled else (0.4, 0.4, 0.4), 509 ) 510 511 try: 512 t_str = str(plus.get_v1_account_ticket_count()) 513 except Exception: 514 t_str = '?' 515 if self._get_tickets_button: 516 bui.buttonwidget( 517 edit=self._get_tickets_button, 518 label=bui.charstr(bui.SpecialChar.TICKET) + t_str, 519 ) 520 if self._ticket_count_text: 521 bui.textwidget( 522 edit=self._ticket_count_text, 523 text=bui.charstr(bui.SpecialChar.TICKET) + t_str, 524 ) 525 526 def _launch(self) -> None: 527 assert bui.app.classic is not None 528 if self._launched: 529 return 530 self._launched = True 531 launched = False 532 533 # If they gave us an existing activity, just restart it. 534 if self._tournament_activity is not None: 535 try: 536 bui.apptimer(0.1, bui.getsound('cashRegister').play) 537 with self._tournament_activity.context: 538 self._tournament_activity.end( 539 {'outcome': 'restart'}, force=True 540 ) 541 bui.apptimer(0.3, self._transition_out) 542 launched = True 543 bui.screenmessage( 544 bui.Lstr( 545 translate=('serverResponses', 'Entering tournament...') 546 ), 547 color=(0, 1, 0), 548 ) 549 550 # We can hit exceptions here if _tournament_activity ends before 551 # our restart attempt happens. 552 # In this case we'll fall back to launching a new session. 553 # This is not ideal since players will have to rejoin, etc., 554 # but it works for now. 555 except Exception: 556 logging.exception('Error restarting tournament activity.') 557 558 # If we had no existing activity (or were unable to restart it) 559 # launch a new session. 560 if not launched: 561 bui.apptimer(0.1, bui.getsound('cashRegister').play) 562 bui.apptimer( 563 1.0, 564 lambda: bui.app.classic.launch_coop_game( 565 self._tournament_info['game'], 566 args={ 567 'min_players': self._tournament_info['minPlayers'], 568 'max_players': self._tournament_info['maxPlayers'], 569 'tournament_id': self._tournament_id, 570 }, 571 ) 572 if bui.app.classic is not None 573 else None, 574 ) 575 bui.apptimer(0.7, self._transition_out) 576 bui.screenmessage( 577 bui.Lstr( 578 translate=('serverResponses', 'Entering tournament...') 579 ), 580 color=(0, 1, 0), 581 ) 582 583 def _on_pay_with_tickets_press(self) -> None: 584 from bauiv1lib import getcurrency 585 586 plus = bui.app.plus 587 assert plus is not None 588 589 # If we're already entering, ignore. 590 if self._entering: 591 return 592 593 if not self._have_valid_data: 594 bui.screenmessage( 595 bui.Lstr(resource='tournamentCheckingStateText'), 596 color=(1, 0, 0), 597 ) 598 bui.getsound('error').play() 599 return 600 601 # If we don't have a price. 602 if self._purchase_price is None: 603 bui.screenmessage( 604 bui.Lstr(resource='tournamentCheckingStateText'), 605 color=(1, 0, 0), 606 ) 607 bui.getsound('error').play() 608 return 609 610 # Deny if it looks like the tourney has ended. 611 if self._seconds_remaining == 0: 612 bui.screenmessage( 613 bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) 614 ) 615 bui.getsound('error').play() 616 return 617 618 # Deny if we don't have enough tickets. 619 ticket_count: int | None 620 try: 621 ticket_count = plus.get_v1_account_ticket_count() 622 except Exception: 623 # FIXME: should add a bui.NotSignedInError we can use here. 624 ticket_count = None 625 ticket_cost = self._purchase_price 626 if ticket_count is not None and ticket_count < ticket_cost: 627 getcurrency.show_get_tickets_prompt() 628 bui.getsound('error').play() 629 self._transition_out() 630 return 631 632 cur_time = bui.apptime() 633 self._last_ticket_press_time = cur_time 634 assert isinstance(ticket_cost, int) 635 plus.in_game_purchase(self._purchase_name, ticket_cost) 636 637 self._entering = True 638 plus.add_v1_account_transaction( 639 { 640 'type': 'ENTER_TOURNAMENT', 641 'fee': self._fee, 642 'tournamentID': self._tournament_id, 643 } 644 ) 645 plus.run_v1_account_transactions() 646 self._launch() 647 648 def _on_pay_with_ad_press(self) -> None: 649 # If we're already entering, ignore. 650 if self._entering: 651 return 652 653 if not self._have_valid_data: 654 bui.screenmessage( 655 bui.Lstr(resource='tournamentCheckingStateText'), 656 color=(1, 0, 0), 657 ) 658 bui.getsound('error').play() 659 return 660 661 # Deny if it looks like the tourney has ended. 662 if self._seconds_remaining == 0: 663 bui.screenmessage( 664 bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) 665 ) 666 bui.getsound('error').play() 667 return 668 669 cur_time = bui.apptime() 670 if cur_time - self._last_ad_press_time > 5.0: 671 self._last_ad_press_time = cur_time 672 assert bui.app.classic is not None 673 bui.app.classic.ads.show_ad_2( 674 'tournament_entry', 675 on_completion_call=bui.WeakCall(self._on_ad_complete), 676 ) 677 678 def _on_ad_complete(self, actually_showed: bool) -> None: 679 plus = bui.app.plus 680 assert plus is not None 681 682 # Make sure any transactions the ad added got locally applied 683 # (rewards added, etc.). 684 plus.run_v1_account_transactions() 685 686 # If we're already entering the tourney, ignore. 687 if self._entering: 688 return 689 690 if not actually_showed: 691 return 692 693 # This should have awarded us the tournament_entry_ad purchase; 694 # make sure that's present. 695 # (otherwise the server will ignore our tournament entry anyway) 696 if not plus.get_purchased('tournament_entry_ad'): 697 print('no tournament_entry_ad purchase present in _on_ad_complete') 698 bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0)) 699 bui.getsound('error').play() 700 return 701 702 self._entering = True 703 plus.add_v1_account_transaction( 704 { 705 'type': 'ENTER_TOURNAMENT', 706 'fee': 'ad', 707 'tournamentID': self._tournament_id, 708 } 709 ) 710 plus.run_v1_account_transactions() 711 self._launch() 712 713 def _on_get_tickets_press(self) -> None: 714 from bauiv1lib import getcurrency 715 716 # If we're already entering, ignore presses. 717 if self._entering: 718 return 719 720 # Bring up get-tickets window and then kill ourself (we're on the 721 # overlay layer so we'd show up above it). 722 getcurrency.GetCurrencyWindow( 723 modal=True, origin_widget=self._get_tickets_button 724 ) 725 self._transition_out() 726 727 def _on_cancel(self) -> None: 728 plus = bui.app.plus 729 assert plus is not None 730 # Don't allow canceling for several seconds after poking an enter 731 # button if it looks like we're waiting on a purchase or entering 732 # the tournament. 733 if (bui.apptime() - self._last_ticket_press_time < 6.0) and ( 734 plus.have_outstanding_v1_account_transactions() 735 or plus.get_purchased(self._purchase_name) 736 or self._entering 737 ): 738 bui.getsound('error').play() 739 return 740 self._transition_out() 741 742 def _transition_out(self) -> None: 743 if not self.root_widget: 744 return 745 if not self._transitioning_out: 746 self._transitioning_out = True 747 self._save_state() 748 bui.containerwidget(edit=self.root_widget, transition='out_scale') 749 if self._on_close_call is not None: 750 self._on_close_call() 751 752 def on_popup_cancel(self) -> None: 753 bui.getsound('swish').play() 754 self._on_cancel()
Popup window for entering tournaments.
TournamentEntryWindow( tournament_id: str, tournament_activity: bascenev1._activity.Activity | None = None, position: tuple[float, float] = (0.0, 0.0), delegate: Any = None, scale: float | None = None, offset: tuple[float, float] = (0.0, 0.0), on_close_call: Optional[Callable[[], Any]] = None)
23 def __init__( 24 self, 25 tournament_id: str, 26 tournament_activity: bs.Activity | None = None, 27 position: tuple[float, float] = (0.0, 0.0), 28 delegate: Any = None, 29 scale: float | None = None, 30 offset: tuple[float, float] = (0.0, 0.0), 31 on_close_call: Callable[[], Any] | None = None, 32 ): 33 # Needs some tidying. 34 # pylint: disable=too-many-branches 35 # pylint: disable=too-many-statements 36 37 assert bui.app.classic is not None 38 bui.set_analytics_screen('Tournament Entry Window') 39 40 self._tournament_id = tournament_id 41 self._tournament_info = bui.app.classic.accounts.tournament_info[ 42 self._tournament_id 43 ] 44 45 # Set a few vars depending on the tourney fee. 46 self._fee = self._tournament_info['fee'] 47 self._allow_ads = self._tournament_info['allowAds'] 48 if self._fee == 4: 49 self._purchase_name = 'tournament_entry_4' 50 self._purchase_price_name = 'price.tournament_entry_4' 51 elif self._fee == 3: 52 self._purchase_name = 'tournament_entry_3' 53 self._purchase_price_name = 'price.tournament_entry_3' 54 elif self._fee == 2: 55 self._purchase_name = 'tournament_entry_2' 56 self._purchase_price_name = 'price.tournament_entry_2' 57 elif self._fee == 1: 58 self._purchase_name = 'tournament_entry_1' 59 self._purchase_price_name = 'price.tournament_entry_1' 60 else: 61 if self._fee != 0: 62 raise ValueError('invalid fee: ' + str(self._fee)) 63 self._purchase_name = 'tournament_entry_0' 64 self._purchase_price_name = 'price.tournament_entry_0' 65 66 self._purchase_price: int | None = None 67 68 self._on_close_call = on_close_call 69 if scale is None: 70 uiscale = bui.app.ui_v1.uiscale 71 scale = ( 72 2.3 73 if uiscale is bui.UIScale.SMALL 74 else 1.65 75 if uiscale is bui.UIScale.MEDIUM 76 else 1.23 77 ) 78 self._delegate = delegate 79 self._transitioning_out = False 80 81 self._tournament_activity = tournament_activity 82 83 self._width = 340 84 self._height = 225 85 86 bg_color = (0.5, 0.4, 0.6) 87 88 # Creates our root_widget. 89 super().__init__( 90 position=position, 91 size=(self._width, self._height), 92 scale=scale, 93 bg_color=bg_color, 94 offset=offset, 95 toolbar_visibility='menu_currency', 96 ) 97 98 self._last_ad_press_time = -9999.0 99 self._last_ticket_press_time = -9999.0 100 self._entering = False 101 self._launched = False 102 103 # Show the ad button only if we support ads *and* it has a level 1 fee. 104 self._do_ad_btn = bui.has_video_ads() and self._allow_ads 105 106 x_offs = 0 if self._do_ad_btn else 85 107 108 self._cancel_button = bui.buttonwidget( 109 parent=self.root_widget, 110 position=(20, self._height - 34), 111 size=(60, 60), 112 scale=0.5, 113 label='', 114 color=bg_color, 115 on_activate_call=self._on_cancel, 116 autoselect=True, 117 icon=bui.gettexture('crossOut'), 118 iconscale=1.2, 119 ) 120 121 self._title_text = bui.textwidget( 122 parent=self.root_widget, 123 position=(self._width * 0.5, self._height - 20), 124 size=(0, 0), 125 h_align='center', 126 v_align='center', 127 scale=0.6, 128 text=bui.Lstr(resource='tournamentEntryText'), 129 maxwidth=180, 130 color=(1, 1, 1, 0.4), 131 ) 132 133 btn = self._pay_with_tickets_button = bui.buttonwidget( 134 parent=self.root_widget, 135 position=(30 + x_offs, 60), 136 autoselect=True, 137 button_type='square', 138 size=(120, 120), 139 label='', 140 on_activate_call=self._on_pay_with_tickets_press, 141 ) 142 self._ticket_img_pos = (50 + x_offs, 94) 143 self._ticket_img_pos_free = (50 + x_offs, 80) 144 self._ticket_img = bui.imagewidget( 145 parent=self.root_widget, 146 draw_controller=btn, 147 size=(80, 80), 148 position=self._ticket_img_pos, 149 texture=bui.gettexture('tickets'), 150 ) 151 self._ticket_cost_text_position = (87 + x_offs, 88) 152 self._ticket_cost_text_position_free = (87 + x_offs, 120) 153 self._ticket_cost_text = bui.textwidget( 154 parent=self.root_widget, 155 draw_controller=btn, 156 position=self._ticket_cost_text_position, 157 size=(0, 0), 158 h_align='center', 159 v_align='center', 160 scale=0.6, 161 text='', 162 maxwidth=95, 163 color=(0, 1, 0), 164 ) 165 self._free_plays_remaining_text = bui.textwidget( 166 parent=self.root_widget, 167 draw_controller=btn, 168 position=(87 + x_offs, 78), 169 size=(0, 0), 170 h_align='center', 171 v_align='center', 172 scale=0.33, 173 text='', 174 maxwidth=95, 175 color=(0, 0.8, 0), 176 ) 177 self._pay_with_ad_btn: bui.Widget | None 178 if self._do_ad_btn: 179 btn = self._pay_with_ad_btn = bui.buttonwidget( 180 parent=self.root_widget, 181 position=(190, 60), 182 autoselect=True, 183 button_type='square', 184 size=(120, 120), 185 label='', 186 on_activate_call=self._on_pay_with_ad_press, 187 ) 188 self._pay_with_ad_img = bui.imagewidget( 189 parent=self.root_widget, 190 draw_controller=btn, 191 size=(80, 80), 192 position=(210, 94), 193 texture=bui.gettexture('tv'), 194 ) 195 196 self._ad_text_position = (251, 88) 197 self._ad_text_position_remaining = (251, 92) 198 have_ad_tries_remaining = ( 199 self._tournament_info['adTriesRemaining'] is not None 200 ) 201 self._ad_text = bui.textwidget( 202 parent=self.root_widget, 203 draw_controller=btn, 204 position=self._ad_text_position_remaining 205 if have_ad_tries_remaining 206 else self._ad_text_position, 207 size=(0, 0), 208 h_align='center', 209 v_align='center', 210 scale=0.6, 211 # Note: AdMob now requires rewarded ad usage 212 # specifically says 'Ad' in it. 213 text=bui.Lstr(resource='watchAnAdText'), 214 maxwidth=95, 215 color=(0, 1, 0), 216 ) 217 ad_plays_remaining_text = ( 218 '' 219 if not have_ad_tries_remaining 220 else '' + str(self._tournament_info['adTriesRemaining']) 221 ) 222 self._ad_plays_remaining_text = bui.textwidget( 223 parent=self.root_widget, 224 draw_controller=btn, 225 position=(251, 78), 226 size=(0, 0), 227 h_align='center', 228 v_align='center', 229 scale=0.33, 230 text=ad_plays_remaining_text, 231 maxwidth=95, 232 color=(0, 0.8, 0), 233 ) 234 235 bui.textwidget( 236 parent=self.root_widget, 237 position=(self._width * 0.5, 120), 238 size=(0, 0), 239 h_align='center', 240 v_align='center', 241 scale=0.6, 242 text=bui.Lstr( 243 resource='orText', subs=[('${A}', ''), ('${B}', '')] 244 ), 245 maxwidth=35, 246 color=(1, 1, 1, 0.5), 247 ) 248 else: 249 self._pay_with_ad_btn = None 250 251 self._get_tickets_button: bui.Widget | None = None 252 self._ticket_count_text: bui.Widget | None = None 253 if not bui.app.ui_v1.use_toolbars: 254 if bui.app.classic.allow_ticket_purchases: 255 self._get_tickets_button = bui.buttonwidget( 256 parent=self.root_widget, 257 position=(self._width - 190 + 125, self._height - 34), 258 autoselect=True, 259 scale=0.5, 260 size=(120, 60), 261 textcolor=(0.2, 1, 0.2), 262 label=bui.charstr(bui.SpecialChar.TICKET), 263 color=(0.65, 0.5, 0.8), 264 on_activate_call=self._on_get_tickets_press, 265 ) 266 else: 267 self._ticket_count_text = bui.textwidget( 268 parent=self.root_widget, 269 scale=0.5, 270 position=(self._width - 190 + 125, self._height - 34), 271 color=(0.2, 1, 0.2), 272 h_align='center', 273 v_align='center', 274 ) 275 276 self._seconds_remaining = None 277 278 bui.containerwidget( 279 edit=self.root_widget, cancel_button=self._cancel_button 280 ) 281 282 # Let's also ask the server for info about this tournament 283 # (time remaining, etc) so we can show the user time remaining, 284 # disallow entry if time has run out, etc. 285 # xoffs = 104 if bui.app.ui.use_toolbars else 0 286 self._time_remaining_text = bui.textwidget( 287 parent=self.root_widget, 288 position=(self._width / 2, 28), 289 size=(0, 0), 290 h_align='center', 291 v_align='center', 292 text='-', 293 scale=0.65, 294 maxwidth=100, 295 flatness=1.0, 296 color=(0.7, 0.7, 0.7), 297 ) 298 self._time_remaining_label_text = bui.textwidget( 299 parent=self.root_widget, 300 position=(self._width / 2, 45), 301 size=(0, 0), 302 h_align='center', 303 v_align='center', 304 text=bui.Lstr(resource='coopSelectWindow.timeRemainingText'), 305 scale=0.45, 306 flatness=1.0, 307 maxwidth=100, 308 color=(0.7, 0.7, 0.7), 309 ) 310 311 self._last_query_time: float | None = None 312 313 # If there seems to be a relatively-recent valid cached info for this 314 # tournament, use it. Otherwise we'll kick off a query ourselves. 315 if ( 316 self._tournament_id in bui.app.classic.accounts.tournament_info 317 and bui.app.classic.accounts.tournament_info[self._tournament_id][ 318 'valid' 319 ] 320 and ( 321 bui.apptime() 322 - bui.app.classic.accounts.tournament_info[self._tournament_id][ 323 'timeReceived' 324 ] 325 < 60 * 5 326 ) 327 ): 328 try: 329 info = bui.app.classic.accounts.tournament_info[ 330 self._tournament_id 331 ] 332 self._seconds_remaining = max( 333 0, 334 info['timeRemaining'] 335 - int((bui.apptime() - info['timeReceived'])), 336 ) 337 self._have_valid_data = True 338 self._last_query_time = bui.apptime() 339 except Exception: 340 logging.exception('Error using valid tourney data.') 341 self._have_valid_data = False 342 else: 343 self._have_valid_data = False 344 345 self._fg_state = bui.app.fg_state 346 self._running_query = False 347 self._update_timer = bui.AppTimer( 348 1.0, bui.WeakCall(self._update), repeat=True 349 ) 350 self._update() 351 self._restore_state()
def
on_popup_cancel(self) -> None:
Called when the popup is canceled.
Cancels can occur due to clicking outside the window, hitting escape, etc.