bastd.ui.tournamententry

Defines a popup window for entering tournaments.

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

Popup window for entering tournaments.

TournamentEntryWindow( tournament_id: str, tournament_activity: ba._activity.Activity | None = None, position: tuple[float, float] = (0.0, 0.0), delegate: Any = None, scale: float | None = None, offset: tuple[float, float] = (0.0, 0.0), on_close_call: Optional[Callable[[], Any]] = None)
 21    def __init__(
 22        self,
 23        tournament_id: str,
 24        tournament_activity: ba.Activity | None = None,
 25        position: tuple[float, float] = (0.0, 0.0),
 26        delegate: Any = None,
 27        scale: float | None = None,
 28        offset: tuple[float, float] = (0.0, 0.0),
 29        on_close_call: Callable[[], Any] | None = None,
 30    ):
 31        # Needs some tidying.
 32        # pylint: disable=too-many-branches
 33        # pylint: disable=too-many-statements
 34
 35        ba.set_analytics_screen('Tournament Entry Window')
 36
 37        self._tournament_id = tournament_id
 38        self._tournament_info = ba.app.accounts_v1.tournament_info[
 39            self._tournament_id
 40        ]
 41
 42        # Set a few vars depending on the tourney fee.
 43        self._fee = self._tournament_info['fee']
 44        self._allow_ads = self._tournament_info['allowAds']
 45        if self._fee == 4:
 46            self._purchase_name = 'tournament_entry_4'
 47            self._purchase_price_name = 'price.tournament_entry_4'
 48        elif self._fee == 3:
 49            self._purchase_name = 'tournament_entry_3'
 50            self._purchase_price_name = 'price.tournament_entry_3'
 51        elif self._fee == 2:
 52            self._purchase_name = 'tournament_entry_2'
 53            self._purchase_price_name = 'price.tournament_entry_2'
 54        elif self._fee == 1:
 55            self._purchase_name = 'tournament_entry_1'
 56            self._purchase_price_name = 'price.tournament_entry_1'
 57        else:
 58            if self._fee != 0:
 59                raise ValueError('invalid fee: ' + str(self._fee))
 60            self._purchase_name = 'tournament_entry_0'
 61            self._purchase_price_name = 'price.tournament_entry_0'
 62
 63        self._purchase_price: int | None = None
 64
 65        self._on_close_call = on_close_call
 66        if scale is None:
 67            uiscale = ba.app.ui.uiscale
 68            scale = (
 69                2.3
 70                if uiscale is ba.UIScale.SMALL
 71                else 1.65
 72                if uiscale is ba.UIScale.MEDIUM
 73                else 1.23
 74            )
 75        self._delegate = delegate
 76        self._transitioning_out = False
 77
 78        self._tournament_activity = tournament_activity
 79
 80        self._width = 340
 81        self._height = 225
 82
 83        bg_color = (0.5, 0.4, 0.6)
 84
 85        # Creates our root_widget.
 86        popup.PopupWindow.__init__(
 87            self,
 88            position=position,
 89            size=(self._width, self._height),
 90            scale=scale,
 91            bg_color=bg_color,
 92            offset=offset,
 93            toolbar_visibility='menu_currency',
 94        )
 95
 96        self._last_ad_press_time = -9999.0
 97        self._last_ticket_press_time = -9999.0
 98        self._entering = False
 99        self._launched = False
