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