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

Called when the popup is canceled.

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