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()
class TournamentEntryWindow(bastd.ui.popup.PopupWindow):
 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()
def on_popup_cancel(self) -> None:
635    def on_popup_cancel(self) -> None:
636        ba.playsound(ba.getsound('swish'))
637        self._on_cancel()

Called when the popup is canceled.

Cancels can occur due to clicking outside the window, hitting escape, etc.