bastd.ui.playlist.mapselect

Provides UI for selecting maps in playlists.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides UI for selecting maps in playlists."""
  4
  5from __future__ import annotations
  6
  7import math
  8from typing import TYPE_CHECKING
  9
 10import ba
 11import ba.internal
 12
 13if TYPE_CHECKING:
 14    from typing import Any, Callable
 15
 16
 17class PlaylistMapSelectWindow(ba.Window):
 18    """Window to select a map."""
 19
 20    def __init__(
 21        self,
 22        gametype: type[ba.GameActivity],
 23        sessiontype: type[ba.Session],
 24        config: dict[str, Any],
 25        edit_info: dict[str, Any],
 26        completion_call: Callable[[dict[str, Any] | None], Any],
 27        transition: str = 'in_right',
 28    ):
 29        from ba.internal import get_filtered_map_name
 30
 31        self._gametype = gametype
 32        self._sessiontype = sessiontype
 33        self._config = config
 34        self._completion_call = completion_call
 35        self._edit_info = edit_info
 36        self._maps: list[tuple[str, ba.Texture]] = []
 37        try:
 38            self._previous_map = get_filtered_map_name(
 39                config['settings']['map']
 40            )
 41        except Exception:
 42            self._previous_map = ''
 43
 44        uiscale = ba.app.ui.uiscale
 45        width = 715 if uiscale is ba.UIScale.SMALL else 615
 46        x_inset = 50 if uiscale is ba.UIScale.SMALL else 0
 47        height = (
 48            400
 49            if uiscale is ba.UIScale.SMALL
 50            else 480
 51            if uiscale is ba.UIScale.MEDIUM
 52            else 600
 53        )
 54
 55        top_extra = 20 if uiscale is ba.UIScale.SMALL else 0
 56        super().__init__(
 57            root_widget=ba.containerwidget(
 58                size=(width, height + top_extra),
 59                transition=transition,
 60                scale=(
 61                    2.17
 62                    if uiscale is ba.UIScale.SMALL
 63                    else 1.3
 64                    if uiscale is ba.UIScale.MEDIUM
 65                    else 1.0
 66                ),
 67                stack_offset=(0, -27)
 68                if uiscale is ba.UIScale.SMALL
 69                else (0, 0),
 70            )
 71        )
 72
 73        self._cancel_button = btn = ba.buttonwidget(
 74            parent=self._root_widget,
 75            position=(38 + x_inset, height - 67),
 76            size=(140, 50),
 77            scale=0.9,
 78            text_scale=1.0,
 79            autoselect=True,
 80            label=ba.Lstr(resource='cancelText'),
 81            on_activate_call=self._cancel,
 82        )
 83
 84        ba.containerwidget(edit=self._root_widget, cancel_button=btn)
 85        ba.textwidget(
 86            parent=self._root_widget,
 87            position=(width * 0.5, height - 46),
 88            size=(0, 0),
 89            maxwidth=260,
 90            scale=1.1,
 91            text=ba.Lstr(
 92                resource='mapSelectTitleText',
 93                subs=[('${GAME}', self._gametype.get_display_string())],
 94            ),
 95            color=ba.app.ui.title_color,
 96            h_align='center',
 97            v_align='center',
 98        )
 99        v = height - 70
100        self._scroll_width = width - (80 + 2 * x_inset)
101        self._scroll_height = height - 140
102
103        self._scrollwidget = ba.scrollwidget(
104            parent=self._root_widget,
105            position=(40 + x_inset, v - self._scroll_height),
106            size=(self._scroll_width, self._scroll_height),
107        )
108        ba.containerwidget(
109            edit=self._root_widget, selected_child=self._scrollwidget
110        )
111        ba.containerwidget(edit=self._scrollwidget, claims_left_right=True)
112
113        self._subcontainer: ba.Widget | None = None
114        self._refresh()
115
116    def _refresh(self, select_get_more_maps_button: bool = False) -> None:
117        # pylint: disable=too-many-statements
118        # pylint: disable=too-many-branches
119        # pylint: disable=too-many-locals
120        from ba.internal import (
121            get_unowned_maps,
122            get_map_class,
123            get_map_display_string,
124        )
125
126        # Kill old.
127        if self._subcontainer is not None:
128            self._subcontainer.delete()
129
130        model_opaque = ba.getmodel('level_select_button_opaque')
131        model_transparent = ba.getmodel('level_select_button_transparent')
132
133        self._maps = []
134        map_list = self._gametype.get_supported_maps(self._sessiontype)
135        map_list_sorted = list(map_list)
136        map_list_sorted.sort()
137        unowned_maps = get_unowned_maps()
138
139        for mapname in map_list_sorted:
140
141            # Disallow ones we don't own.
142            if mapname in unowned_maps:
143                continue
144            map_tex_name = get_map_class(mapname).get_preview_texture_name()
145            if map_tex_name is not None:
146                try:
147                    map_tex = ba.gettexture(map_tex_name)
148                    self._maps.append((mapname, map_tex))
149                except Exception:
150                    print(f'Invalid map preview texture: "{map_tex_name}".')
151            else:
152                print('Error: no map preview texture for map:', mapname)
153
154        count = len(self._maps)
155        columns = 2
156        rows = int(math.ceil(float(count) / columns))
157        button_width = 220
158        button_height = button_width * 0.5
159        button_buffer_h = 16
160        button_buffer_v = 19
161        self._sub_width = self._scroll_width * 0.95
162        self._sub_height = (
163            5 + rows * (button_height + 2 * button_buffer_v) + 100
164        )
165        self._subcontainer = ba.containerwidget(
166            parent=self._scrollwidget,
167            size=(self._sub_width, self._sub_height),
168            background=False,
169        )
170        index = 0
171        mask_texture = ba.gettexture('mapPreviewMask')
172        h_offs = 130 if len(self._maps) == 1 else 0
173        for y in range(rows):
174            for x in range(columns):
175                pos = (
176                    x * (button_width + 2 * button_buffer_h)
177                    + button_buffer_h
178                    + h_offs,
179                    self._sub_height
180                    - (y + 1) * (button_height + 2 * button_buffer_v)
181                    + 12,
182                )
183                btn = ba.buttonwidget(
184                    parent=self._subcontainer,
185                    button_type='square',
186                    size=(button_width, button_height),
187                    autoselect=True,
188                    texture=self._maps[index][1],
189                    mask_texture=mask_texture,
190                    model_opaque=model_opaque,
191                    model_transparent=model_transparent,
192                    label='',
193                    color=(1, 1, 1),
194                    on_activate_call=ba.Call(
195                        self._select_with_delay, self._maps[index][0]
196                    ),
197                    position=pos,
198                )
199                if x == 0:
200                    ba.widget(edit=btn, left_widget=self._cancel_button)
201                if y == 0:
202                    ba.widget(edit=btn, up_widget=self._cancel_button)
203                if x == columns - 1 and ba.app.ui.use_toolbars:
204                    ba.widget(
205                        edit=btn,
206                        right_widget=ba.internal.get_special_widget(
207                            'party_button'
208                        ),
209                    )
210
211                ba.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60)
212                if self._maps[index][0] == self._previous_map:
213                    ba.containerwidget(
214                        edit=self._subcontainer,
215                        selected_child=btn,
216                        visible_child=btn,
217                    )
218                name = get_map_display_string(self._maps[index][0])
219                ba.textwidget(
220                    parent=self._subcontainer,
221                    text=name,
222                    position=(pos[0] + button_width * 0.5, pos[1] - 12),
223                    size=(0, 0),
224                    scale=0.5,
225                    maxwidth=button_width,
226                    draw_controller=btn,
227                    h_align='center',
228                    v_align='center',
229                    color=(0.8, 0.8, 0.8, 0.8),
230                )
231                index += 1
232
233                if index >= count:
234                    break
235            if index >= count:
236                break
237        self._get_more_maps_button = btn = ba.buttonwidget(
238            parent=self._subcontainer,
239            size=(self._sub_width * 0.8, 60),
240            position=(self._sub_width * 0.1, 30),
241            label=ba.Lstr(resource='mapSelectGetMoreMapsText'),
242            on_activate_call=self._on_store_press,
243            color=(0.6, 0.53, 0.63),
244            textcolor=(0.75, 0.7, 0.8),
245            autoselect=True,
246        )
247        ba.widget(edit=btn, show_buffer_top=30, show_buffer_bottom=30)
248        if select_get_more_maps_button:
249            ba.containerwidget(
250                edit=self._subcontainer, selected_child=btn, visible_child=btn
251            )
252
253    def _on_store_press(self) -> None:
254        from bastd.ui import account
255        from bastd.ui.store.browser import StoreBrowserWindow
256
257        if ba.internal.get_v1_account_state() != 'signed_in':
258            account.show_sign_in_prompt()
259            return
260        StoreBrowserWindow(
261            modal=True,
262            show_tab=StoreBrowserWindow.TabID.MAPS,
263            on_close_call=self._on_store_close,
264            origin_widget=self._get_more_maps_button,
265        )
266
267    def _on_store_close(self) -> None:
268        self._refresh(select_get_more_maps_button=True)
269
270    def _select(self, map_name: str) -> None:
271        from bastd.ui.playlist.editgame import PlaylistEditGameWindow
272
273        self._config['settings']['map'] = map_name
274        ba.containerwidget(edit=self._root_widget, transition='out_right')
275        ba.app.ui.set_main_menu_window(
276            PlaylistEditGameWindow(
277                self._gametype,
278                self._sessiontype,
279                self._config,
280                self._completion_call,
281                default_selection='map',
282                transition='in_left',
283                edit_info=self._edit_info,
284            ).get_root_widget()
285        )
286
287    def _select_with_delay(self, map_name: str) -> None:
288        ba.internal.lock_all_input()
289        ba.timer(0.1, ba.internal.unlock_all_input, timetype=ba.TimeType.REAL)
290        ba.timer(
291            0.1, ba.WeakCall(self._select, map_name), timetype=ba.TimeType.REAL
292        )
293
294    def _cancel(self) -> None:
295        from bastd.ui.playlist.editgame import PlaylistEditGameWindow
296
297        ba.containerwidget(edit=self._root_widget, transition='out_right')
298        ba.app.ui.set_main_menu_window(
299            PlaylistEditGameWindow(
300                self._gametype,
301                self._sessiontype,
302                self._config,
303                self._completion_call,
304                default_selection='map',
305                transition='in_left',
306                edit_info=self._edit_info,
307            ).get_root_widget()
308        )
class PlaylistMapSelectWindow(ba.ui.Window):
 18class PlaylistMapSelectWindow(ba.Window):
 19    """Window to select a map."""
 20
 21    def __init__(
 22        self,
 23        gametype: type[ba.GameActivity],
 24        sessiontype: type[ba.Session],
 25        config: dict[str, Any],
 26        edit_info: dict[str, Any],
 27        completion_call: Callable[[dict[str, Any] | None], Any],
 28        transition: str = 'in_right',
 29    ):
 30        from ba.internal import get_filtered_map_name
 31
 32        self._gametype = gametype
 33        self._sessiontype = sessiontype
 34        self._config = config
 35        self._completion_call = completion_call
 36        self._edit_info = edit_info
 37        self._maps: list[tuple[str, ba.Texture]] = []
 38        try:
 39            self._previous_map = get_filtered_map_name(
 40                config['settings']['map']
 41            )
 42        except Exception:
 43            self._previous_map = ''
 44
 45        uiscale = ba.app.ui.uiscale
 46        width = 715 if uiscale is ba.UIScale.SMALL else 615
 47        x_inset = 50 if uiscale is ba.UIScale.SMALL else 0
 48        height = (
 49            400
 50            if uiscale is ba.UIScale.SMALL
 51            else 480
 52            if uiscale is ba.UIScale.MEDIUM
 53            else 600
 54        )
 55
 56        top_extra = 20 if uiscale is ba.UIScale.SMALL else 0
 57        super().__init__(
 58            root_widget=ba.containerwidget(
 59                size=(width, height + top_extra),
 60                transition=transition,
 61                scale=(
 62                    2.17
 63                    if uiscale is ba.UIScale.SMALL
 64                    else 1.3
 65                    if uiscale is ba.UIScale.MEDIUM
 66                    else 1.0
 67                ),
 68                stack_offset=(0, -27)
 69                if uiscale is ba.UIScale.SMALL
 70                else (0, 0),
 71            )
 72        )
 73
 74        self._cancel_button = btn = ba.buttonwidget(
 75            parent=self._root_widget,
 76            position=(38 + x_inset, height - 67),
 77            size=(140, 50),
 78            scale=0.9,
 79            text_scale=1.0,
 80            autoselect=True,
 81            label=ba.Lstr(resource='cancelText'),
 82            on_activate_call=self._cancel,
 83        )
 84
 85        ba.containerwidget(edit=self._root_widget, cancel_button=btn)
 86        ba.textwidget(
 87            parent=self._root_widget,
 88            position=(width * 0.5, height - 46),
 89            size=(0, 0),
 90            maxwidth=260,
 91            scale=1.1,
 92            text=ba.Lstr(
 93                resource='mapSelectTitleText',
 94                subs=[('${GAME}', self._gametype.get_display_string())],
 95            ),
 96            color=ba.app.ui.title_color,
 97            h_align='center',
 98            v_align='center',
 99        )
100        v = height - 70
101        self._scroll_width = width - (80 + 2 * x_inset)
102        self._scroll_height = height - 140
103
104        self._scrollwidget = ba.scrollwidget(
105            parent=self._root_widget,
106            position=(40 + x_inset, v - self._scroll_height),
107            size=(self._scroll_width, self._scroll_height),
108        )
109        ba.containerwidget(
110            edit=self._root_widget, selected_child=self._scrollwidget
111        )
112        ba.containerwidget(edit=self._scrollwidget, claims_left_right=True)
113
114        self._subcontainer: ba.Widget | None = None
115        self._refresh()
116
117    def _refresh(self, select_get_more_maps_button: bool = False) -> None:
118        # pylint: disable=too-many-statements
119        # pylint: disable=too-many-branches
120        # pylint: disable=too-many-locals
121        from ba.internal import (
122            get_unowned_maps,
123            get_map_class,
124            get_map_display_string,
125        )
126
127        # Kill old.
128        if self._subcontainer is not None:
129            self._subcontainer.delete()
130
131        model_opaque = ba.getmodel('level_select_button_opaque')
132        model_transparent = ba.getmodel('level_select_button_transparent')
133
134        self._maps = []
135        map_list = self._gametype.get_supported_maps(self._sessiontype)
136        map_list_sorted = list(map_list)
137        map_list_sorted.sort()
138        unowned_maps = get_unowned_maps()
139
140        for mapname in map_list_sorted:
141
142            # Disallow ones we don't own.
143            if mapname in unowned_maps:
144                continue
145            map_tex_name = get_map_class(mapname).get_preview_texture_name()
146            if map_tex_name is not None:
147                try:
148                    map_tex = ba.gettexture(map_tex_name)
149                    self._maps.append((mapname, map_tex))
150                except Exception:
151                    print(f'Invalid map preview texture: "{map_tex_name}".')
152            else:
153                print('Error: no map preview texture for map:', mapname)
154
155        count = len(self._maps)
156        columns = 2
157        rows = int(math.ceil(float(count) / columns))
158        button_width = 220
159        button_height = button_width * 0.5
160        button_buffer_h = 16
161        button_buffer_v = 19
162        self._sub_width = self._scroll_width * 0.95
163        self._sub_height = (
164            5 + rows * (button_height + 2 * button_buffer_v) + 100
165        )
166        self._subcontainer = ba.containerwidget(
167            parent=self._scrollwidget,
168            size=(self._sub_width, self._sub_height),
169            background=False,
170        )
171        index = 0
172        mask_texture = ba.gettexture('mapPreviewMask')
173        h_offs = 130 if len(self._maps) == 1 else 0
174        for y in range(rows):
175            for x in range(columns):
176                pos = (
177                    x * (button_width + 2 * button_buffer_h)
178                    + button_buffer_h
179                    + h_offs,
180                    self._sub_height
181                    - (y + 1) * (button_height + 2 * button_buffer_v)
182                    + 12,
183                )
184                btn = ba.buttonwidget(
185                    parent=self._subcontainer,
186                    button_type='square',
187                    size=(button_width, button_height),
188                    autoselect=True,
189                    texture=self._maps[index][1],
190                    mask_texture=mask_texture,
191                    model_opaque=model_opaque,
192                    model_transparent=model_transparent,
193                    label='',
194                    color=(1, 1, 1),
195                    on_activate_call=ba.Call(
196                        self._select_with_delay, self._maps[index][0]
197                    ),
198                    position=pos,
199                )
200                if x == 0:
201                    ba.widget(edit=btn, left_widget=self._cancel_button)
202                if y == 0:
203                    ba.widget(edit=btn, up_widget=self._cancel_button)
204                if x == columns - 1 and ba.app.ui.use_toolbars:
205                    ba.widget(
206                        edit=btn,
207                        right_widget=ba.internal.get_special_widget(
208                            'party_button'
209                        ),
210                    )
211
212                ba.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60)
213                if self._maps[index][0] == self._previous_map:
214                    ba.containerwidget(
215                        edit=self._subcontainer,
216                        selected_child=btn,
217                        visible_child=btn,
218                    )
219                name = get_map_display_string(self._maps[index][0])
220                ba.textwidget(
221                    parent=self._subcontainer,
222                    text=name,
223                    position=(pos[0] + button_width * 0.5, pos[1] - 12),
224                    size=(0, 0),
225                    scale=0.5,
226                    maxwidth=button_width,
227                    draw_controller=btn,
228                    h_align='center',
229                    v_align='center',
230                    color=(0.8, 0.8, 0.8, 0.8),
231                )
232                index += 1
233
234                if index >= count:
235                    break
236            if index >= count:
237                break
238        self._get_more_maps_button = btn = ba.buttonwidget(
239            parent=self._subcontainer,
240            size=(self._sub_width * 0.8, 60),
241            position=(self._sub_width * 0.1, 30),
242            label=ba.Lstr(resource='mapSelectGetMoreMapsText'),
243            on_activate_call=self._on_store_press,
244            color=(0.6, 0.53, 0.63),
245            textcolor=(0.75, 0.7, 0.8),
246            autoselect=True,
247        )
248        ba.widget(edit=btn, show_buffer_top=30, show_buffer_bottom=30)
249        if select_get_more_maps_button:
250            ba.containerwidget(
251                edit=self._subcontainer, selected_child=btn, visible_child=btn
252            )
253
254    def _on_store_press(self) -> None:
255        from bastd.ui import account
256        from bastd.ui.store.browser import StoreBrowserWindow
257
258        if ba.internal.get_v1_account_state() != 'signed_in':
259            account.show_sign_in_prompt()
260            return
261        StoreBrowserWindow(
262            modal=True,
263            show_tab=StoreBrowserWindow.TabID.MAPS,
264            on_close_call=self._on_store_close,
265            origin_widget=self._get_more_maps_button,
266        )
267
268    def _on_store_close(self) -> None:
269        self._refresh(select_get_more_maps_button=True)
270
271    def _select(self, map_name: str) -> None:
272        from bastd.ui.playlist.editgame import PlaylistEditGameWindow
273
274        self._config['settings']['map'] = map_name
275        ba.containerwidget(edit=self._root_widget, transition='out_right')
276        ba.app.ui.set_main_menu_window(
277            PlaylistEditGameWindow(
278                self._gametype,
279                self._sessiontype,
280                self._config,
281                self._completion_call,
282                default_selection='map',
283                transition='in_left',
284                edit_info=self._edit_info,
285            ).get_root_widget()
286        )
287
288    def _select_with_delay(self, map_name: str) -> None:
289        ba.internal.lock_all_input()
290        ba.timer(0.1, ba.internal.unlock_all_input, timetype=ba.TimeType.REAL)
291        ba.timer(
292            0.1, ba.WeakCall(self._select, map_name), timetype=ba.TimeType.REAL
293        )
294
295    def _cancel(self) -> None:
296        from bastd.ui.playlist.editgame import PlaylistEditGameWindow
297
298        ba.containerwidget(edit=self._root_widget, transition='out_right')
299        ba.app.ui.set_main_menu_window(
300            PlaylistEditGameWindow(
301                self._gametype,
302                self._sessiontype,
303                self._config,
304                self._completion_call,
305                default_selection='map',
306                transition='in_left',
307                edit_info=self._edit_info,
308            ).get_root_widget()
309        )

Window to select a map.

PlaylistMapSelectWindow( gametype: type[ba._gameactivity.GameActivity], sessiontype: type[ba._session.Session], config: dict[str, typing.Any], edit_info: dict[str, typing.Any], completion_call: Callable[[dict[str, Any] | None], Any], transition: str = 'in_right')
 21    def __init__(
 22        self,
 23        gametype: type[ba.GameActivity],
 24        sessiontype: type[ba.Session],
 25        config: dict[str, Any],
 26        edit_info: dict[str, Any],
 27        completion_call: Callable[[dict[str, Any] | None], Any],
 28        transition: str = 'in_right',
 29    ):
 30        from ba.internal import get_filtered_map_name
 31
 32        self._gametype = gametype
 33        self._sessiontype = sessiontype
 34        self._config = config
 35        self._completion_call = completion_call
 36        self._edit_info = edit_info
 37        self._maps: list[tuple[str, ba.Texture]] = []
 38        try:
 39            self._previous_map = get_filtered_map_name(
 40                config['settings']['map']
 41            )
 42        except Exception:
 43            self._previous_map = ''
 44
 45        uiscale = ba.app.ui.uiscale
 46        width = 715 if uiscale is ba.UIScale.SMALL else 615
 47        x_inset = 50 if uiscale is ba.UIScale.SMALL else 0
 48        height = (
 49            400
 50            if uiscale is ba.UIScale.SMALL
 51            else 480
 52            if uiscale is ba.UIScale.MEDIUM
 53            else 600
 54        )
 55
 56        top_extra = 20 if uiscale is ba.UIScale.SMALL else 0
 57        super().__init__(
 58            root_widget=ba.containerwidget(
 59                size=(width, height + top_extra),
 60                transition=transition,
 61                scale=(
 62                    2.17
 63                    if uiscale is ba.UIScale.SMALL
 64                    else 1.3
 65                    if uiscale is ba.UIScale.MEDIUM
 66                    else 1.0
 67                ),
 68                stack_offset=(0, -27)
 69                if uiscale is ba.UIScale.SMALL
 70                else (0, 0),
 71            )
 72        )
 73
 74        self._cancel_button = btn = ba.buttonwidget(
 75            parent=self._root_widget,
 76            position=(38 + x_inset, height - 67),
 77            size=(140, 50),
 78            scale=0.9,
 79            text_scale=1.0,
 80            autoselect=True,
 81            label=ba.Lstr(resource='cancelText'),
 82            on_activate_call=self._cancel,
 83        )
 84
 85        ba.containerwidget(edit=self._root_widget, cancel_button=btn)
 86        ba.textwidget(
 87            parent=self._root_widget,
 88            position=(width * 0.5, height - 46),
 89            size=(0, 0),
 90            maxwidth=260,
 91            scale=1.1,
 92            text=ba.Lstr(
 93                resource='mapSelectTitleText',
 94                subs=[('${GAME}', self._gametype.get_display_string())],
 95            ),
 96            color=ba.app.ui.title_color,
 97            h_align='center',
 98            v_align='center',
 99        )
100        v = height - 70
101        self._scroll_width = width - (80 + 2 * x_inset)
102        self._scroll_height = height - 140
103
104        self._scrollwidget = ba.scrollwidget(
105            parent=self._root_widget,
106            position=(40 + x_inset, v - self._scroll_height),
107            size=(self._scroll_width, self._scroll_height),
108        )
109        ba.containerwidget(
110            edit=self._root_widget, selected_child=self._scrollwidget
111        )
112        ba.containerwidget(edit=self._scrollwidget, claims_left_right=True)
113
114        self._subcontainer: ba.Widget | None = None
115        self._refresh()
Inherited Members
ba.ui.Window
get_root_widget