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