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