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