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