100
101        # Show the ad button only if we support ads *and* it has a level 1 fee.
102        self._do_ad_btn = ba.internal.has_video_ads() and self._allow_ads
103
104        x_offs = 0 if self._do_ad_btn else 85
105
106        self._cancel_button = ba.buttonwidget(
107            parent=self.root_widget,
108            position=(20, self._height - 34),
109            size=(60, 60),
110            scale=0.5,
111            label='',
112            color=bg_color,
113            on_activate_call=self._on_cancel,
114            autoselect=True,
115            icon=ba.gettexture('crossOut'),
116            iconscale=1.2,
117        )
118
119        self._title_text = ba.textwidget(
120            parent=self.root_widget,
121            position=(self._width * 0.5, self._height - 20),
122            size=(0, 0),
123            h_align='center',
124            v_align='center',
125            scale=0.6,
126            text=ba.Lstr(resource='tournamentEntryText'),
127            maxwidth=180,
128            color=(1, 1, 1, 0.4),
129        )
130
131        btn = self._pay_with_tickets_button = ba.buttonwidget(
132            parent=self.root_widget,
133            position=(30 + x_offs, 60),
134            autoselect=True,
135            button_type='square',
136            size=(120, 120),
137            label='',
138            on_activate_call=self._on_pay_with_tickets_press,
139        )
140        self._ticket_img_pos = (50 + x_offs, 94)
141        self._ticket_img_pos_free = (50 + x_offs, 80)
142        self._ticket_img = ba.imagewidget(
143            parent=self.root_widget,
144            draw_controller=btn,
145            size=(80, 80),
146            position=self._ticket_img_pos,
147            texture=ba.gettexture('tickets'),
148        )
149        self._ticket_cost_text_position = (87 + x_offs, 88)
150        self._ticket_cost_text_position_free = (87 + x_offs, 120)
151        self._ticket_cost_text = ba.textwidget(
152            parent=self.root_widget,
153            draw_controller=btn,
154            position=self._ticket_cost_text_position,
155            size=(0, 0),
156            h_align='center',
157            v_align='center',
158            scale=0.6,
159            text='',
160            maxwidth=95,
161            color=(0, 1, 0),
162        )
163        self._free_plays_remaining_text = ba.textwidget(
164            parent=self.root_widget,
165            draw_controller=btn,
166            position=(87 + x_offs, 78),
167            size=(0, 0),
168            h_align='center',
169            v_align='center',
170            scale=0.33,
171            text='',
172            maxwidth=95,
173            color=(0, 0.8, 0),
174        )
175        self._pay_with_ad_btn: ba.Widget | None
176        if self._do_ad_btn:
177            btn = self._pay_with_ad_btn = ba.buttonwidget(
178                parent=self.root_widget,
179                position=(190, 60),
180                autoselect=True,
181                button_type='square',
182                size=(120, 120),
183                label='',
184                on_activate_call=self._on_pay_with_ad_press,
185            )
186            self._pay_with_ad_img = ba.imagewidget(
187                parent=self.root_widget,
188                draw_controller=btn,
189                size=(80, 80),
190                position=(210, 94),
191                texture=ba.gettexture('tv'),
192            )
193
194            self._ad_text_position = (251, 88)
195            self._ad_text_position_remaining = (251, 92)
196            have_ad_tries_remaining = (
197                self._tournament_info['adTriesRemaining'] is not None
198            )
199            self._ad_text = ba.textwidget(
200                parent=self.root_widget,
201                draw_controller=btn,
202                position=self._ad_text_position_remaining
203                if have_ad_tries_remaining
204                else self._ad_text_position,
205                size=(0, 0),
206                h_align='center',
207                v_align='center',
208                scale=0.6,
209                # Note: AdMob now requires rewarded ad usage
210                # specifically says 'Ad' in it.
211                text=ba.Lstr(resource='watchAnAdText'),
212                maxwidth=95,
213                color=(0, 1, 0),
214            )
215            ad_plays_remaining_text = (
216                ''
217                if not have_ad_tries_remaining
218                else '' + str(self._tournament_info['adTriesRemaining'])
219            )
220            self._ad_plays_remaining_text = ba.textwidget(
221                parent=self.root_widget,
222                draw_controller=btn,
223                position=(251, 78),
224                size=(0, 0),
225                h_align='center',
226                v_align='center',
227                scale=0.33,
228                text=ad_plays_remaining_text,
229                maxwidth=95,
230                color=(0, 0.8, 0),
231            )
232
233            ba.textwidget(
234                parent=self.root_widget,
235                position=(self._width * 0.5, 120),
236                size=(0, 0),
237                h_align='center',
238                v_align='center',
239                scale=0.6,
240                text=ba.Lstr(
241                    resource='orText', subs=[('${A}', ''), ('${B}', '')]
242                ),
243                maxwidth=35,
244                color=(1, 1, 1, 0.5),
245            )
246        else:
247            self._pay_with_ad_btn = None
248
249        self._get_tickets_button: ba.Widget | None = None
250        self._ticket_count_text: ba.Widget | None = None
251        if not ba.app.ui.use_toolbars:
252            if ba.app.allow_ticket_purchases:
253                self._get_tickets_button = ba.buttonwidget(
254                    parent=self.root_widget,
255                    position=(self._width - 190 + 125, self._height - 34),
256                    autoselect=True,
257                    scale=0.5,
258                    size=(120, 60),
259                    textcolor=(0.2, 1, 0.2),
260                    label=ba.charstr(ba.SpecialChar.TICKET),
261                    color=(0.65, 0.5, 0.8),
262                    on_activate_call=self._on_get_tickets_press,
263                )
264            else:
265                self._ticket_count_text = ba.textwidget(
266                    parent=self.root_widget,
267                    scale=0.5,
268                    position=(self._width - 190 + 125, self._height - 34),
269                    color=(0.2, 1, 0.2),
270                    h_align='center',
271                    v_align='center',
272                )
273
274        self._seconds_remaining = None
275
276        ba.containerwidget(
277            edit=self.root_widget, cancel_button=self._cancel_button
278        )
279
280        # Let's also ask the server for info about this tournament
281        # (time remaining, etc) so we can show the user time remaining,
282        # disallow entry if time has run out, etc.
283        # xoffs = 104 if ba.app.ui.use_toolbars else 0
284        self._time_remaining_text = ba.textwidget(
285            parent=self.root_widget,
286            position=(self._width / 2, 28),
287            size=(0, 0),
288            h_align='center',
289            v_align='center',
290            text='-',
291            scale=0.65,
292            maxwidth=100,
293            flatness=1.0,
294            color=(0.7, 0.7, 0.7),
295        )
296        self._time_remaining_label_text = ba.textwidget(
297            parent=self.root_widget,
298            position=(self._width / 2, 45),
299            size=(0, 0),
300            h_align='center',
301            v_align='center',
302            text=ba.Lstr(resource='coopSelectWindow.timeRemainingText'),
303            scale=0.45,
304            flatness=1.0,
305            maxwidth=100,
306            color=(0.7, 0.7, 0.7),
307        )
308
309        self._last_query_time: float | None = None
310
311        # If there seems to be a relatively-recent valid cached info for this
312        # tournament, use it. Otherwise we'll kick off a query ourselves.
313        if (
314            self._tournament_id in ba.app.accounts_v1.tournament_info
315            and ba.app.accounts_v1.tournament_info[self._tournament_id]['valid']
316            and (
317                ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS)
318                - ba.app.accounts_v1.tournament_info[self._tournament_id][
319                    'timeReceived'
320                ]
321                < 1000 * 60 * 5
322            )
323        ):
324            try:
325                info = ba.app.accounts_v1.tournament_info[self._tournament_id]
326                self._seconds_remaining = max(
327                    0,
328                    info['timeRemaining']
329                    - int(
330                        (
331                            ba.time(
332                                ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS
333                            )
334                            - info['timeReceived']
335                        )
336                        / 1000
337                    ),
338                )
339                self._have_valid_data = True
340                self._last_query_time = ba.time(ba.TimeType.REAL)
341            except Exception:
342                ba.print_exception('error using valid tourney data')
343                self._have_valid_data = False
344        else:
345            self._have_valid_data = False
346
347        self._fg_state = ba.app.fg_state
348        self._running_query = False
349        self._update_timer = ba.Timer(
350            1.0,
351            ba.WeakCall(self._update),
352            repeat=True,
353            timetype=ba.TimeType.REAL,
354        )
355        self._update()
356        self._restore_state()
def on_popup_cancel(self) -> None:
757    def on_popup_cancel(self) -> None:
758        ba.playsound(ba.getsound('swish'))
759        self._on_cancel()

Called when the popup is canceled.

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