bastd.ui.playlist.browser
Provides a window for browsing and launching game playlists.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Provides a window for browsing and launching game playlists.""" 4 5from __future__ import annotations 6 7import copy 8import math 9from typing import TYPE_CHECKING 10 11import _ba 12import ba 13 14if TYPE_CHECKING: 15 pass 16 17 18class PlaylistBrowserWindow(ba.Window): 19 """Window for starting teams games.""" 20 21 def __init__(self, 22 sessiontype: type[ba.Session], 23 transition: str | None = 'in_right', 24 origin_widget: ba.Widget | None = None): 25 # pylint: disable=too-many-statements 26 # pylint: disable=cyclic-import 27 from bastd.ui.playlist import PlaylistTypeVars 28 29 # If they provided an origin-widget, scale up from that. 30 scale_origin: tuple[float, float] | None 31 if origin_widget is not None: 32 self._transition_out = 'out_scale' 33 scale_origin = origin_widget.get_screen_space_center() 34 transition = 'in_scale' 35 else: 36 self._transition_out = 'out_right' 37 scale_origin = None 38 39 # Store state for when we exit the next game. 40 if issubclass(sessiontype, ba.DualTeamSession): 41 ba.app.ui.set_main_menu_location('Team Game Select') 42 ba.set_analytics_screen('Teams Window') 43 elif issubclass(sessiontype, ba.FreeForAllSession): 44 ba.app.ui.set_main_menu_location('Free-for-All Game Select') 45 ba.set_analytics_screen('FreeForAll Window') 46 else: 47 raise TypeError(f'Invalid sessiontype: {sessiontype}.') 48 self._pvars = PlaylistTypeVars(sessiontype) 49 50 self._sessiontype = sessiontype 51 52 self._customize_button: ba.Widget | None = None 53 self._sub_width: float | None = None 54 self._sub_height: float | None = None 55 56 self._ensure_standard_playlists_exist() 57 58 # Get the current selection (if any). 59 self._selected_playlist = ba.app.config.get(self._pvars.config_name + 60 ' Playlist Selection') 61 62 uiscale = ba.app.ui.uiscale 63 self._width = 900 if uiscale is ba.UIScale.SMALL else 800 64 x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 65 self._height = (480 if uiscale is ba.UIScale.SMALL else 66 510 if uiscale is ba.UIScale.MEDIUM else 580) 67 68 top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 69 70 super().__init__(root_widget=ba.containerwidget( 71 size=(self._width, self._height + top_extra), 72 transition=transition, 73 toolbar_visibility='menu_full', 74 scale_origin_stack_offset=scale_origin, 75 scale=(1.69 if uiscale is ba.UIScale.SMALL else 76 1.05 if uiscale is ba.UIScale.MEDIUM else 0.9), 77 stack_offset=(0, -26) if uiscale is ba.UIScale.SMALL else (0, 0))) 78 79 self._back_button: ba.Widget | None = ba.buttonwidget( 80 parent=self._root_widget, 81 position=(59 + x_inset, self._height - 70), 82 size=(120, 60), 83 scale=1.0, 84 on_activate_call=self._on_back_press, 85 autoselect=True, 86 label=ba.Lstr(resource='backText'), 87 button_type='back') 88 ba.containerwidget(edit=self._root_widget, 89 cancel_button=self._back_button) 90 txt = self._title_text = ba.textwidget( 91 parent=self._root_widget, 92 position=(self._width * 0.5, self._height - 41), 93 size=(0, 0), 94 text=self._pvars.window_title_name, 95 scale=1.3, 96 res_scale=1.5, 97 color=ba.app.ui.heading_color, 98 h_align='center', 99 v_align='center') 100 if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: 101 ba.textwidget(edit=txt, text='') 102 103 ba.buttonwidget(edit=self._back_button, 104 button_type='backSmall', 105 size=(60, 54), 106 position=(59 + x_inset, self._height - 67), 107 label=ba.charstr(ba.SpecialChar.BACK)) 108 109 if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: 110 self._back_button.delete() 111 self._back_button = None 112 ba.containerwidget(edit=self._root_widget, 113 on_cancel_call=self._on_back_press) 114 scroll_offs = 33 115 else: 116 scroll_offs = 0 117 self._scroll_width = self._width - (100 + 2 * x_inset) 118 self._scroll_height = (self._height - 119 (146 if uiscale is ba.UIScale.SMALL 120 and ba.app.ui.use_toolbars else 136)) 121 self._scrollwidget = ba.scrollwidget( 122 parent=self._root_widget, 123 highlight=False, 124 size=(self._scroll_width, self._scroll_height), 125 position=((self._width - self._scroll_width) * 0.5, 126 65 + scroll_offs)) 127 ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) 128 self._subcontainer: ba.Widget | None = None 129 self._config_name_full = self._pvars.config_name + ' Playlists' 130 self._last_config = None 131 132 # Update now and once per second. 133 # (this should do our initial refresh) 134 self._update() 135 self._update_timer = ba.Timer(1.0, 136 ba.WeakCall(self._update), 137 timetype=ba.TimeType.REAL, 138 repeat=True) 139 140 def _ensure_standard_playlists_exist(self) -> None: 141 # On new installations, go ahead and create a few playlists 142 # besides the hard-coded default one: 143 if not _ba.get_v1_account_misc_val('madeStandardPlaylists', False): 144 _ba.add_transaction({ 145 'type': 146 'ADD_PLAYLIST', 147 'playlistType': 148 'Free-for-All', 149 'playlistName': 150 ba.Lstr(resource='singleGamePlaylistNameText' 151 ).evaluate().replace( 152 '${GAME}', 153 ba.Lstr(translate=('gameNames', 154 'Death Match')).evaluate()), 155 'playlist': [ 156 { 157 'type': 'bs_death_match.DeathMatchGame', 158 'settings': { 159 'Epic Mode': False, 160 'Kills to Win Per Player': 10, 161 'Respawn Times': 1.0, 162 'Time Limit': 300, 163 'map': 'Doom Shroom' 164 } 165 }, 166 { 167 'type': 'bs_death_match.DeathMatchGame', 168 'settings': { 169 'Epic Mode': False, 170 'Kills to Win Per Player': 10, 171 'Respawn Times': 1.0, 172 'Time Limit': 300, 173 'map': 'Crag Castle' 174 } 175 }, 176 ] 177 }) 178 _ba.add_transaction({ 179 'type': 180 'ADD_PLAYLIST', 181 'playlistType': 182 'Team Tournament', 183 'playlistName': 184 ba.Lstr( 185 resource='singleGamePlaylistNameText' 186 ).evaluate().replace( 187 '${GAME}', 188 ba.Lstr(translate=('gameNames', 189 'Capture the Flag')).evaluate()), 190 'playlist': [ 191 { 192 'type': 'bs_capture_the_flag.CTFGame', 193 'settings': { 194 'map': 'Bridgit', 195 'Score to Win': 3, 196 'Flag Idle Return Time': 30, 197 'Flag Touch Return Time': 0, 198 'Respawn Times': 1.0, 199 'Time Limit': 600, 200 'Epic Mode': False 201 } 202 }, 203 { 204 'type': 'bs_capture_the_flag.CTFGame', 205 'settings': { 206 'map': 'Roundabout', 207 'Score to Win': 2, 208 'Flag Idle Return Time': 30, 209 'Flag Touch Return Time': 0, 210 'Respawn Times': 1.0, 211 'Time Limit': 600, 212 'Epic Mode': False 213 } 214 }, 215 { 216 'type': 'bs_capture_the_flag.CTFGame', 217 'settings': { 218 'map': 'Tip Top', 219 'Score to Win': 2, 220 'Flag Idle Return Time': 30, 221 'Flag Touch Return Time': 3, 222 'Respawn Times': 1.0, 223 'Time Limit': 300, 224 'Epic Mode': False 225 } 226 }, 227 ] 228 }) 229 _ba.add_transaction({ 230 'type': 231 'ADD_PLAYLIST', 232 'playlistType': 233 'Team Tournament', 234 'playlistName': 235 ba.Lstr(translate=('playlistNames', 'Just Sports') 236 ).evaluate(), 237 'playlist': [ 238 { 239 'type': 'bs_hockey.HockeyGame', 240 'settings': { 241 'Time Limit': 0, 242 'map': 'Hockey Stadium', 243 'Score to Win': 1, 244 'Respawn Times': 1.0 245 } 246 }, 247 { 248 'type': 'bs_football.FootballTeamGame', 249 'settings': { 250 'Time Limit': 0, 251 'map': 'Football Stadium', 252 'Score to Win': 21, 253 'Respawn Times': 1.0 254 } 255 }, 256 ] 257 }) 258 _ba.add_transaction({ 259 'type': 260 'ADD_PLAYLIST', 261 'playlistType': 262 'Free-for-All', 263 'playlistName': 264 ba.Lstr(translate=('playlistNames', 'Just Epic') 265 ).evaluate(), 266 'playlist': [{ 267 'type': 'bs_elimination.EliminationGame', 268 'settings': { 269 'Time Limit': 120, 270 'map': 'Tip Top', 271 'Respawn Times': 1.0, 272 'Lives Per Player': 1, 273 'Epic Mode': 1 274 } 275 }] 276 }) 277 _ba.add_transaction({ 278 'type': 'SET_MISC_VAL', 279 'name': 'madeStandardPlaylists', 280 'value': True 281 }) 282 _ba.run_transactions() 283 284 def _refresh(self) -> None: 285 # FIXME: Should tidy this up. 286 # pylint: disable=too-many-statements 287 # pylint: disable=too-many-branches 288 # pylint: disable=too-many-locals 289 # pylint: disable=too-many-nested-blocks 290 from efro.util import asserttype 291 from ba.internal import get_map_class, filter_playlist 292 if not self._root_widget: 293 return 294 if self._subcontainer is not None: 295 self._save_state() 296 self._subcontainer.delete() 297 298 # Make sure config exists. 299 if self._config_name_full not in ba.app.config: 300 ba.app.config[self._config_name_full] = {} 301 302 items = list(ba.app.config[self._config_name_full].items()) 303 304 # Make sure everything is unicode. 305 items = [(i[0].decode(), i[1]) if not isinstance(i[0], str) else i 306 for i in items] 307 308 items.sort(key=lambda x2: asserttype(x2[0], str).lower()) 309 items = [['__default__', None]] + items # default is always first 310 311 count = len(items) 312 columns = 3 313 rows = int(math.ceil(float(count) / columns)) 314 button_width = 230 315 button_height = 230 316 button_buffer_h = -3 317 button_buffer_v = 0 318 319 self._sub_width = self._scroll_width 320 self._sub_height = 40 + rows * (button_height + 321 2 * button_buffer_v) + 90 322 assert self._sub_width is not None 323 assert self._sub_height is not None 324 self._subcontainer = ba.containerwidget(parent=self._scrollwidget, 325 size=(self._sub_width, 326 self._sub_height), 327 background=False) 328 329 children = self._subcontainer.get_children() 330 for child in children: 331 child.delete() 332 333 ba.textwidget(parent=self._subcontainer, 334 text=ba.Lstr(resource='playlistsText'), 335 position=(40, self._sub_height - 26), 336 size=(0, 0), 337 scale=1.0, 338 maxwidth=400, 339 color=ba.app.ui.title_color, 340 h_align='left', 341 v_align='center') 342 343 index = 0 344 appconfig = ba.app.config 345 346 model_opaque = ba.getmodel('level_select_button_opaque') 347 model_transparent = ba.getmodel('level_select_button_transparent') 348 mask_tex = ba.gettexture('mapPreviewMask') 349 350 h_offs = 225 if count == 1 else 115 if count == 2 else 0 351 h_offs_bottom = 0 352 353 uiscale = ba.app.ui.uiscale 354 for y in range(rows): 355 for x in range(columns): 356 name = items[index][0] 357 assert name is not None 358 pos = (x * (button_width + 2 * button_buffer_h) + 359 button_buffer_h + 8 + h_offs, self._sub_height - 47 - 360 (y + 1) * (button_height + 2 * button_buffer_v)) 361 btn = ba.buttonwidget(parent=self._subcontainer, 362 button_type='square', 363 size=(button_width, button_height), 364 autoselect=True, 365 label='', 366 position=pos) 367 368 if (x == 0 and ba.app.ui.use_toolbars 369 and uiscale is ba.UIScale.SMALL): 370 ba.widget( 371 edit=btn, 372 left_widget=_ba.get_special_widget('back_button')) 373 if (x == columns - 1 and ba.app.ui.use_toolbars 374 and uiscale is ba.UIScale.SMALL): 375 ba.widget( 376 edit=btn, 377 right_widget=_ba.get_special_widget('party_button')) 378 ba.buttonwidget( 379 edit=btn, 380 on_activate_call=ba.Call(self._on_playlist_press, btn, 381 name), 382 on_select_call=ba.Call(self._on_playlist_select, name)) 383 ba.widget(edit=btn, show_buffer_top=50, show_buffer_bottom=50) 384 385 if self._selected_playlist == name: 386 ba.containerwidget(edit=self._subcontainer, 387 selected_child=btn, 388 visible_child=btn) 389 390 if self._back_button is not None: 391 if y == 0: 392 ba.widget(edit=btn, up_widget=self._back_button) 393 if x == 0: 394 ba.widget(edit=btn, left_widget=self._back_button) 395 396 print_name: str | ba.Lstr | None 397 if name == '__default__': 398 print_name = self._pvars.default_list_name 399 else: 400 print_name = name 401 ba.textwidget(parent=self._subcontainer, 402 text=print_name, 403 position=(pos[0] + button_width * 0.5, 404 pos[1] + button_height * 0.79), 405 size=(0, 0), 406 scale=button_width * 0.003, 407 maxwidth=button_width * 0.7, 408 draw_controller=btn, 409 h_align='center', 410 v_align='center') 411 412 # Poke into this playlist and see if we can display some of 413 # its maps. 414 map_images = [] 415 try: 416 map_textures = [] 417 map_texture_entries = [] 418 if name == '__default__': 419 playlist = self._pvars.get_default_list_call() 420 else: 421 if name not in appconfig[self._pvars.config_name + 422 ' Playlists']: 423 print( 424 'NOT FOUND ERR', 425 appconfig[self._pvars.config_name + 426 ' Playlists']) 427 playlist = appconfig[self._pvars.config_name + 428 ' Playlists'][name] 429 playlist = filter_playlist(playlist, 430 self._sessiontype, 431 remove_unowned=False, 432 mark_unowned=True) 433 for entry in playlist: 434 mapname = entry['settings']['map'] 435 maptype: type[ba.Map] | None 436 try: 437 maptype = get_map_class(mapname) 438 except ba.NotFoundError: 439 maptype = None 440 if maptype is not None: 441 tex_name = maptype.get_preview_texture_name() 442 if tex_name is not None: 443 map_textures.append(tex_name) 444 map_texture_entries.append(entry) 445 if len(map_textures) >= 6: 446 break 447 448 if len(map_textures) > 4: 449 img_rows = 3 450 img_columns = 2 451 scl = 0.33 452 h_offs_img = 30 453 v_offs_img = 126 454 elif len(map_textures) > 2: 455 img_rows = 2 456 img_columns = 2 457 scl = 0.35 458 h_offs_img = 24 459 v_offs_img = 110 460 elif len(map_textures) > 1: 461 img_rows = 2 462 img_columns = 1 463 scl = 0.5 464 h_offs_img = 47 465 v_offs_img = 105 466 else: 467 img_rows = 1 468 img_columns = 1 469 scl = 0.75 470 h_offs_img = 20 471 v_offs_img = 65 472 473 v = None 474 for row in range(img_rows): 475 for col in range(img_columns): 476 tex_index = row * img_columns + col 477 if tex_index < len(map_textures): 478 entry = map_texture_entries[tex_index] 479 480 owned = not (('is_unowned_map' in entry 481 and entry['is_unowned_map']) or 482 ('is_unowned_game' in entry 483 and entry['is_unowned_game'])) 484 485 tex_name = map_textures[tex_index] 486 h = pos[0] + h_offs_img + scl * 250 * col 487 v = pos[1] + v_offs_img - scl * 130 * row 488 map_images.append( 489 ba.imagewidget( 490 parent=self._subcontainer, 491 size=(scl * 250.0, scl * 125.0), 492 position=(h, v), 493 texture=ba.gettexture(tex_name), 494 opacity=1.0 if owned else 0.25, 495 draw_controller=btn, 496 model_opaque=model_opaque, 497 model_transparent=model_transparent, 498 mask_texture=mask_tex)) 499 if not owned: 500 ba.imagewidget( 501 parent=self._subcontainer, 502 size=(scl * 100.0, scl * 100.0), 503 position=(h + scl * 75, v + scl * 10), 504 texture=ba.gettexture('lock'), 505 draw_controller=btn) 506 if v is not None: 507 v -= scl * 130.0 508 509 except Exception: 510 ba.print_exception('Error listing playlist maps.') 511 512 if not map_images: 513 ba.textwidget(parent=self._subcontainer, 514 text='???', 515 scale=1.5, 516 size=(0, 0), 517 color=(1, 1, 1, 0.5), 518 h_align='center', 519 v_align='center', 520 draw_controller=btn, 521 position=(pos[0] + button_width * 0.5, 522 pos[1] + button_height * 0.5)) 523 524 index += 1 525 526 if index >= count: 527 break 528 if index >= count: 529 break 530 self._customize_button = btn = ba.buttonwidget( 531 parent=self._subcontainer, 532 size=(100, 30), 533 position=(34 + h_offs_bottom, 50), 534 text_scale=0.6, 535 label=ba.Lstr(resource='customizeText'), 536 on_activate_call=self._on_customize_press, 537 color=(0.54, 0.52, 0.67), 538 textcolor=(0.7, 0.65, 0.7), 539 autoselect=True) 540 ba.widget(edit=btn, show_buffer_top=22, show_buffer_bottom=28) 541 self._restore_state() 542 543 def on_play_options_window_run_game(self) -> None: 544 """(internal)""" 545 if not self._root_widget: 546 return 547 ba.containerwidget(edit=self._root_widget, transition='out_left') 548 549 def _on_playlist_select(self, playlist_name: str) -> None: 550 self._selected_playlist = playlist_name 551 552 def _update(self) -> None: 553 554 # make sure config exists 555 if self._config_name_full not in ba.app.config: 556 ba.app.config[self._config_name_full] = {} 557 558 cfg = ba.app.config[self._config_name_full] 559 if cfg != self._last_config: 560 self._last_config = copy.deepcopy(cfg) 561 self._refresh() 562 563 def _on_playlist_press(self, button: ba.Widget, 564 playlist_name: str) -> None: 565 # pylint: disable=cyclic-import 566 from bastd.ui.playoptions import PlayOptionsWindow 567 568 # Make sure the target playlist still exists. 569 exists = (playlist_name == '__default__' 570 or playlist_name in ba.app.config.get( 571 self._config_name_full, {})) 572 if not exists: 573 return 574 575 self._save_state() 576 PlayOptionsWindow(sessiontype=self._sessiontype, 577 scale_origin=button.get_screen_space_center(), 578 playlist=playlist_name, 579 delegate=self) 580 581 def _on_customize_press(self) -> None: 582 # pylint: disable=cyclic-import 583 from bastd.ui.playlist.customizebrowser import ( 584 PlaylistCustomizeBrowserWindow) 585 self._save_state() 586 ba.containerwidget(edit=self._root_widget, transition='out_left') 587 ba.app.ui.set_main_menu_window( 588 PlaylistCustomizeBrowserWindow( 589 origin_widget=self._customize_button, 590 sessiontype=self._sessiontype).get_root_widget()) 591 592 def _on_back_press(self) -> None: 593 # pylint: disable=cyclic-import 594 from bastd.ui.play import PlayWindow 595 596 # Store our selected playlist if that's changed. 597 if self._selected_playlist is not None: 598 prev_sel = ba.app.config.get(self._pvars.config_name + 599 ' Playlist Selection') 600 if self._selected_playlist != prev_sel: 601 cfg = ba.app.config 602 cfg[self._pvars.config_name + 603 ' Playlist Selection'] = self._selected_playlist 604 cfg.commit() 605 606 self._save_state() 607 ba.containerwidget(edit=self._root_widget, 608 transition=self._transition_out) 609 ba.app.ui.set_main_menu_window( 610 PlayWindow(transition='in_left').get_root_widget()) 611 612 def _save_state(self) -> None: 613 try: 614 sel = self._root_widget.get_selected_child() 615 if sel == self._back_button: 616 sel_name = 'Back' 617 elif sel == self._scrollwidget: 618 assert self._subcontainer is not None 619 subsel = self._subcontainer.get_selected_child() 620 if subsel == self._customize_button: 621 sel_name = 'Customize' 622 else: 623 sel_name = 'Scroll' 624 else: 625 raise Exception('unrecognized selected widget') 626 ba.app.ui.window_states[type(self)] = sel_name 627 except Exception: 628 ba.print_exception(f'Error saving state for {self}.') 629 630 def _restore_state(self) -> None: 631 try: 632 sel_name = ba.app.ui.window_states.get(type(self)) 633 if sel_name == 'Back': 634 sel = self._back_button 635 elif sel_name == 'Scroll': 636 sel = self._scrollwidget 637 elif sel_name == 'Customize': 638 sel = self._scrollwidget 639 ba.containerwidget(edit=self._subcontainer, 640 selected_child=self._customize_button, 641 visible_child=self._customize_button) 642 else: 643 sel = self._scrollwidget 644 ba.containerwidget(edit=self._root_widget, selected_child=sel) 645 except Exception: 646 ba.print_exception(f'Error restoring state for {self}.')
class
PlaylistBrowserWindow(ba.ui.Window):
19class PlaylistBrowserWindow(ba.Window): 20 """Window for starting teams games.""" 21 22 def __init__(self, 23 sessiontype: type[ba.Session], 24 transition: str | None = 'in_right', 25 origin_widget: ba.Widget | None = None): 26 # pylint: disable=too-many-statements 27 # pylint: disable=cyclic-import 28 from bastd.ui.playlist import PlaylistTypeVars 29 30 # If they provided an origin-widget, scale up from that. 31 scale_origin: tuple[float, float] | None 32 if origin_widget is not None: 33 self._transition_out = 'out_scale' 34 scale_origin = origin_widget.get_screen_space_center() 35 transition = 'in_scale' 36 else: 37 self._transition_out = 'out_right' 38 scale_origin = None 39 40 # Store state for when we exit the next game. 41 if issubclass(sessiontype, ba.DualTeamSession): 42 ba.app.ui.set_main_menu_location('Team Game Select') 43 ba.set_analytics_screen('Teams Window') 44 elif issubclass(sessiontype, ba.FreeForAllSession): 45 ba.app.ui.set_main_menu_location('Free-for-All Game Select') 46 ba.set_analytics_screen('FreeForAll Window') 47 else: 48 raise TypeError(f'Invalid sessiontype: {sessiontype}.') 49 self._pvars = PlaylistTypeVars(sessiontype) 50 51 self._sessiontype = sessiontype 52 53 self._customize_button: ba.Widget | None = None 54 self._sub_width: float | None = None 55 self._sub_height: float | None = None 56 57 self._ensure_standard_playlists_exist() 58 59 # Get the current selection (if any). 60 self._selected_playlist = ba.app.config.get(self._pvars.config_name + 61 ' Playlist Selection') 62 63 uiscale = ba.app.ui.uiscale 64 self._width = 900 if uiscale is ba.UIScale.SMALL else 800 65 x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 66 self._height = (480 if uiscale is ba.UIScale.SMALL else 67 510 if uiscale is ba.UIScale.MEDIUM else 580) 68 69 top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 70 71 super().__init__(root_widget=ba.containerwidget( 72 size=(self._width, self._height + top_extra), 73 transition=transition, 74 toolbar_visibility='menu_full', 75 scale_origin_stack_offset=scale_origin, 76 scale=(1.69 if uiscale is ba.UIScale.SMALL else 77 1.05 if uiscale is ba.UIScale.MEDIUM else 0.9), 78 stack_offset=(0, -26) if uiscale is ba.UIScale.SMALL else (0, 0))) 79 80 self._back_button: ba.Widget | None = ba.buttonwidget( 81 parent=self._root_widget, 82 position=(59 + x_inset, self._height - 70), 83 size=(120, 60), 84 scale=1.0, 85 on_activate_call=self._on_back_press, 86 autoselect=True, 87 label=ba.Lstr(resource='backText'), 88 button_type='back') 89 ba.containerwidget(edit=self._root_widget, 90 cancel_button=self._back_button) 91 txt = self._title_text = ba.textwidget( 92 parent=self._root_widget, 93 position=(self._width * 0.5, self._height - 41), 94 size=(0, 0), 95 text=self._pvars.window_title_name, 96 scale=1.3, 97 res_scale=1.5, 98 color=ba.app.ui.heading_color, 99 h_align='center', 100 v_align='center') 101 if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: 102 ba.textwidget(edit=txt, text='') 103 104 ba.buttonwidget(edit=self._back_button, 105 button_type='backSmall', 106 size=(60, 54), 107 position=(59 + x_inset, self._height - 67), 108 label=ba.charstr(ba.SpecialChar.BACK)) 109 110 if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: 111 self._back_button.delete() 112 self._back_button = None 113 ba.containerwidget(edit=self._root_widget, 114 on_cancel_call=self._on_back_press) 115 scroll_offs = 33 116 else: 117 scroll_offs = 0 118 self._scroll_width = self._width - (100 + 2 * x_inset) 119 self._scroll_height = (self._height - 120 (146 if uiscale is ba.UIScale.SMALL 121 and ba.app.ui.use_toolbars else 136)) 122 self._scrollwidget = ba.scrollwidget( 123 parent=self._root_widget, 124 highlight=False, 125 size=(self._scroll_width, self._scroll_height), 126 position=((self._width - self._scroll_width) * 0.5, 127 65 + scroll_offs)) 128 ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) 129 self._subcontainer: ba.Widget | None = None 130 self._config_name_full = self._pvars.config_name + ' Playlists' 131 self._last_config = None 132 133 # Update now and once per second. 134 # (this should do our initial refresh) 135 self._update() 136 self._update_timer = ba.Timer(1.0, 137 ba.WeakCall(self._update), 138 timetype=ba.TimeType.REAL, 139 repeat=True) 140 141 def _ensure_standard_playlists_exist(self) -> None: 142 # On new installations, go ahead and create a few playlists 143 # besides the hard-coded default one: 144 if not _ba.get_v1_account_misc_val('madeStandardPlaylists', False): 145 _ba.add_transaction({ 146 'type': 147 'ADD_PLAYLIST', 148 'playlistType': 149 'Free-for-All', 150 'playlistName': 151 ba.Lstr(resource='singleGamePlaylistNameText' 152 ).evaluate().replace( 153 '${GAME}', 154 ba.Lstr(translate=('gameNames', 155 'Death Match')).evaluate()), 156 'playlist': [ 157 { 158 'type': 'bs_death_match.DeathMatchGame', 159 'settings': { 160 'Epic Mode': False, 161 'Kills to Win Per Player': 10, 162 'Respawn Times': 1.0, 163 'Time Limit': 300, 164 'map': 'Doom Shroom' 165 } 166 }, 167 { 168 'type': 'bs_death_match.DeathMatchGame', 169 'settings': { 170 'Epic Mode': False, 171 'Kills to Win Per Player': 10, 172 'Respawn Times': 1.0, 173 'Time Limit': 300, 174 'map': 'Crag Castle' 175 } 176 }, 177 ] 178 }) 179 _ba.add_transaction({ 180 'type': 181 'ADD_PLAYLIST', 182 'playlistType': 183 'Team Tournament', 184 'playlistName': 185 ba.Lstr( 186 resource='singleGamePlaylistNameText' 187 ).evaluate().replace( 188 '${GAME}', 189 ba.Lstr(translate=('gameNames', 190 'Capture the Flag')).evaluate()), 191 'playlist': [ 192 { 193 'type': 'bs_capture_the_flag.CTFGame', 194 'settings': { 195 'map': 'Bridgit', 196 'Score to Win': 3, 197 'Flag Idle Return Time': 30, 198 'Flag Touch Return Time': 0, 199 'Respawn Times': 1.0, 200 'Time Limit': 600, 201 'Epic Mode': False 202 } 203 }, 204 { 205 'type': 'bs_capture_the_flag.CTFGame', 206 'settings': { 207 'map': 'Roundabout', 208 'Score to Win': 2, 209 'Flag Idle Return Time': 30, 210 'Flag Touch Return Time': 0, 211 'Respawn Times': 1.0, 212 'Time Limit': 600, 213 'Epic Mode': False 214 } 215 }, 216 { 217 'type': 'bs_capture_the_flag.CTFGame', 218 'settings': { 219 'map': 'Tip Top', 220 'Score to Win': 2, 221 'Flag Idle Return Time': 30, 222 'Flag Touch Return Time': 3, 223 'Respawn Times': 1.0, 224 'Time Limit': 300, 225 'Epic Mode': False 226 } 227 }, 228 ] 229 }) 230 _ba.add_transaction({ 231 'type': 232 'ADD_PLAYLIST', 233 'playlistType': 234 'Team Tournament', 235 'playlistName': 236 ba.Lstr(translate=('playlistNames', 'Just Sports') 237 ).evaluate(), 238 'playlist': [ 239 { 240 'type': 'bs_hockey.HockeyGame', 241 'settings': { 242 'Time Limit': 0, 243 'map': 'Hockey Stadium', 244 'Score to Win': 1, 245 'Respawn Times': 1.0 246 } 247 }, 248 { 249 'type': 'bs_football.FootballTeamGame', 250 'settings': { 251 'Time Limit': 0, 252 'map': 'Football Stadium', 253 'Score to Win': 21, 254 'Respawn Times': 1.0 255 } 256 }, 257 ] 258 }) 259 _ba.add_transaction({ 260 'type': 261 'ADD_PLAYLIST', 262 'playlistType': 263 'Free-for-All', 264 'playlistName': 265 ba.Lstr(translate=('playlistNames', 'Just Epic') 266 ).evaluate(), 267 'playlist': [{ 268 'type': 'bs_elimination.EliminationGame', 269 'settings': { 270 'Time Limit': 120, 271 'map': 'Tip Top', 272 'Respawn Times': 1.0, 273 'Lives Per Player': 1, 274 'Epic Mode': 1 275 } 276 }] 277 }) 278 _ba.add_transaction({ 279 'type': 'SET_MISC_VAL', 280 'name': 'madeStandardPlaylists', 281 'value': True 282 }) 283 _ba.run_transactions() 284 285 def _refresh(self) -> None: 286 # FIXME: Should tidy this up. 287 # pylint: disable=too-many-statements 288 # pylint: disable=too-many-branches 289 # pylint: disable=too-many-locals 290 # pylint: disable=too-many-nested-blocks 291 from efro.util import asserttype 292 from ba.internal import get_map_class, filter_playlist 293 if not self._root_widget: 294 return 295 if self._subcontainer is not None: 296 self._save_state() 297 self._subcontainer.delete() 298 299 # Make sure config exists. 300 if self._config_name_full not in ba.app.config: 301 ba.app.config[self._config_name_full] = {} 302 303 items = list(ba.app.config[self._config_name_full].items()) 304 305 # Make sure everything is unicode. 306 items = [(i[0].decode(), i[1]) if not isinstance(i[0], str) else i 307 for i in items] 308 309 items.sort(key=lambda x2: asserttype(x2[0], str).lower()) 310 items = [['__default__', None]] + items # default is always first 311 312 count = len(items) 313 columns = 3 314 rows = int(math.ceil(float(count) / columns)) 315 button_width = 230 316 button_height = 230 317 button_buffer_h = -3 318 button_buffer_v = 0 319 320 self._sub_width = self._scroll_width 321 self._sub_height = 40 + rows * (button_height + 322 2 * button_buffer_v) + 90 323 assert self._sub_width is not None 324 assert self._sub_height is not None 325 self._subcontainer = ba.containerwidget(parent=self._scrollwidget, 326 size=(self._sub_width, 327 self._sub_height), 328 background=False) 329 330 children = self._subcontainer.get_children() 331 for child in children: 332 child.delete() 333 334 ba.textwidget(parent=self._subcontainer, 335 text=ba.Lstr(resource='playlistsText'), 336 position=(40, self._sub_height - 26), 337 size=(0, 0), 338 scale=1.0, 339 maxwidth=400, 340 color=ba.app.ui.title_color, 341 h_align='left', 342 v_align='center') 343 344 index = 0 345 appconfig = ba.app.config 346 347 model_opaque = ba.getmodel('level_select_button_opaque') 348 model_transparent = ba.getmodel('level_select_button_transparent') 349 mask_tex = ba.gettexture('mapPreviewMask') 350 351 h_offs = 225 if count == 1 else 115 if count == 2 else 0 352 h_offs_bottom = 0 353 354 uiscale = ba.app.ui.uiscale 355 for y in range(rows): 356 for x in range(columns): 357 name = items[index][0] 358 assert name is not None 359 pos = (x * (button_width + 2 * button_buffer_h) + 360 button_buffer_h + 8 + h_offs, self._sub_height - 47 - 361 (y + 1) * (button_height + 2 * button_buffer_v)) 362 btn = ba.buttonwidget(parent=self._subcontainer, 363 button_type='square', 364 size=(button_width, button_height), 365 autoselect=True, 366 label='', 367 position=pos) 368 369 if (x == 0 and ba.app.ui.use_toolbars 370 and uiscale is ba.UIScale.SMALL): 371 ba.widget( 372 edit=btn, 373 left_widget=_ba.get_special_widget('back_button')) 374 if (x == columns - 1 and ba.app.ui.use_toolbars 375 and uiscale is ba.UIScale.SMALL): 376 ba.widget( 377 edit=btn, 378 right_widget=_ba.get_special_widget('party_button')) 379 ba.buttonwidget( 380 edit=btn, 381 on_activate_call=ba.Call(self._on_playlist_press, btn, 382 name), 383 on_select_call=ba.Call(self._on_playlist_select, name)) 384 ba.widget(edit=btn, show_buffer_top=50, show_buffer_bottom=50) 385 386 if self._selected_playlist == name: 387 ba.containerwidget(edit=self._subcontainer, 388 selected_child=btn, 389 visible_child=btn) 390 391 if self._back_button is not None: 392 if y == 0: 393 ba.widget(edit=btn, up_widget=self._back_button) 394 if x == 0: 395 ba.widget(edit=btn, left_widget=self._back_button) 396 397 print_name: str | ba.Lstr | None 398 if name == '__default__': 399 print_name = self._pvars.default_list_name 400 else: 401 print_name = name 402 ba.textwidget(parent=self._subcontainer, 403 text=print_name, 404 position=(pos[0] + button_width * 0.5, 405 pos[1] + button_height * 0.79), 406 size=(0, 0), 407 scale=button_width * 0.003, 408 maxwidth=button_width * 0.7, 409 draw_controller=btn, 410 h_align='center', 411 v_align='center') 412 413 # Poke into this playlist and see if we can display some of 414 # its maps. 415 map_images = [] 416 try: 417 map_textures = [] 418 map_texture_entries = [] 419 if name == '__default__': 420 playlist = self._pvars.get_default_list_call() 421 else: 422 if name not in appconfig[self._pvars.config_name + 423 ' Playlists']: 424 print( 425 'NOT FOUND ERR', 426 appconfig[self._pvars.config_name + 427 ' Playlists']) 428 playlist = appconfig[self._pvars.config_name + 429 ' Playlists'][name] 430 playlist = filter_playlist(playlist, 431 self._sessiontype, 432 remove_unowned=False, 433 mark_unowned=True) 434 for entry in playlist: 435 mapname = entry['settings']['map'] 436 maptype: type[ba.Map] | None 437 try: 438 maptype = get_map_class(mapname) 439 except ba.NotFoundError: 440 maptype = None 441 if maptype is not None: 442 tex_name = maptype.get_preview_texture_name() 443 if tex_name is not None: 444 map_textures.append(tex_name) 445 map_texture_entries.append(entry) 446 if len(map_textures) >= 6: 447 break 448 449 if len(map_textures) > 4: 450 img_rows = 3 451 img_columns = 2 452 scl = 0.33 453 h_offs_img = 30 454 v_offs_img = 126 455 elif len(map_textures) > 2: 456 img_rows = 2 457 img_columns = 2 458 scl = 0.35 459 h_offs_img = 24 460 v_offs_img = 110 461 elif len(map_textures) > 1: 462 img_rows = 2 463 img_columns = 1 464 scl = 0.5 465 h_offs_img = 47 466 v_offs_img = 105 467 else: 468 img_rows = 1 469 img_columns = 1 470 scl = 0.75 471 h_offs_img = 20 472 v_offs_img = 65 473 474 v = None 475 for row in range(img_rows): 476 for col in range(img_columns): 477 tex_index = row * img_columns + col 478 if tex_index < len(map_textures): 479 entry = map_texture_entries[tex_index] 480 481 owned = not (('is_unowned_map' in entry 482 and entry['is_unowned_map']) or 483 ('is_unowned_game' in entry 484 and entry['is_unowned_game'])) 485 486 tex_name = map_textures[tex_index] 487 h = pos[0] + h_offs_img + scl * 250 * col 488 v = pos[1] + v_offs_img - scl * 130 * row 489 map_images.append( 490 ba.imagewidget( 491 parent=self._subcontainer, 492 size=(scl * 250.0, scl * 125.0), 493 position=(h, v), 494 texture=ba.gettexture(tex_name), 495 opacity=1.0 if owned else 0.25, 496 draw_controller=btn, 497 model_opaque=model_opaque, 498 model_transparent=model_transparent, 499 mask_texture=mask_tex)) 500 if not owned: 501 ba.imagewidget( 502 parent=self._subcontainer, 503 size=(scl * 100.0, scl * 100.0), 504 position=(h + scl * 75, v + scl * 10), 505 texture=ba.gettexture('lock'), 506 draw_controller=btn) 507 if v is not None: 508 v -= scl * 130.0 509 510 except Exception: 511 ba.print_exception('Error listing playlist maps.') 512 513 if not map_images: 514 ba.textwidget(parent=self._subcontainer, 515 text='???', 516 scale=1.5, 517 size=(0, 0), 518 color=(1, 1, 1, 0.5), 519 h_align='center', 520 v_align='center', 521 draw_controller=btn, 522 position=(pos[0] + button_width * 0.5, 523 pos[1] + button_height * 0.5)) 524 525 index += 1 526 527 if index >= count: 528 break 529 if index >= count: 530 break 531 self._customize_button = btn = ba.buttonwidget( 532 parent=self._subcontainer, 533 size=(100, 30), 534 position=(34 + h_offs_bottom, 50), 535 text_scale=0.6, 536 label=ba.Lstr(resource='customizeText'), 537 on_activate_call=self._on_customize_press, 538 color=(0.54, 0.52, 0.67), 539 textcolor=(0.7, 0.65, 0.7), 540 autoselect=True) 541 ba.widget(edit=btn, show_buffer_top=22, show_buffer_bottom=28) 542 self._restore_state() 543 544 def on_play_options_window_run_game(self) -> None: 545 """(internal)""" 546 if not self._root_widget: 547 return 548 ba.containerwidget(edit=self._root_widget, transition='out_left') 549 550 def _on_playlist_select(self, playlist_name: str) -> None: 551 self._selected_playlist = playlist_name 552 553 def _update(self) -> None: 554 555 # make sure config exists 556 if self._config_name_full not in ba.app.config: 557 ba.app.config[self._config_name_full] = {} 558 559 cfg = ba.app.config[self._config_name_full] 560 if cfg != self._last_config: 561 self._last_config = copy.deepcopy(cfg) 562 self._refresh() 563 564 def _on_playlist_press(self, button: ba.Widget, 565 playlist_name: str) -> None: 566 # pylint: disable=cyclic-import 567 from bastd.ui.playoptions import PlayOptionsWindow 568 569 # Make sure the target playlist still exists. 570 exists = (playlist_name == '__default__' 571 or playlist_name in ba.app.config.get( 572 self._config_name_full, {})) 573 if not exists: 574 return 575 576 self._save_state() 577 PlayOptionsWindow(sessiontype=self._sessiontype, 578 scale_origin=button.get_screen_space_center(), 579 playlist=playlist_name, 580 delegate=self) 581 582 def _on_customize_press(self) -> None: 583 # pylint: disable=cyclic-import 584 from bastd.ui.playlist.customizebrowser import ( 585 PlaylistCustomizeBrowserWindow) 586 self._save_state() 587 ba.containerwidget(edit=self._root_widget, transition='out_left') 588 ba.app.ui.set_main_menu_window( 589 PlaylistCustomizeBrowserWindow( 590 origin_widget=self._customize_button, 591 sessiontype=self._sessiontype).get_root_widget()) 592 593 def _on_back_press(self) -> None: 594 # pylint: disable=cyclic-import 595 from bastd.ui.play import PlayWindow 596 597 # Store our selected playlist if that's changed. 598 if self._selected_playlist is not None: 599 prev_sel = ba.app.config.get(self._pvars.config_name + 600 ' Playlist Selection') 601 if self._selected_playlist != prev_sel: 602 cfg = ba.app.config 603 cfg[self._pvars.config_name + 604 ' Playlist Selection'] = self._selected_playlist 605 cfg.commit() 606 607 self._save_state() 608 ba.containerwidget(edit=self._root_widget, 609 transition=self._transition_out) 610 ba.app.ui.set_main_menu_window( 611 PlayWindow(transition='in_left').get_root_widget()) 612 613 def _save_state(self) -> None: 614 try: 615 sel = self._root_widget.get_selected_child() 616 if sel == self._back_button: 617 sel_name = 'Back' 618 elif sel == self._scrollwidget: 619 assert self._subcontainer is not None 620 subsel = self._subcontainer.get_selected_child() 621 if subsel == self._customize_button: 622 sel_name = 'Customize' 623 else: 624 sel_name = 'Scroll' 625 else: 626 raise Exception('unrecognized selected widget') 627 ba.app.ui.window_states[type(self)] = sel_name 628 except Exception: 629 ba.print_exception(f'Error saving state for {self}.') 630 631 def _restore_state(self) -> None: 632 try: 633 sel_name = ba.app.ui.window_states.get(type(self)) 634 if sel_name == 'Back': 635 sel = self._back_button 636 elif sel_name == 'Scroll': 637 sel = self._scrollwidget 638 elif sel_name == 'Customize': 639 sel = self._scrollwidget 640 ba.containerwidget(edit=self._subcontainer, 641 selected_child=self._customize_button, 642 visible_child=self._customize_button) 643 else: 644 sel = self._scrollwidget 645 ba.containerwidget(edit=self._root_widget, selected_child=sel) 646 except Exception: 647 ba.print_exception(f'Error restoring state for {self}.')
Window for starting teams games.
PlaylistBrowserWindow( sessiontype: type[ba._session.Session], transition: str | None = 'in_right', origin_widget: _ba.Widget | None = None)
22 def __init__(self, 23 sessiontype: type[ba.Session], 24 transition: str | None = 'in_right', 25 origin_widget: ba.Widget | None = None): 26 # pylint: disable=too-many-statements 27 # pylint: disable=cyclic-import 28 from bastd.ui.playlist import PlaylistTypeVars 29 30 # If they provided an origin-widget, scale up from that. 31 scale_origin: tuple[float, float] | None 32 if origin_widget is not None: 33 self._transition_out = 'out_scale' 34 scale_origin = origin_widget.get_screen_space_center() 35 transition = 'in_scale' 36 else: 37 self._transition_out = 'out_right' 38 scale_origin = None 39 40 # Store state for when we exit the next game. 41 if issubclass(sessiontype, ba.DualTeamSession): 42 ba.app.ui.set_main_menu_location('Team Game Select') 43 ba.set_analytics_screen('Teams Window') 44 elif issubclass(sessiontype, ba.FreeForAllSession): 45 ba.app.ui.set_main_menu_location('Free-for-All Game Select') 46 ba.set_analytics_screen('FreeForAll Window') 47 else: 48 raise TypeError(f'Invalid sessiontype: {sessiontype}.') 49 self._pvars = PlaylistTypeVars(sessiontype) 50 51 self._sessiontype = sessiontype 52 53 self._customize_button: ba.Widget | None = None 54 self._sub_width: float | None = None 55 self._sub_height: float | None = None 56 57 self._ensure_standard_playlists_exist() 58 59 # Get the current selection (if any). 60 self._selected_playlist = ba.app.config.get(self._pvars.config_name + 61 ' Playlist Selection') 62 63 uiscale = ba.app.ui.uiscale 64 self._width = 900 if uiscale is ba.UIScale.SMALL else 800 65 x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 66 self._height = (480 if uiscale is ba.UIScale.SMALL else 67 510 if uiscale is ba.UIScale.MEDIUM else 580) 68 69 top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 70 71 super().__init__(root_widget=ba.containerwidget( 72 size=(self._width, self._height + top_extra), 73 transition=transition, 74 toolbar_visibility='menu_full', 75 scale_origin_stack_offset=scale_origin, 76 scale=(1.69 if uiscale is ba.UIScale.SMALL else 77 1.05 if uiscale is ba.UIScale.MEDIUM else 0.9), 78 stack_offset=(0, -26) if uiscale is ba.UIScale.SMALL else (0, 0))) 79 80 self._back_button: ba.Widget | None = ba.buttonwidget( 81 parent=self._root_widget, 82 position=(59 + x_inset, self._height - 70), 83 size=(120, 60), 84 scale=1.0, 85 on_activate_call=self._on_back_press, 86 autoselect=True, 87 label=ba.Lstr(resource='backText'), 88 button_type='back') 89 ba.containerwidget(edit=self._root_widget, 90 cancel_button=self._back_button) 91 txt = self._title_text = ba.textwidget( 92 parent=self._root_widget, 93 position=(self._width * 0.5, self._height - 41), 94 size=(0, 0), 95 text=self._pvars.window_title_name, 96 scale=1.3, 97 res_scale=1.5, 98 color=ba.app.ui.heading_color, 99 h_align='center', 100 v_align='center') 101 if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: 102 ba.textwidget(edit=txt, text='') 103 104 ba.buttonwidget(edit=self._back_button, 105 button_type='backSmall', 106 size=(60, 54), 107 position=(59 + x_inset, self._height - 67), 108 label=ba.charstr(ba.SpecialChar.BACK)) 109 110 if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: 111 self._back_button.delete() 112 self._back_button = None 113 ba.containerwidget(edit=self._root_widget, 114 on_cancel_call=self._on_back_press) 115 scroll_offs = 33 116 else: 117 scroll_offs = 0 118 self._scroll_width = self._width - (100 + 2 * x_inset) 119 self._scroll_height = (self._height - 120 (146 if uiscale is ba.UIScale.SMALL 121 and ba.app.ui.use_toolbars else 136)) 122 self._scrollwidget = ba.scrollwidget( 123 parent=self._root_widget, 124 highlight=False, 125 size=(self._scroll_width, self._scroll_height), 126 position=((self._width - self._scroll_width) * 0.5, 127 65 + scroll_offs)) 128 ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) 129 self._subcontainer: ba.Widget | None = None 130 self._config_name_full = self._pvars.config_name + ' Playlists' 131 self._last_config = None 132 133 # Update now and once per second. 134 # (this should do our initial refresh) 135 self._update() 136 self._update_timer = ba.Timer(1.0, 137 ba.WeakCall(self._update), 138 timetype=ba.TimeType.REAL, 139 repeat=True)
Inherited Members
- ba.ui.Window
- get_root_widget