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