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)
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()