bastd.ui.playoptions

Provides a window for configuring play options.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides a window for configuring play options."""
  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
 15
 16
 17class PlayOptionsWindow(popup.PopupWindow):
 18    """A popup window for configuring play options."""
 19
 20    def __init__(
 21        self,
 22        sessiontype: type[ba.Session],
 23        playlist: str,
 24        scale_origin: tuple[float, float],
 25        delegate: Any = None,
 26    ):
 27        # FIXME: Tidy this up.
 28        # pylint: disable=too-many-branches
 29        # pylint: disable=too-many-statements
 30        # pylint: disable=too-many-locals
 31        from ba.internal import get_map_class, getclass, filter_playlist
 32        from bastd.ui.playlist import PlaylistTypeVars
 33
 34        self._r = 'gameListWindow'
 35        self._delegate = delegate
 36        self._pvars = PlaylistTypeVars(sessiontype)
 37        self._transitioning_out = False
 38
 39        # We behave differently if we're being used for playlist selection
 40        # vs starting a game directly (should make this more elegant).
 41        self._selecting_mode = ba.app.ui.selecting_private_party_playlist
 42
 43        self._do_randomize_val = ba.app.config.get(
 44            self._pvars.config_name + ' Playlist Randomize', 0
 45        )
 46
 47        self._sessiontype = sessiontype
 48        self._playlist = playlist
 49
 50        self._width = 500.0
 51        self._height = 330.0 - 50.0
 52
 53        # In teams games, show the custom names/colors button.
 54        if self._sessiontype is ba.DualTeamSession:
 55            self._height += 50.0
 56
 57        self._row_height = 45.0
 58
 59        # Grab our maps to display.
 60        model_opaque = ba.getmodel('level_select_button_opaque')
 61        model_transparent = ba.getmodel('level_select_button_transparent')
 62        mask_tex = ba.gettexture('mapPreviewMask')
 63
 64        # Poke into this playlist and see if we can display some of its maps.
 65        map_textures = []
 66        map_texture_entries = []
 67        rows = 0
 68        columns = 0
 69        game_count = 0
 70        scl = 0.35
 71        c_width_total = 0.0
 72        try:
 73            max_columns = 5
 74            name = playlist
 75            if name == '__default__':
 76                plst = self._pvars.get_default_list_call()
 77            else:
 78                try:
 79                    plst = ba.app.config[
 80                        self._pvars.config_name + ' Playlists'
 81                    ][name]
 82                except Exception:
 83                    print(
 84                        'ERROR INFO: self._config_name is:',
 85                        self._pvars.config_name,
 86                    )
 87                    print(
 88                        'ERROR INFO: playlist names are:',
 89                        list(
 90                            ba.app.config[
 91                                self._pvars.config_name + ' Playlists'
 92                            ].keys()
 93                        ),
 94                    )
 95                    raise
 96            plst = filter_playlist(
 97                plst,
 98                self._sessiontype,
 99                remove_unowned=False,
100                mark_unowned=True,
101                name=name,
102            )
103            game_count = len(plst)
104            for entry in plst:
105                mapname = entry['settings']['map']
106                maptype: type[ba.Map] | None
107                try:
108                    maptype = get_map_class(mapname)
109                except ba.NotFoundError:
110                    maptype = None
111                if maptype is not None:
112                    tex_name = maptype.get_preview_texture_name()
113                    if tex_name is not None:
114                        map_textures.append(tex_name)
115                        map_texture_entries.append(entry)
116            rows = (max(0, len(map_textures) - 1) // max_columns) + 1
117            columns = min(max_columns, len(map_textures))
118
119            if len(map_textures) == 1:
120                scl = 1.1
121            elif len(map_textures) == 2:
122                scl = 0.7
123            elif len(map_textures) == 3:
124                scl = 0.55
125            else:
126                scl = 0.35
127            self._row_height = 128.0 * scl
128            c_width_total = scl * 250.0 * columns
129            if map_textures:
130                self._height += self._row_height * rows
131
132        except Exception:
133            ba.print_exception('Error listing playlist maps.')
134
135        show_shuffle_check_box = game_count > 1
136
137        if show_shuffle_check_box:
138            self._height += 40
139
140        # Creates our _root_widget.
141        uiscale = ba.app.ui.uiscale
142        scale = (
143            1.69
144            if uiscale is ba.UIScale.SMALL
145            else 1.1
146            if uiscale is ba.UIScale.MEDIUM
147            else 0.85
148        )
149        super().__init__(
150            position=scale_origin, size=(self._width, self._height), scale=scale
151        )
152
153        playlist_name: str | ba.Lstr = (
154            self._pvars.default_list_name
155            if playlist == '__default__'
156            else playlist
157        )
158        self._title_text = ba.textwidget(
159            parent=self.root_widget,
160            position=(self._width * 0.5, self._height - 89 + 51),
161            size=(0, 0),
162            text=playlist_name,
163            scale=1.4,
164            color=(1, 1, 1),
165            maxwidth=self._width * 0.7,
166            h_align='center',
167            v_align='center',
168        )
169
170        self._cancel_button = ba.buttonwidget(
171            parent=self.root_widget,
172            position=(25, self._height - 53),
173            size=(50, 50),
174            scale=0.7,
175            label='',
176            color=(0.42, 0.73, 0.2),
177            on_activate_call=self._on_cancel_press,
178            autoselect=True,
179            icon=ba.gettexture('crossOut'),
180            iconscale=1.2,
181        )
182
183        h_offs_img = self._width * 0.5 - c_width_total * 0.5
184        v_offs_img = self._height - 118 - scl * 125.0 + 50
185        bottom_row_buttons = []
186        self._have_at_least_one_owned = False
187
188        for row in range(rows):
189            for col in range(columns):
190                tex_index = row * columns + col
191                if tex_index < len(map_textures):
192                    tex_name = map_textures[tex_index]
193                    h = h_offs_img + scl * 250 * col
194                    v = v_offs_img - self._row_height * row
195                    entry = map_texture_entries[tex_index]
196                    owned = not (
197                        ('is_unowned_map' in entry and entry['is_unowned_map'])
198                        or (
199                            'is_unowned_game' in entry
200                            and entry['is_unowned_game']
201                        )
202                    )
203
204                    if owned:
205                        self._have_at_least_one_owned = True
206
207                    try:
208                        desc = getclass(
209                            entry['type'], subclassof=ba.GameActivity
210                        ).get_settings_display_string(entry)
211                        if not owned:
212                            desc = ba.Lstr(
213                                value='${DESC}\n${UNLOCK}',
214                                subs=[
215                                    ('${DESC}', desc),
216                                    (
217                                        '${UNLOCK}',
218                                        ba.Lstr(
219                                            resource='unlockThisInTheStoreText'
220                                        ),
221                                    ),
222                                ],
223                            )
224                        desc_color = (0, 1, 0) if owned else (1, 0, 0)
225                    except Exception:
226                        desc = ba.Lstr(value='(invalid)')
227                        desc_color = (1, 0, 0)
228
229                    btn = ba.buttonwidget(
230                        parent=self.root_widget,
231                        size=(scl * 240.0, scl * 120.0),
232                        position=(h, v),
233                        texture=ba.gettexture(tex_name if owned else 'empty'),
234                        model_opaque=model_opaque if owned else None,
235                        on_activate_call=ba.Call(
236                            ba.screenmessage, desc, desc_color
237                        ),
238                        label='',
239                        color=(1, 1, 1),
240                        autoselect=True,
241                        extra_touch_border_scale=0.0,
242                        model_transparent=model_transparent if owned else None,
243                        mask_texture=mask_tex if owned else None,
244                    )
245                    if row == 0 and col == 0:
246                        ba.widget(edit=self._cancel_button, down_widget=btn)
247                    if row == rows - 1:
248                        bottom_row_buttons.append(btn)
249                    if not owned:
250
251                        # Ewww; buttons don't currently have alpha so in this
252                        # case we draw an image over our button with an empty
253                        # texture on it.
254                        ba.imagewidget(
255                            parent=self.root_widget,
256                            size=(scl * 260.0, scl * 130.0),
257                            position=(h - 10.0 * scl, v - 4.0 * scl),
258                            draw_controller=btn,
259                            color=(1, 1, 1),
260                            texture=ba.gettexture(tex_name),
261                            model_opaque=model_opaque,
262                            opacity=0.25,
263                            model_transparent=model_transparent,
264                            mask_texture=mask_tex,
265                        )
266
267                        ba.imagewidget(
268                            parent=self.root_widget,
269                            size=(scl * 100, scl * 100),
270                            draw_controller=btn,
271                            position=(h + scl * 70, v + scl * 10),
272                            texture=ba.gettexture('lock'),
273                        )
274
275        # Team names/colors.
276        self._custom_colors_names_button: ba.Widget | None
277        if self._sessiontype is ba.DualTeamSession:
278            y_offs = 50 if show_shuffle_check_box else 0
279            self._custom_colors_names_button = ba.buttonwidget(
280                parent=self.root_widget,
281                position=(100, 200 + y_offs),
282                size=(290, 35),
283                on_activate_call=ba.WeakCall(self._custom_colors_names_press),
284                autoselect=True,
285                textcolor=(0.8, 0.8, 0.8),
286                label=ba.Lstr(resource='teamNamesColorText'),
287            )
288            if not ba.app.accounts_v1.have_pro():
289                ba.imagewidget(
290                    parent=self.root_widget,
291                    size=(30, 30),
292                    position=(95, 202 + y_offs),
293                    texture=ba.gettexture('lock'),
294                    draw_controller=self._custom_colors_names_button,
295                )
296        else:
297            self._custom_colors_names_button = None
298
299        # Shuffle.
300        def _cb_callback(val: bool) -> None:
301            self._do_randomize_val = val
302            cfg = ba.app.config
303            cfg[
304                self._pvars.config_name + ' Playlist Randomize'
305            ] = self._do_randomize_val
306            cfg.commit()
307
308        if show_shuffle_check_box:
309            self._shuffle_check_box = ba.checkboxwidget(
310                parent=self.root_widget,
311                position=(110, 200),
312                scale=1.0,
313                size=(250, 30),
314                autoselect=True,
315                text=ba.Lstr(resource=self._r + '.shuffleGameOrderText'),
316                maxwidth=300,
317                textcolor=(0.8, 0.8, 0.8),
318                value=self._do_randomize_val,
319                on_value_change_call=_cb_callback,
320            )
321
322        # Show tutorial.
323        show_tutorial = bool(ba.app.config.get('Show Tutorial', True))
324
325        def _cb_callback_2(val: bool) -> None:
326            cfg = ba.app.config
327            cfg['Show Tutorial'] = val
328            cfg.commit()
329
330        self._show_tutorial_check_box = ba.checkboxwidget(
331            parent=self.root_widget,
332            position=(110, 151),
333            scale=1.0,
334            size=(250, 30),
335            autoselect=True,
336            text=ba.Lstr(resource=self._r + '.showTutorialText'),
337            maxwidth=300,
338            textcolor=(0.8, 0.8, 0.8),
339            value=show_tutorial,
340            on_value_change_call=_cb_callback_2,
341        )
342
343        # Grumble: current autoselect doesn't do a very good job
344        # with checkboxes.
345        if self._custom_colors_names_button is not None:
346            for btn in bottom_row_buttons:
347                ba.widget(
348                    edit=btn, down_widget=self._custom_colors_names_button
349                )
350            if show_shuffle_check_box:
351                ba.widget(
352                    edit=self._custom_colors_names_button,
353                    down_widget=self._shuffle_check_box,
354                )
355                ba.widget(
356                    edit=self._shuffle_check_box,
357                    up_widget=self._custom_colors_names_button,
358                )
359            else:
360                ba.widget(
361                    edit=self._custom_colors_names_button,
362                    down_widget=self._show_tutorial_check_box,
363                )
364                ba.widget(
365                    edit=self._show_tutorial_check_box,
366                    up_widget=self._custom_colors_names_button,
367                )
368
369        self._ok_button = ba.buttonwidget(
370            parent=self.root_widget,
371            position=(70, 44),
372            size=(200, 45),
373            scale=1.8,
374            text_res_scale=1.5,
375            on_activate_call=self._on_ok_press,
376            autoselect=True,
377            label=ba.Lstr(
378                resource='okText' if self._selecting_mode else 'playText'
379            ),
380        )
381
382        ba.widget(edit=self._ok_button, up_widget=self._show_tutorial_check_box)
383
384        ba.containerwidget(
385            edit=self.root_widget,
386            start_button=self._ok_button,
387            cancel_button=self._cancel_button,
388            selected_child=self._ok_button,
389        )
390
391        # Update now and once per second.
392        self._update_timer = ba.Timer(
393            1.0,
394            ba.WeakCall(self._update),
395            timetype=ba.TimeType.REAL,
396            repeat=True,
397        )
398        self._update()
399
400    def _custom_colors_names_press(self) -> None:
401        from bastd.ui.account import show_sign_in_prompt
402        from bastd.ui.teamnamescolors import TeamNamesColorsWindow
403        from bastd.ui.purchase import PurchaseWindow
404
405        if not ba.app.accounts_v1.have_pro():
406            if ba.internal.get_v1_account_state() != 'signed_in':
407                show_sign_in_prompt()
408            else:
409                PurchaseWindow(items=['pro'])
410            self._transition_out()
411            return
412        assert self._custom_colors_names_button
413        TeamNamesColorsWindow(
414            scale_origin=(
415                self._custom_colors_names_button.get_screen_space_center()
416            )
417        )
418
419    def _does_target_playlist_exist(self) -> bool:
420        if self._playlist == '__default__':
421            return True
422        return self._playlist in ba.app.config.get(
423            self._pvars.config_name + ' Playlists', {}
424        )
425
426    def _update(self) -> None:
427        # All we do here is make sure our targeted playlist still exists,
428        # and close ourself if not.
429        if not self._does_target_playlist_exist():
430            self._transition_out()
431
432    def _transition_out(self, transition: str = 'out_scale') -> None:
433        if not self._transitioning_out:
434            self._transitioning_out = True
435            ba.containerwidget(edit=self.root_widget, transition=transition)
436
437    def on_popup_cancel(self) -> None:
438        ba.playsound(ba.getsound('swish'))
439        self._transition_out()
440
441    def _on_cancel_press(self) -> None:
442        self._transition_out()
443
444    def _on_ok_press(self) -> None:
445
446        # Disallow if our playlist has disappeared.
447        if not self._does_target_playlist_exist():
448            return
449
450        # Disallow if we have no unlocked games.
451        if not self._have_at_least_one_owned:
452            ba.playsound(ba.getsound('error'))
453            ba.screenmessage(
454                ba.Lstr(resource='playlistNoValidGamesErrorText'),
455                color=(1, 0, 0),
456            )
457            return
458
459        cfg = ba.app.config
460        cfg[self._pvars.config_name + ' Playlist Selection'] = self._playlist
461
462        # Head back to the gather window in playlist-select mode
463        # or start the game in regular mode.
464        if self._selecting_mode:
465            from bastd.ui.gather import GatherWindow
466
467            if self._sessiontype is ba.FreeForAllSession:
468                typename = 'ffa'
469            elif self._sessiontype is ba.DualTeamSession:
470                typename = 'teams'
471            else:
472                raise RuntimeError('Only teams and ffa currently supported')
473            cfg['Private Party Host Session Type'] = typename
474            ba.playsound(ba.getsound('gunCocking'))
475            ba.app.ui.set_main_menu_window(
476                GatherWindow(transition='in_right').get_root_widget()
477            )
478            self._transition_out(transition='out_left')
479            if self._delegate is not None:
480                self._delegate.on_play_options_window_run_game()
481        else:
482            ba.internal.fade_screen(False, endcall=self._run_selected_playlist)
483            ba.internal.lock_all_input()
484            self._transition_out(transition='out_left')
485            if self._delegate is not None:
486                self._delegate.on_play_options_window_run_game()
487
488        cfg.commit()
489
490    def _run_selected_playlist(self) -> None:
491        ba.internal.unlock_all_input()
492        try:
493            ba.internal.new_host_session(self._sessiontype)
494        except Exception:
495            from bastd import mainmenu
496
497            ba.print_exception('exception running session', self._sessiontype)
498
499            # Drop back into a main menu session.
500            ba.internal.new_host_session(mainmenu.MainMenuSession)
class PlayOptionsWindow(bastd.ui.popup.PopupWindow):
 18class PlayOptionsWindow(popup.PopupWindow):
 19    """A popup window for configuring play options."""
 20
 21    def __init__(
 22        self,
 23        sessiontype: type[ba.Session],
 24        playlist: str,
 25        scale_origin: tuple[float, float],
 26        delegate: Any = None,
 27    ):
 28        # FIXME: Tidy this up.
 29        # pylint: disable=too-many-branches
 30        # pylint: disable=too-many-statements
 31        # pylint: disable=too-many-locals
 32        from ba.internal import get_map_class, getclass, filter_playlist
 33        from bastd.ui.playlist import PlaylistTypeVars
 34
 35        self._r = 'gameListWindow'
 36        self._delegate = delegate
 37        self._pvars = PlaylistTypeVars(sessiontype)
 38        self._transitioning_out = False
 39
 40        # We behave differently if we're being used for playlist selection
 41        # vs starting a game directly (should make this more elegant).
 42        self._selecting_mode = ba.app.ui.selecting_private_party_playlist
 43
 44        self._do_randomize_val = ba.app.config.get(
 45            self._pvars.config_name + ' Playlist Randomize', 0
 46        )
 47
 48        self._sessiontype = sessiontype
 49        self._playlist = playlist
 50
 51        self._width = 500.0
 52        self._height = 330.0 - 50.0
 53
 54        # In teams games, show the custom names/colors button.
 55        if self._sessiontype is ba.DualTeamSession:
 56            self._height += 50.0
 57
 58        self._row_height = 45.0
 59
 60        # Grab our maps to display.
 61        model_opaque = ba.getmodel('level_select_button_opaque')
 62        model_transparent = ba.getmodel('level_select_button_transparent')
 63        mask_tex = ba.gettexture('mapPreviewMask')
 64
 65        # Poke into this playlist and see if we can display some of its maps.
 66        map_textures = []
 67        map_texture_entries = []
 68        rows = 0
 69        columns = 0
 70        game_count = 0
 71        scl = 0.35
 72        c_width_total = 0.0
 73        try:
 74            max_columns = 5
 75            name = playlist
 76            if name == '__default__':
 77                plst = self._pvars.get_default_list_call()
 78            else:
 79                try:
 80                    plst = ba.app.config[
 81                        self._pvars.config_name + ' Playlists'
 82                    ][name]
 83                except Exception:
 84                    print(
 85                        'ERROR INFO: self._config_name is:',
 86                        self._pvars.config_name,
 87                    )
 88                    print(
 89                        'ERROR INFO: playlist names are:',
 90                        list(
 91                            ba.app.config[
 92                                self._pvars.config_name + ' Playlists'
 93                            ].keys()
 94                        ),
 95                    )
 96                    raise
 97            plst = filter_playlist(
 98                plst,
 99                self._sessiontype,
100                remove_unowned=False,
101                mark_unowned=True,
102                name=name,
103            )
104            game_count = len(plst)
105            for entry in plst:
106                mapname = entry['settings']['map']
107                maptype: type[ba.Map] | None
108                try:
109                    maptype = get_map_class(mapname)
110                except ba.NotFoundError:
111                    maptype = None
112                if maptype is not None:
113                    tex_name = maptype.get_preview_texture_name()
114                    if tex_name is not None:
115                        map_textures.append(tex_name)
116                        map_texture_entries.append(entry)
117            rows = (max(0, len(map_textures) - 1) // max_columns) + 1
118            columns = min(max_columns, len(map_textures))
119
120            if len(map_textures) == 1:
121                scl = 1.1
122            elif len(map_textures) == 2:
123                scl = 0.7
124            elif len(map_textures) == 3:
125                scl = 0.55
126            else:
127                scl = 0.35
128            self._row_height = 128.0 * scl
129            c_width_total = scl * 250.0 * columns
130            if map_textures:
131                self._height += self._row_height * rows
132
133        except Exception:
134            ba.print_exception('Error listing playlist maps.')
135
136        show_shuffle_check_box = game_count > 1
137
138        if show_shuffle_check_box:
139            self._height += 40
140
141        # Creates our _root_widget.
142        uiscale = ba.app.ui.uiscale
143        scale = (
144            1.69
145            if uiscale is ba.UIScale.SMALL
146            else 1.1
147            if uiscale is ba.UIScale.MEDIUM
148            else 0.85
149        )
150        super().__init__(
151            position=scale_origin, size=(self._width, self._height), scale=scale
152        )
153
154        playlist_name: str | ba.Lstr = (
155            self._pvars.default_list_name
156            if playlist == '__default__'
157            else playlist
158        )
159        self._title_text = ba.textwidget(
160            parent=self.root_widget,
161            position=(self._width * 0.5, self._height - 89 + 51),
162            size=(0, 0),
163            text=playlist_name,
164            scale=1.4,
165            color=(1, 1, 1),
166            maxwidth=self._width * 0.7,
167            h_align='center',
168            v_align='center',
169        )
170
171        self._cancel_button = ba.buttonwidget(
172            parent=self.root_widget,
173            position=(25, self._height - 53),
174            size=(50, 50),
175            scale=0.7,
176            label='',
177            color=(0.42, 0.73, 0.2),
178            on_activate_call=self._on_cancel_press,
179            autoselect=True,
180            icon=ba.gettexture('crossOut'),
181            iconscale=1.2,
182        )
183
184        h_offs_img = self._width * 0.5 - c_width_total * 0.5
185        v_offs_img = self._height - 118 - scl * 125.0 + 50
186        bottom_row_buttons = []
187        self._have_at_least_one_owned = False
188
189        for row in range(rows):
190            for col in range(columns):
191                tex_index = row * columns + col
192                if tex_index < len(map_textures):
193                    tex_name = map_textures[tex_index]
194                    h = h_offs_img + scl * 250 * col
195                    v = v_offs_img - self._row_height * row
196                    entry = map_texture_entries[tex_index]
197                    owned = not (
198                        ('is_unowned_map' in entry and entry['is_unowned_map'])
199                        or (
200                            'is_unowned_game' in entry
201                            and entry['is_unowned_game']
202                        )
203                    )
204
205                    if owned:
206                        self._have_at_least_one_owned = True
207
208                    try:
209                        desc = getclass(
210                            entry['type'], subclassof=ba.GameActivity
211                        ).get_settings_display_string(entry)
212                        if not owned:
213                            desc = ba.Lstr(
214                                value='${DESC}\n${UNLOCK}',
215                                subs=[
216                                    ('${DESC}', desc),
217                                    (
218                                        '${UNLOCK}',
219                                        ba.Lstr(
220                                            resource='unlockThisInTheStoreText'
221                                        ),
222                                    ),
223                                ],
224                            )
225                        desc_color = (0, 1, 0) if owned else (1, 0, 0)
226                    except Exception:
227                        desc = ba.Lstr(value='(invalid)')
228                        desc_color = (1, 0, 0)
229
230                    btn = ba.buttonwidget(
231                        parent=self.root_widget,
232                        size=(scl * 240.0, scl * 120.0),
233                        position=(h, v),
234                        texture=ba.gettexture(tex_name if owned else 'empty'),
235                        model_opaque=model_opaque if owned else None,
236                        on_activate_call=ba.Call(
237                            ba.screenmessage, desc, desc_color
238                        ),
239                        label='',
240                        color=(1, 1, 1),
241                        autoselect=True,
242                        extra_touch_border_scale=0.0,
243                        model_transparent=model_transparent if owned else None,
244                        mask_texture=mask_tex if owned else None,
245                    )
246                    if row == 0 and col == 0:
247                        ba.widget(edit=self._cancel_button, down_widget=btn)
248                    if row == rows - 1:
249                        bottom_row_buttons.append(btn)
250                    if not owned:
251
252                        # Ewww; buttons don't currently have alpha so in this
253                        # case we draw an image over our button with an empty
254                        # texture on it.
255                        ba.imagewidget(
256                            parent=self.root_widget,
257                            size=(scl * 260.0, scl * 130.0),
258                            position=(h - 10.0 * scl, v - 4.0 * scl),
259                            draw_controller=btn,
260                            color=(1, 1, 1),
261                            texture=ba.gettexture(tex_name),
262                            model_opaque=model_opaque,
263                            opacity=0.25,
264                            model_transparent=model_transparent,
265                            mask_texture=mask_tex,
266                        )
267
268                        ba.imagewidget(
269                            parent=self.root_widget,
270                            size=(scl * 100, scl * 100),
271                            draw_controller=btn,
272                            position=(h + scl * 70, v + scl * 10),
273                            texture=ba.gettexture('lock'),
274                        )
275
276        # Team names/colors.
277        self._custom_colors_names_button: ba.Widget | None
278        if self._sessiontype is ba.DualTeamSession:
279            y_offs = 50 if show_shuffle_check_box else 0
280            self._custom_colors_names_button = ba.buttonwidget(
281                parent=self.root_widget,
282                position=(100, 200 + y_offs),
283                size=(290, 35),
284                on_activate_call=ba.WeakCall(self._custom_colors_names_press),
285                autoselect=True,
286                textcolor=(0.8, 0.8, 0.8),
287                label=ba.Lstr(resource='teamNamesColorText'),
288            )
289            if not ba.app.accounts_v1.have_pro():
290                ba.imagewidget(
291                    parent=self.root_widget,
292                    size=(30, 30),
293                    position=(95, 202 + y_offs),
294                    texture=ba.gettexture('lock'),
295                    draw_controller=self._custom_colors_names_button,
296                )
297        else:
298            self._custom_colors_names_button = None
299
300        # Shuffle.
301        def _cb_callback(val: bool) -> None:
302            self._do_randomize_val = val
303            cfg = ba.app.config
304            cfg[
305                self._pvars.config_name + ' Playlist Randomize'
306            ] = self._do_randomize_val
307            cfg.commit()
308
309        if show_shuffle_check_box:
310            self._shuffle_check_box = ba.checkboxwidget(
311                parent=self.root_widget,
312                position=(110, 200),
313                scale=1.0,
314                size=(250, 30),
315                autoselect=True,
316                text=ba.Lstr(resource=self._r + '.shuffleGameOrderText'),
317                maxwidth=300,
318                textcolor=(0.8, 0.8, 0.8),
319                value=self._do_randomize_val,
320                on_value_change_call=_cb_callback,
321            )
322
323        # Show tutorial.
324        show_tutorial = bool(ba.app.config.get('Show Tutorial', True))
325
326        def _cb_callback_2(val: bool) -> None:
327            cfg = ba.app.config
328            cfg['Show Tutorial'] = val
329            cfg.commit()
330
331        self._show_tutorial_check_box = ba.checkboxwidget(
332            parent=self.root_widget,
333            position=(110, 151),
334            scale=1.0,
335            size=(250, 30),
336            autoselect=True,
337            text=ba.Lstr(resource=self._r + '.showTutorialText'),
338            maxwidth=300,
339            textcolor=(0.8, 0.8, 0.8),
340            value=show_tutorial,
341            on_value_change_call=_cb_callback_2,
342        )
343
344        # Grumble: current autoselect doesn't do a very good job
345        # with checkboxes.
346        if self._custom_colors_names_button is not None:
347            for btn in bottom_row_buttons:
348                ba.widget(
349                    edit=btn, down_widget=self._custom_colors_names_button
350                )
351            if show_shuffle_check_box:
352                ba.widget(
353                    edit=self._custom_colors_names_button,
354                    down_widget=self._shuffle_check_box,
355                )
356                ba.widget(
357                    edit=self._shuffle_check_box,
358                    up_widget=self._custom_colors_names_button,
359                )
360            else:
361                ba.widget(
362                    edit=self._custom_colors_names_button,
363                    down_widget=self._show_tutorial_check_box,
364                )
365                ba.widget(
366                    edit=self._show_tutorial_check_box,
367                    up_widget=self._custom_colors_names_button,
368                )
369
370        self._ok_button = ba.buttonwidget(
371            parent=self.root_widget,
372            position=(70, 44),
373            size=(200, 45),
374            scale=1.8,
375            text_res_scale=1.5,
376            on_activate_call=self._on_ok_press,
377            autoselect=True,
378            label=ba.Lstr(
379                resource='okText' if self._selecting_mode else 'playText'
380            ),
381        )
382
383        ba.widget(edit=self._ok_button, up_widget=self._show_tutorial_check_box)
384
385        ba.containerwidget(
386            edit=self.root_widget,
387            start_button=self._ok_button,
388            cancel_button=self._cancel_button,
389            selected_child=self._ok_button,
390        )
391
392        # Update now and once per second.
393        self._update_timer = ba.Timer(
394            1.0,
395            ba.WeakCall(self._update),
396            timetype=ba.TimeType.REAL,
397            repeat=True,
398        )
399        self._update()
400
401    def _custom_colors_names_press(self) -> None:
402        from bastd.ui.account import show_sign_in_prompt
403        from bastd.ui.teamnamescolors import TeamNamesColorsWindow
404        from bastd.ui.purchase import PurchaseWindow
405
406        if not ba.app.accounts_v1.have_pro():
407            if ba.internal.get_v1_account_state() != 'signed_in':
408                show_sign_in_prompt()
409            else:
410                PurchaseWindow(items=['pro'])
411            self._transition_out()
412            return
413        assert self._custom_colors_names_button
414        TeamNamesColorsWindow(
415            scale_origin=(
416                self._custom_colors_names_button.get_screen_space_center()
417            )
418        )
419
420    def _does_target_playlist_exist(self) -> bool:
421        if self._playlist == '__default__':
422            return True
423        return self._playlist in ba.app.config.get(
424            self._pvars.config_name + ' Playlists', {}
425        )
426
427    def _update(self) -> None:
428        # All we do here is make sure our targeted playlist still exists,
429        # and close ourself if not.
430        if not self._does_target_playlist_exist():
431            self._transition_out()
432
433    def _transition_out(self, transition: str = 'out_scale') -> None:
434        if not self._transitioning_out:
435            self._transitioning_out = True
436            ba.containerwidget(edit=self.root_widget, transition=transition)
437
438    def on_popup_cancel(self) -> None:
439        ba.playsound(ba.getsound('swish'))
440        self._transition_out()
441
442    def _on_cancel_press(self) -> None:
443        self._transition_out()
444
445    def _on_ok_press(self) -> None:
446
447        # Disallow if our playlist has disappeared.
448        if not self._does_target_playlist_exist():
449            return
450
451        # Disallow if we have no unlocked games.
452        if not self._have_at_least_one_owned:
453            ba.playsound(ba.getsound('error'))
454            ba.screenmessage(
455                ba.Lstr(resource='playlistNoValidGamesErrorText'),
456                color=(1, 0, 0),
457            )
458            return
459
460        cfg = ba.app.config
461        cfg[self._pvars.config_name + ' Playlist Selection'] = self._playlist
462
463        # Head back to the gather window in playlist-select mode
464        # or start the game in regular mode.
465        if self._selecting_mode:
466            from bastd.ui.gather import GatherWindow
467
468            if self._sessiontype is ba.FreeForAllSession:
469                typename = 'ffa'
470            elif self._sessiontype is ba.DualTeamSession:
471                typename = 'teams'
472            else:
473                raise RuntimeError('Only teams and ffa currently supported')
474            cfg['Private Party Host Session Type'] = typename
475            ba.playsound(ba.getsound('gunCocking'))
476            ba.app.ui.set_main_menu_window(
477                GatherWindow(transition='in_right').get_root_widget()
478            )
479            self._transition_out(transition='out_left')
480            if self._delegate is not None:
481                self._delegate.on_play_options_window_run_game()
482        else:
483            ba.internal.fade_screen(False, endcall=self._run_selected_playlist)
484            ba.internal.lock_all_input()
485            self._transition_out(transition='out_left')
486            if self._delegate is not None:
487                self._delegate.on_play_options_window_run_game()
488
489        cfg.commit()
490
491    def _run_selected_playlist(self) -> None:
492        ba.internal.unlock_all_input()
493        try:
494            ba.internal.new_host_session(self._sessiontype)
495        except Exception:
496            from bastd import mainmenu
497
498            ba.print_exception('exception running session', self._sessiontype)
499
500            # Drop back into a main menu session.
501            ba.internal.new_host_session(mainmenu.MainMenuSession)

A popup window for configuring play options.

PlayOptionsWindow( sessiontype: type[ba._session.Session], playlist: str, scale_origin: tuple[float, float], delegate: Any = None)
 21    def __init__(
 22        self,
 23        sessiontype: type[ba.Session],
 24        playlist: str,
 25        scale_origin: tuple[float, float],
 26        delegate: Any = None,
 27    ):
 28        # FIXME: Tidy this up.
 29        # pylint: disable=too-many-branches
 30        # pylint: disable=too-many-statements
 31        # pylint: disable=too-many-locals
 32        from ba.internal import get_map_class, getclass, filter_playlist
 33        from bastd.ui.playlist import PlaylistTypeVars
 34
 35        self._r = 'gameListWindow'
 36        self._delegate = delegate
 37        self._pvars = PlaylistTypeVars(sessiontype)
 38        self._transitioning_out = False
 39
 40        # We behave differently if we're being used for playlist selection
 41        # vs starting a game directly (should make this more elegant).
 42        self._selecting_mode = ba.app.ui.selecting_private_party_playlist
 43
 44        self._do_randomize_val = ba.app.config.get(
 45            self._pvars.config_name + ' Playlist Randomize', 0
 46        )
 47
 48        self._sessiontype = sessiontype
 49        self._playlist = playlist
 50
 51        self._width = 500.0
 52        self._height = 330.0 - 50.0
 53
 54        # In teams games, show the custom names/colors button.
 55        if self._sessiontype is ba.DualTeamSession:
 56            self._height += 50.0
 57
 58        self._row_height = 45.0
 59
 60        # Grab our maps to display.
 61        model_opaque = ba.getmodel('level_select_button_opaque')
 62        model_transparent = ba.getmodel('level_select_button_transparent')
 63        mask_tex = ba.gettexture('mapPreviewMask')
 64
 65        # Poke into this playlist and see if we can display some of its maps.
 66        map_textures = []
 67        map_texture_entries = []
 68        rows = 0
 69        columns = 0
 70        game_count = 0
 71        scl = 0.35
 72        c_width_total = 0.0
 73        try:
 74            max_columns = 5
 75            name = playlist
 76            if name == '__default__':
 77                plst = self._pvars.get_default_list_call()
 78            else:
 79                try:
 80                    plst = ba.app.config[
 81                        self._pvars.config_name + ' Playlists'
 82                    ][name]
 83                except Exception:
 84                    print(
 85                        'ERROR INFO: self._config_name is:',
 86                        self._pvars.config_name,
 87                    )
 88                    print(
 89                        'ERROR INFO: playlist names are:',
 90                        list(
 91                            ba.app.config[
 92                                self._pvars.config_name + ' Playlists'
 93                            ].keys()
 94                        ),
 95                    )
 96                    raise
 97            plst = filter_playlist(
 98                plst,
 99                self._sessiontype,
100                remove_unowned=False,
101                mark_unowned=True,
102                name=name,
103            )
104            game_count = len(plst)
105            for entry in plst:
106                mapname = entry['settings']['map']
107                maptype: type[ba.Map] | None
108                try:
109                    maptype = get_map_class(mapname)
110                except ba.NotFoundError:
111                    maptype = None
112                if maptype is not None:
113                    tex_name = maptype.get_preview_texture_name()
114                    if tex_name is not None:
115                        map_textures.append(tex_name)
116                        map_texture_entries.append(entry)
117            rows = (max(0, len(map_textures) - 1) // max_columns) + 1
118            columns = min(max_columns, len(map_textures))
119
120            if len(map_textures) == 1:
121                scl = 1.1
122            elif len(map_textures) == 2:
123                scl = 0.7
124            elif len(map_textures) == 3:
125                scl = 0.55
126            else:
127                scl = 0.35
128            self._row_height = 128.0 * scl
129            c_width_total = scl * 250.0 * columns
130            if map_textures:
131                self._height += self._row_height * rows
132
133        except Exception:
134            ba.print_exception('Error listing playlist maps.')
135
136        show_shuffle_check_box = game_count > 1
137
138        if show_shuffle_check_box:
139            self._height += 40
140
141        # Creates our _root_widget.
142        uiscale = ba.app.ui.uiscale
143        scale = (
144            1.69
145            if uiscale is ba.UIScale.SMALL
146            else 1.1
147            if uiscale is ba.UIScale.MEDIUM
148            else 0.85
149        )
150        super().__init__(
151            position=scale_origin, size=(self._width, self._height), scale=scale
152        )
153
154        playlist_name: str | ba.Lstr = (
155            self._pvars.default_list_name
156            if playlist == '__default__'
157            else playlist
158        )
159        self._title_text = ba.textwidget(
160            parent=self.root_widget,
161            position=(self._width * 0.5, self._height - 89 + 51),
162            size=(0, 0),
163            text=playlist_name,
164            scale=1.4,
165            color=(1, 1, 1),
166            maxwidth=self._width * 0.7,
167            h_align='center',
168            v_align='center',
169        )
170
171        self._cancel_button = ba.buttonwidget(
172            parent=self.root_widget,
173            position=(25, self._height - 53),
174            size=(50, 50),
175            scale=0.7,
176            label='',
177            color=(0.42, 0.73, 0.2),
178            on_activate_call=self._on_cancel_press,
179            autoselect=True,
180            icon=ba.gettexture('crossOut'),
181            iconscale=1.2,
182        )
183
184        h_offs_img = self._width * 0.5 - c_width_total * 0.5
185        v_offs_img = self._height - 118 - scl * 125.0 + 50
186        bottom_row_buttons = []
187        self._have_at_least_one_owned = False
188
189        for row in range(rows):
190            for col in range(columns):
191                tex_index = row * columns + col
192                if tex_index < len(map_textures):
193                    tex_name = map_textures[tex_index]
194                    h = h_offs_img + scl * 250 * col
195                    v = v_offs_img - self._row_height * row
196                    entry = map_texture_entries[tex_index]
197                    owned = not (
198                        ('is_unowned_map' in entry and entry['is_unowned_map'])
199                        or (
200                            'is_unowned_game' in entry
201                            and entry['is_unowned_game']
202                        )
203                    )
204
205                    if owned:
206                        self._have_at_least_one_owned = True
207
208                    try:
209                        desc = getclass(
210                            entry['type'], subclassof=ba.GameActivity
211                        ).get_settings_display_string(entry)
212                        if not owned:
213                            desc = ba.Lstr(
214                                value='${DESC}\n${UNLOCK}',
215                                subs=[
216                                    ('${DESC}', desc),
217                                    (
218                                        '${UNLOCK}',
219                                        ba.Lstr(
220                                            resource='unlockThisInTheStoreText'
221                                        ),
222                                    ),
223                                ],
224                            )
225                        desc_color = (0, 1, 0) if owned else (1, 0, 0)
226                    except Exception:
227                        desc = ba.Lstr(value='(invalid)')
228                        desc_color = (1, 0, 0)
229
230                    btn = ba.buttonwidget(
231                        parent=self.root_widget,
232                        size=(scl * 240.0, scl * 120.0),
233                        position=(h, v),
234                        texture=ba.gettexture(tex_name if owned else 'empty'),
235                        model_opaque=model_opaque if owned else None,
236                        on_activate_call=ba.Call(
237                            ba.screenmessage, desc, desc_color
238                        ),
239                        label='',
240                        color=(1, 1, 1),
241                        autoselect=True,
242                        extra_touch_border_scale=0.0,
243                        model_transparent=model_transparent if owned else None,
244                        mask_texture=mask_tex if owned else None,
245                    )
246                    if row == 0 and col == 0:
247                        ba.widget(edit=self._cancel_button, down_widget=btn)
248                    if row == rows - 1:
249                        bottom_row_buttons.append(btn)
250                    if not owned:
251
252                        # Ewww; buttons don't currently have alpha so in this
253                        # case we draw an image over our button with an empty
254                        # texture on it.
255                        ba.imagewidget(
256                            parent=self.root_widget,
257                            size=(scl * 260.0, scl * 130.0),
258                            position=(h - 10.0 * scl, v - 4.0 * scl),
259                            draw_controller=btn,
260                            color=(1, 1, 1),
261                            texture=ba.gettexture(tex_name),
262                            model_opaque=model_opaque,
263                            opacity=0.25,
264                            model_transparent=model_transparent,
265                            mask_texture=mask_tex,
266                        )
267
268                        ba.imagewidget(
269                            parent=self.root_widget,
270                            size=(scl * 100, scl * 100),
271                            draw_controller=btn,
272                            position=(h + scl * 70, v + scl * 10),
273                            texture=ba.gettexture('lock'),
274                        )
275
276        # Team names/colors.
277        self._custom_colors_names_button: ba.Widget | None
278        if self._sessiontype is ba.DualTeamSession:
279            y_offs = 50 if show_shuffle_check_box else 0
280            self._custom_colors_names_button = ba.buttonwidget(
281                parent=self.root_widget,
282                position=(100, 200 + y_offs),
283                size=(290, 35),
284                on_activate_call=ba.WeakCall(self._custom_colors_names_press),
285                autoselect=True,
286                textcolor=(0.8, 0.8, 0.8),
287                label=ba.Lstr(resource='teamNamesColorText'),
288            )
289            if not ba.app.accounts_v1.have_pro():
290                ba.imagewidget(
291                    parent=self.root_widget,
292                    size=(30, 30),
293                    position=(95, 202 + y_offs),
294                    texture=ba.gettexture('lock'),
295                    draw_controller=self._custom_colors_names_button,
296                )
297        else:
298            self._custom_colors_names_button = None
299
300        # Shuffle.
301        def _cb_callback(val: bool) -> None:
302            self._do_randomize_val = val
303            cfg = ba.app.config
304            cfg[
305                self._pvars.config_name + ' Playlist Randomize'
306            ] = self._do_randomize_val
307            cfg.commit()
308
309        if show_shuffle_check_box:
310            self._shuffle_check_box = ba.checkboxwidget(
311                parent=self.root_widget,
312                position=(110, 200),
313                scale=1.0,
314                size=(250, 30),
315                autoselect=True,
316                text=ba.Lstr(resource=self._r + '.shuffleGameOrderText'),
317                maxwidth=300,
318                textcolor=(0.8, 0.8, 0.8),
319                value=self._do_randomize_val,
320                on_value_change_call=_cb_callback,
321            )
322
323        # Show tutorial.
324        show_tutorial = bool(ba.app.config.get('Show Tutorial', True))
325
326        def _cb_callback_2(val: bool) -> None:
327            cfg = ba.app.config
328            cfg['Show Tutorial'] = val
329            cfg.commit()
330
331        self._show_tutorial_check_box = ba.checkboxwidget(
332            parent=self.root_widget,
333            position=(110, 151),
334            scale=1.0,
335            size=(250, 30),
336            autoselect=True,
337            text=ba.Lstr(resource=self._r + '.showTutorialText'),
338            maxwidth=300,
339            textcolor=(0.8, 0.8, 0.8),
340            value=show_tutorial,
341            on_value_change_call=_cb_callback_2,
342        )
343
344        # Grumble: current autoselect doesn't do a very good job
345        # with checkboxes.
346        if self._custom_colors_names_button is not None:
347            for btn in bottom_row_buttons:
348                ba.widget(
349                    edit=btn, down_widget=self._custom_colors_names_button
350                )
351            if show_shuffle_check_box:
352                ba.widget(
353                    edit=self._custom_colors_names_button,
354                    down_widget=self._shuffle_check_box,
355                )
356                ba.widget(
357                    edit=self._shuffle_check_box,
358                    up_widget=self._custom_colors_names_button,
359                )
360            else:
361                ba.widget(
362                    edit=self._custom_colors_names_button,
363                    down_widget=self._show_tutorial_check_box,
364                )
365                ba.widget(
366                    edit=self._show_tutorial_check_box,
367                    up_widget=self._custom_colors_names_button,
368                )
369
370        self._ok_button = ba.buttonwidget(
371            parent=self.root_widget,
372            position=(70, 44),
373            size=(200, 45),
374            scale=1.8,
375            text_res_scale=1.5,
376            on_activate_call=self._on_ok_press,
377            autoselect=True,
378            label=ba.Lstr(
379                resource='okText' if self._selecting_mode else 'playText'
380            ),
381        )
382
383        ba.widget(edit=self._ok_button, up_widget=self._show_tutorial_check_box)
384
385        ba.containerwidget(
386            edit=self.root_widget,
387            start_button=self._ok_button,
388            cancel_button=self._cancel_button,
389            selected_child=self._ok_button,
390        )
391
392        # Update now and once per second.
393        self._update_timer = ba.Timer(
394            1.0,
395            ba.WeakCall(self._update),
396            timetype=ba.TimeType.REAL,
397            repeat=True,
398        )
399        self._update()
def on_popup_cancel(self) -> None:
438    def on_popup_cancel(self) -> None:
439        ba.playsound(ba.getsound('swish'))
440        self._transition_out()

Called when the popup is canceled.

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