mirror of
https://github.com/hypervortex/VH-Bombsquad-Modded-Server-Files
synced 2025-11-07 17:36:08 +00:00
Added new files
This commit is contained in:
parent
867634cc5c
commit
3a407868d4
1775 changed files with 550222 additions and 0 deletions
64
dist/ba_data/python/bastd/ui/playlist/__init__.py
vendored
Normal file
64
dist/ba_data/python/bastd/ui/playlist/__init__.py
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Playlist ui functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
# FIXME: Could change this to be a classmethod of session types?
|
||||
class PlaylistTypeVars:
|
||||
"""Defines values for a playlist type (config names to use, etc)."""
|
||||
|
||||
def __init__(self, sessiontype: type[ba.Session]):
|
||||
from ba.internal import (
|
||||
get_default_teams_playlist,
|
||||
get_default_free_for_all_playlist,
|
||||
)
|
||||
|
||||
self.sessiontype: type[ba.Session]
|
||||
|
||||
if issubclass(sessiontype, ba.DualTeamSession):
|
||||
play_mode_name = ba.Lstr(
|
||||
resource='playModes.teamsText', fallback_resource='teamsText'
|
||||
)
|
||||
self.get_default_list_call = get_default_teams_playlist
|
||||
self.session_type_name = 'ba.DualTeamSession'
|
||||
self.config_name = 'Team Tournament'
|
||||
self.window_title_name = ba.Lstr(
|
||||
resource='playModes.teamsText', fallback_resource='teamsText'
|
||||
)
|
||||
self.sessiontype = ba.DualTeamSession
|
||||
|
||||
elif issubclass(sessiontype, ba.FreeForAllSession):
|
||||
play_mode_name = ba.Lstr(
|
||||
resource='playModes.freeForAllText',
|
||||
fallback_resource='freeForAllText',
|
||||
)
|
||||
self.get_default_list_call = get_default_free_for_all_playlist
|
||||
self.session_type_name = 'ba.FreeForAllSession'
|
||||
self.config_name = 'Free-for-All'
|
||||
self.window_title_name = ba.Lstr(
|
||||
resource='playModes.freeForAllText',
|
||||
fallback_resource='freeForAllText',
|
||||
)
|
||||
self.sessiontype = ba.FreeForAllSession
|
||||
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f'Playlist type vars undefined for sessiontype: {sessiontype}'
|
||||
)
|
||||
self.default_list_name = ba.Lstr(
|
||||
resource='defaultGameListNameText',
|
||||
subs=[('${PLAYMODE}', play_mode_name)],
|
||||
)
|
||||
self.default_new_list_name = ba.Lstr(
|
||||
resource='defaultNewGameListNameText',
|
||||
subs=[('${PLAYMODE}', play_mode_name)],
|
||||
)
|
||||
271
dist/ba_data/python/bastd/ui/playlist/addgame.py
vendored
Normal file
271
dist/ba_data/python/bastd/ui/playlist/addgame.py
vendored
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides a window for selecting a game type to add to a playlist."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import ba
|
||||
import ba.internal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bastd.ui.playlist.editcontroller import PlaylistEditController
|
||||
|
||||
|
||||
class PlaylistAddGameWindow(ba.Window):
|
||||
"""Window for selecting a game type to add to a playlist."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
editcontroller: PlaylistEditController,
|
||||
transition: str = 'in_right',
|
||||
):
|
||||
self._editcontroller = editcontroller
|
||||
self._r = 'addGameWindow'
|
||||
uiscale = ba.app.ui.uiscale
|
||||
self._width = 750 if uiscale is ba.UIScale.SMALL else 650
|
||||
x_inset = 50 if uiscale is ba.UIScale.SMALL else 0
|
||||
self._height = (
|
||||
346
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 380
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 440
|
||||
)
|
||||
top_extra = 30 if uiscale is ba.UIScale.SMALL else 20
|
||||
self._scroll_width = 210
|
||||
|
||||
super().__init__(
|
||||
root_widget=ba.containerwidget(
|
||||
size=(self._width, self._height + top_extra),
|
||||
transition=transition,
|
||||
scale=(
|
||||
2.17
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 1.5
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 1.0
|
||||
),
|
||||
stack_offset=(0, 1) if uiscale is ba.UIScale.SMALL else (0, 0),
|
||||
)
|
||||
)
|
||||
|
||||
self._back_button = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(58 + x_inset, self._height - 53),
|
||||
size=(165, 70),
|
||||
scale=0.75,
|
||||
text_scale=1.2,
|
||||
label=ba.Lstr(resource='backText'),
|
||||
autoselect=True,
|
||||
button_type='back',
|
||||
on_activate_call=self._back,
|
||||
)
|
||||
self._select_button = select_button = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(self._width - (172 + x_inset), self._height - 50),
|
||||
autoselect=True,
|
||||
size=(160, 60),
|
||||
scale=0.75,
|
||||
text_scale=1.2,
|
||||
label=ba.Lstr(resource='selectText'),
|
||||
on_activate_call=self._add,
|
||||
)
|
||||
|
||||
if ba.app.ui.use_toolbars:
|
||||
ba.widget(
|
||||
edit=select_button,
|
||||
right_widget=ba.internal.get_special_widget('party_button'),
|
||||
)
|
||||
|
||||
ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
position=(self._width * 0.5, self._height - 28),
|
||||
size=(0, 0),
|
||||
scale=1.0,
|
||||
text=ba.Lstr(resource=self._r + '.titleText'),
|
||||
h_align='center',
|
||||
color=ba.app.ui.title_color,
|
||||
maxwidth=250,
|
||||
v_align='center',
|
||||
)
|
||||
v = self._height - 64
|
||||
|
||||
self._selected_title_text = ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
position=(x_inset + self._scroll_width + 50 + 30, v - 15),
|
||||
size=(0, 0),
|
||||
scale=1.0,
|
||||
color=(0.7, 1.0, 0.7, 1.0),
|
||||
maxwidth=self._width - self._scroll_width - 150 - x_inset * 2,
|
||||
h_align='left',
|
||||
v_align='center',
|
||||
)
|
||||
v -= 30
|
||||
|
||||
self._selected_description_text = ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
position=(x_inset + self._scroll_width + 50 + 30, v),
|
||||
size=(0, 0),
|
||||
scale=0.7,
|
||||
color=(0.5, 0.8, 0.5, 1.0),
|
||||
maxwidth=self._width - self._scroll_width - 150 - x_inset * 2,
|
||||
h_align='left',
|
||||
)
|
||||
|
||||
scroll_height = self._height - 100
|
||||
|
||||
v = self._height - 60
|
||||
|
||||
self._scrollwidget = ba.scrollwidget(
|
||||
parent=self._root_widget,
|
||||
position=(x_inset + 61, v - scroll_height),
|
||||
size=(self._scroll_width, scroll_height),
|
||||
highlight=False,
|
||||
)
|
||||
ba.widget(
|
||||
edit=self._scrollwidget,
|
||||
up_widget=self._back_button,
|
||||
left_widget=self._back_button,
|
||||
right_widget=select_button,
|
||||
)
|
||||
self._column: ba.Widget | None = None
|
||||
|
||||
v -= 35
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget,
|
||||
cancel_button=self._back_button,
|
||||
start_button=select_button,
|
||||
)
|
||||
self._selected_game_type: type[ba.GameActivity] | None = None
|
||||
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget, selected_child=self._scrollwidget
|
||||
)
|
||||
|
||||
self._game_types: list[type[ba.GameActivity]] = []
|
||||
|
||||
# Get actual games loading in the bg.
|
||||
ba.app.meta.load_exported_classes(
|
||||
ba.GameActivity,
|
||||
self._on_game_types_loaded,
|
||||
completion_cb_in_bg_thread=True,
|
||||
)
|
||||
|
||||
# Refresh with our initial empty list. We'll refresh again once
|
||||
# game loading is complete.
|
||||
self._refresh()
|
||||
|
||||
def _on_game_types_loaded(
|
||||
self, gametypes: list[type[ba.GameActivity]]
|
||||
) -> None:
|
||||
from ba.internal import get_unowned_game_types
|
||||
|
||||
# We asked for a bg thread completion cb so we can do some
|
||||
# filtering here in the bg thread where its not gonna cause hitches.
|
||||
assert not ba.in_logic_thread()
|
||||
sessiontype = self._editcontroller.get_session_type()
|
||||
unowned = get_unowned_game_types()
|
||||
self._game_types = [
|
||||
gt
|
||||
for gt in gametypes
|
||||
if gt not in unowned and gt.supports_session_type(sessiontype)
|
||||
]
|
||||
|
||||
# Sort in the current language.
|
||||
self._game_types.sort(key=lambda g: g.get_display_string().evaluate())
|
||||
|
||||
# Tell ourself to refresh back in the logic thread.
|
||||
ba.pushcall(self._refresh, from_other_thread=True)
|
||||
|
||||
def _refresh(self, select_get_more_games_button: bool = False) -> None:
|
||||
# from ba.internal import get_game_types
|
||||
|
||||
if self._column is not None:
|
||||
self._column.delete()
|
||||
|
||||
self._column = ba.columnwidget(
|
||||
parent=self._scrollwidget, border=2, margin=0
|
||||
)
|
||||
|
||||
for i, gametype in enumerate(self._game_types):
|
||||
|
||||
def _doit() -> None:
|
||||
if self._select_button:
|
||||
ba.timer(
|
||||
0.1,
|
||||
self._select_button.activate,
|
||||
timetype=ba.TimeType.REAL,
|
||||
)
|
||||
|
||||
txt = ba.textwidget(
|
||||
parent=self._column,
|
||||
position=(0, 0),
|
||||
size=(self._width - 88, 24),
|
||||
text=gametype.get_display_string(),
|
||||
h_align='left',
|
||||
v_align='center',
|
||||
color=(0.8, 0.8, 0.8, 1.0),
|
||||
maxwidth=self._scroll_width * 0.8,
|
||||
on_select_call=ba.Call(self._set_selected_game_type, gametype),
|
||||
always_highlight=True,
|
||||
selectable=True,
|
||||
on_activate_call=_doit,
|
||||
)
|
||||
if i == 0:
|
||||
ba.widget(edit=txt, up_widget=self._back_button)
|
||||
|
||||
self._get_more_games_button = ba.buttonwidget(
|
||||
parent=self._column,
|
||||
autoselect=True,
|
||||
label=ba.Lstr(resource=self._r + '.getMoreGamesText'),
|
||||
color=(0.54, 0.52, 0.67),
|
||||
textcolor=(0.7, 0.65, 0.7),
|
||||
on_activate_call=self._on_get_more_games_press,
|
||||
size=(178, 50),
|
||||
)
|
||||
if select_get_more_games_button:
|
||||
ba.containerwidget(
|
||||
edit=self._column,
|
||||
selected_child=self._get_more_games_button,
|
||||
visible_child=self._get_more_games_button,
|
||||
)
|
||||
|
||||
def _on_get_more_games_press(self) -> None:
|
||||
from bastd.ui.account import show_sign_in_prompt
|
||||
from bastd.ui.store.browser import StoreBrowserWindow
|
||||
|
||||
if ba.internal.get_v1_account_state() != 'signed_in':
|
||||
show_sign_in_prompt()
|
||||
return
|
||||
StoreBrowserWindow(
|
||||
modal=True,
|
||||
show_tab=StoreBrowserWindow.TabID.MINIGAMES,
|
||||
on_close_call=self._on_store_close,
|
||||
origin_widget=self._get_more_games_button,
|
||||
)
|
||||
|
||||
def _on_store_close(self) -> None:
|
||||
self._refresh(select_get_more_games_button=True)
|
||||
|
||||
def _add(self) -> None:
|
||||
ba.internal.lock_all_input() # Make sure no more commands happen.
|
||||
ba.timer(0.1, ba.internal.unlock_all_input, timetype=ba.TimeType.REAL)
|
||||
assert self._selected_game_type is not None
|
||||
self._editcontroller.add_game_type_selected(self._selected_game_type)
|
||||
|
||||
def _set_selected_game_type(self, gametype: type[ba.GameActivity]) -> None:
|
||||
self._selected_game_type = gametype
|
||||
ba.textwidget(
|
||||
edit=self._selected_title_text, text=gametype.get_display_string()
|
||||
)
|
||||
ba.textwidget(
|
||||
edit=self._selected_description_text,
|
||||
text=gametype.get_description_display_string(
|
||||
self._editcontroller.get_session_type()
|
||||
),
|
||||
)
|
||||
|
||||
def _back(self) -> None:
|
||||
self._editcontroller.add_game_cancelled()
|
||||
761
dist/ba_data/python/bastd/ui/playlist/browser.py
vendored
Normal file
761
dist/ba_data/python/bastd/ui/playlist/browser.py
vendored
Normal file
|
|
@ -0,0 +1,761 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides a window for browsing and launching game playlists."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import math
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import ba
|
||||
import ba.internal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
class PlaylistBrowserWindow(ba.Window):
|
||||
"""Window for starting teams games."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sessiontype: type[ba.Session],
|
||||
transition: str | None = 'in_right',
|
||||
origin_widget: ba.Widget | None = None,
|
||||
):
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.playlist import PlaylistTypeVars
|
||||
|
||||
# If they provided an origin-widget, scale up from that.
|
||||
scale_origin: tuple[float, float] | None
|
||||
if origin_widget is not None:
|
||||
self._transition_out = 'out_scale'
|
||||
scale_origin = origin_widget.get_screen_space_center()
|
||||
transition = 'in_scale'
|
||||
else:
|
||||
self._transition_out = 'out_right'
|
||||
scale_origin = None
|
||||
|
||||
# Store state for when we exit the next game.
|
||||
if issubclass(sessiontype, ba.DualTeamSession):
|
||||
ba.app.ui.set_main_menu_location('Team Game Select')
|
||||
ba.set_analytics_screen('Teams Window')
|
||||
elif issubclass(sessiontype, ba.FreeForAllSession):
|
||||
ba.app.ui.set_main_menu_location('Free-for-All Game Select')
|
||||
ba.set_analytics_screen('FreeForAll Window')
|
||||
else:
|
||||
raise TypeError(f'Invalid sessiontype: {sessiontype}.')
|
||||
self._pvars = PlaylistTypeVars(sessiontype)
|
||||
|
||||
self._sessiontype = sessiontype
|
||||
|
||||
self._customize_button: ba.Widget | None = None
|
||||
self._sub_width: float | None = None
|
||||
self._sub_height: float | None = None
|
||||
|
||||
self._ensure_standard_playlists_exist()
|
||||
|
||||
# Get the current selection (if any).
|
||||
self._selected_playlist = ba.app.config.get(
|
||||
self._pvars.config_name + ' Playlist Selection'
|
||||
)
|
||||
|
||||
uiscale = ba.app.ui.uiscale
|
||||
self._width = 900.0 if uiscale is ba.UIScale.SMALL else 800.0
|
||||
x_inset = 50 if uiscale is ba.UIScale.SMALL else 0
|
||||
self._height = (
|
||||
480
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 510
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 580
|
||||
)
|
||||
|
||||
top_extra = 20 if uiscale is ba.UIScale.SMALL else 0
|
||||
|
||||
super().__init__(
|
||||
root_widget=ba.containerwidget(
|
||||
size=(self._width, self._height + top_extra),
|
||||
transition=transition,
|
||||
toolbar_visibility='menu_full',
|
||||
scale_origin_stack_offset=scale_origin,
|
||||
scale=(
|
||||
1.69
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 1.05
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 0.9
|
||||
),
|
||||
stack_offset=(0, -26)
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else (0, 0),
|
||||
)
|
||||
)
|
||||
|
||||
self._back_button: ba.Widget | None = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(59 + x_inset, self._height - 70),
|
||||
size=(120, 60),
|
||||
scale=1.0,
|
||||
on_activate_call=self._on_back_press,
|
||||
autoselect=True,
|
||||
label=ba.Lstr(resource='backText'),
|
||||
button_type='back',
|
||||
)
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget, cancel_button=self._back_button
|
||||
)
|
||||
txt = self._title_text = ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
position=(self._width * 0.5, self._height - 41),
|
||||
size=(0, 0),
|
||||
text=self._pvars.window_title_name,
|
||||
scale=1.3,
|
||||
res_scale=1.5,
|
||||
color=ba.app.ui.heading_color,
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
)
|
||||
if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
|
||||
ba.textwidget(edit=txt, text='')
|
||||
|
||||
ba.buttonwidget(
|
||||
edit=self._back_button,
|
||||
button_type='backSmall',
|
||||
size=(60, 54),
|
||||
position=(59 + x_inset, self._height - 67),
|
||||
label=ba.charstr(ba.SpecialChar.BACK),
|
||||
)
|
||||
|
||||
if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
|
||||
self._back_button.delete()
|
||||
self._back_button = None
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget, on_cancel_call=self._on_back_press
|
||||
)
|
||||
scroll_offs = 33
|
||||
else:
|
||||
scroll_offs = 0
|
||||
self._scroll_width = self._width - (100 + 2 * x_inset)
|
||||
self._scroll_height = self._height - (
|
||||
146
|
||||
if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars
|
||||
else 136
|
||||
)
|
||||
self._scrollwidget = ba.scrollwidget(
|
||||
parent=self._root_widget,
|
||||
highlight=False,
|
||||
size=(self._scroll_width, self._scroll_height),
|
||||
position=(
|
||||
(self._width - self._scroll_width) * 0.5,
|
||||
65 + scroll_offs,
|
||||
),
|
||||
)
|
||||
ba.containerwidget(edit=self._scrollwidget, claims_left_right=True)
|
||||
self._subcontainer: ba.Widget | None = None
|
||||
self._config_name_full = self._pvars.config_name + ' Playlists'
|
||||
self._last_config = None
|
||||
|
||||
# Update now and once per second.
|
||||
# (this should do our initial refresh)
|
||||
self._update()
|
||||
self._update_timer = ba.Timer(
|
||||
1.0,
|
||||
ba.WeakCall(self._update),
|
||||
timetype=ba.TimeType.REAL,
|
||||
repeat=True,
|
||||
)
|
||||
|
||||
def _ensure_standard_playlists_exist(self) -> None:
|
||||
# On new installations, go ahead and create a few playlists
|
||||
# besides the hard-coded default one:
|
||||
if not ba.internal.get_v1_account_misc_val(
|
||||
'madeStandardPlaylists', False
|
||||
):
|
||||
ba.internal.add_transaction(
|
||||
{
|
||||
'type': 'ADD_PLAYLIST',
|
||||
'playlistType': 'Free-for-All',
|
||||
'playlistName': ba.Lstr(
|
||||
resource='singleGamePlaylistNameText'
|
||||
)
|
||||
.evaluate()
|
||||
.replace(
|
||||
'${GAME}',
|
||||
ba.Lstr(
|
||||
translate=('gameNames', 'Death Match')
|
||||
).evaluate(),
|
||||
),
|
||||
'playlist': [
|
||||
{
|
||||
'type': 'bs_death_match.DeathMatchGame',
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Kills to Win Per Player': 10,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Doom Shroom',
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'bs_death_match.DeathMatchGame',
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Kills to Win Per Player': 10,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Crag Castle',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
ba.internal.add_transaction(
|
||||
{
|
||||
'type': 'ADD_PLAYLIST',
|
||||
'playlistType': 'Team Tournament',
|
||||
'playlistName': ba.Lstr(
|
||||
resource='singleGamePlaylistNameText'
|
||||
)
|
||||
.evaluate()
|
||||
.replace(
|
||||
'${GAME}',
|
||||
ba.Lstr(
|
||||
translate=('gameNames', 'Capture the Flag')
|
||||
).evaluate(),
|
||||
),
|
||||
'playlist': [
|
||||
{
|
||||
'type': 'bs_capture_the_flag.CTFGame',
|
||||
'settings': {
|
||||
'map': 'Bridgit',
|
||||
'Score to Win': 3,
|
||||
'Flag Idle Return Time': 30,
|
||||
'Flag Touch Return Time': 0,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 600,
|
||||
'Epic Mode': False,
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'bs_capture_the_flag.CTFGame',
|
||||
'settings': {
|
||||
'map': 'Roundabout',
|
||||
'Score to Win': 2,
|
||||
'Flag Idle Return Time': 30,
|
||||
'Flag Touch Return Time': 0,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 600,
|
||||
'Epic Mode': False,
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'bs_capture_the_flag.CTFGame',
|
||||
'settings': {
|
||||
'map': 'Tip Top',
|
||||
'Score to Win': 2,
|
||||
'Flag Idle Return Time': 30,
|
||||
'Flag Touch Return Time': 3,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'Epic Mode': False,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
ba.internal.add_transaction(
|
||||
{
|
||||
'type': 'ADD_PLAYLIST',
|
||||
'playlistType': 'Team Tournament',
|
||||
'playlistName': ba.Lstr(
|
||||
translate=('playlistNames', 'Just Sports')
|
||||
).evaluate(),
|
||||
'playlist': [
|
||||
{
|
||||
'type': 'bs_hockey.HockeyGame',
|
||||
'settings': {
|
||||
'Time Limit': 0,
|
||||
'map': 'Hockey Stadium',
|
||||
'Score to Win': 1,
|
||||
'Respawn Times': 1.0,
|
||||
},
|
||||
},
|
||||
{
|
||||
'type': 'bs_football.FootballTeamGame',
|
||||
'settings': {
|
||||
'Time Limit': 0,
|
||||
'map': 'Football Stadium',
|
||||
'Score to Win': 21,
|
||||
'Respawn Times': 1.0,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
ba.internal.add_transaction(
|
||||
{
|
||||
'type': 'ADD_PLAYLIST',
|
||||
'playlistType': 'Free-for-All',
|
||||
'playlistName': ba.Lstr(
|
||||
translate=('playlistNames', 'Just Epic')
|
||||
).evaluate(),
|
||||
'playlist': [
|
||||
{
|
||||
'type': 'bs_elimination.EliminationGame',
|
||||
'settings': {
|
||||
'Time Limit': 120,
|
||||
'map': 'Tip Top',
|
||||
'Respawn Times': 1.0,
|
||||
'Lives Per Player': 1,
|
||||
'Epic Mode': 1,
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
ba.internal.add_transaction(
|
||||
{
|
||||
'type': 'SET_MISC_VAL',
|
||||
'name': 'madeStandardPlaylists',
|
||||
'value': True,
|
||||
}
|
||||
)
|
||||
ba.internal.run_transactions()
|
||||
|
||||
def _refresh(self) -> None:
|
||||
# FIXME: Should tidy this up.
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
from efro.util import asserttype
|
||||
from ba.internal import get_map_class, filter_playlist
|
||||
|
||||
if not self._root_widget:
|
||||
return
|
||||
if self._subcontainer is not None:
|
||||
self._save_state()
|
||||
self._subcontainer.delete()
|
||||
|
||||
# Make sure config exists.
|
||||
if self._config_name_full not in ba.app.config:
|
||||
ba.app.config[self._config_name_full] = {}
|
||||
|
||||
items = list(ba.app.config[self._config_name_full].items())
|
||||
|
||||
# Make sure everything is unicode.
|
||||
items = [
|
||||
(i[0].decode(), i[1]) if not isinstance(i[0], str) else i
|
||||
for i in items
|
||||
]
|
||||
|
||||
items.sort(key=lambda x2: asserttype(x2[0], str).lower())
|
||||
items = [['__default__', None]] + items # default is always first
|
||||
|
||||
count = len(items)
|
||||
columns = 3
|
||||
rows = int(math.ceil(float(count) / columns))
|
||||
button_width = 230
|
||||
button_height = 230
|
||||
button_buffer_h = -3
|
||||
button_buffer_v = 0
|
||||
|
||||
self._sub_width = self._scroll_width
|
||||
self._sub_height = (
|
||||
40.0 + rows * (button_height + 2 * button_buffer_v) + 90
|
||||
)
|
||||
assert self._sub_width is not None
|
||||
assert self._sub_height is not None
|
||||
self._subcontainer = ba.containerwidget(
|
||||
parent=self._scrollwidget,
|
||||
size=(self._sub_width, self._sub_height),
|
||||
background=False,
|
||||
)
|
||||
|
||||
children = self._subcontainer.get_children()
|
||||
for child in children:
|
||||
child.delete()
|
||||
|
||||
ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
text=ba.Lstr(resource='playlistsText'),
|
||||
position=(40, self._sub_height - 26),
|
||||
size=(0, 0),
|
||||
scale=1.0,
|
||||
maxwidth=400,
|
||||
color=ba.app.ui.title_color,
|
||||
h_align='left',
|
||||
v_align='center',
|
||||
)
|
||||
|
||||
index = 0
|
||||
appconfig = ba.app.config
|
||||
|
||||
model_opaque = ba.getmodel('level_select_button_opaque')
|
||||
model_transparent = ba.getmodel('level_select_button_transparent')
|
||||
mask_tex = ba.gettexture('mapPreviewMask')
|
||||
|
||||
h_offs = 225 if count == 1 else 115 if count == 2 else 0
|
||||
h_offs_bottom = 0
|
||||
|
||||
uiscale = ba.app.ui.uiscale
|
||||
for y in range(rows):
|
||||
for x in range(columns):
|
||||
name = items[index][0]
|
||||
assert name is not None
|
||||
pos = (
|
||||
x * (button_width + 2 * button_buffer_h)
|
||||
+ button_buffer_h
|
||||
+ 8
|
||||
+ h_offs,
|
||||
self._sub_height
|
||||
- 47
|
||||
- (y + 1) * (button_height + 2 * button_buffer_v),
|
||||
)
|
||||
btn = ba.buttonwidget(
|
||||
parent=self._subcontainer,
|
||||
button_type='square',
|
||||
size=(button_width, button_height),
|
||||
autoselect=True,
|
||||
label='',
|
||||
position=pos,
|
||||
)
|
||||
|
||||
if (
|
||||
x == 0
|
||||
and ba.app.ui.use_toolbars
|
||||
and uiscale is ba.UIScale.SMALL
|
||||
):
|
||||
ba.widget(
|
||||
edit=btn,
|
||||
left_widget=ba.internal.get_special_widget(
|
||||
'back_button'
|
||||
),
|
||||
)
|
||||
if (
|
||||
x == columns - 1
|
||||
and ba.app.ui.use_toolbars
|
||||
and uiscale is ba.UIScale.SMALL
|
||||
):
|
||||
ba.widget(
|
||||
edit=btn,
|
||||
right_widget=ba.internal.get_special_widget(
|
||||
'party_button'
|
||||
),
|
||||
)
|
||||
ba.buttonwidget(
|
||||
edit=btn,
|
||||
on_activate_call=ba.Call(
|
||||
self._on_playlist_press, btn, name
|
||||
),
|
||||
on_select_call=ba.Call(self._on_playlist_select, name),
|
||||
)
|
||||
ba.widget(edit=btn, show_buffer_top=50, show_buffer_bottom=50)
|
||||
|
||||
if self._selected_playlist == name:
|
||||
ba.containerwidget(
|
||||
edit=self._subcontainer,
|
||||
selected_child=btn,
|
||||
visible_child=btn,
|
||||
)
|
||||
|
||||
if self._back_button is not None:
|
||||
if y == 0:
|
||||
ba.widget(edit=btn, up_widget=self._back_button)
|
||||
if x == 0:
|
||||
ba.widget(edit=btn, left_widget=self._back_button)
|
||||
|
||||
print_name: str | ba.Lstr | None
|
||||
if name == '__default__':
|
||||
print_name = self._pvars.default_list_name
|
||||
else:
|
||||
print_name = name
|
||||
ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
text=print_name,
|
||||
position=(
|
||||
pos[0] + button_width * 0.5,
|
||||
pos[1] + button_height * 0.79,
|
||||
),
|
||||
size=(0, 0),
|
||||
scale=button_width * 0.003,
|
||||
maxwidth=button_width * 0.7,
|
||||
draw_controller=btn,
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
)
|
||||
|
||||
# Poke into this playlist and see if we can display some of
|
||||
# its maps.
|
||||
map_images = []
|
||||
try:
|
||||
map_textures = []
|
||||
map_texture_entries = []
|
||||
if name == '__default__':
|
||||
playlist = self._pvars.get_default_list_call()
|
||||
else:
|
||||
if (
|
||||
name
|
||||
not in appconfig[
|
||||
self._pvars.config_name + ' Playlists'
|
||||
]
|
||||
):
|
||||
print(
|
||||
'NOT FOUND ERR',
|
||||
appconfig[
|
||||
self._pvars.config_name + ' Playlists'
|
||||
],
|
||||
)
|
||||
playlist = appconfig[
|
||||
self._pvars.config_name + ' Playlists'
|
||||
][name]
|
||||
playlist = filter_playlist(
|
||||
playlist,
|
||||
self._sessiontype,
|
||||
remove_unowned=False,
|
||||
mark_unowned=True,
|
||||
name=name,
|
||||
)
|
||||
for entry in playlist:
|
||||
mapname = entry['settings']['map']
|
||||
maptype: type[ba.Map] | None
|
||||
try:
|
||||
maptype = get_map_class(mapname)
|
||||
except ba.NotFoundError:
|
||||
maptype = None
|
||||
if maptype is not None:
|
||||
tex_name = maptype.get_preview_texture_name()
|
||||
if tex_name is not None:
|
||||
map_textures.append(tex_name)
|
||||
map_texture_entries.append(entry)
|
||||
if len(map_textures) >= 6:
|
||||
break
|
||||
|
||||
if len(map_textures) > 4:
|
||||
img_rows = 3
|
||||
img_columns = 2
|
||||
scl = 0.33
|
||||
h_offs_img = 30
|
||||
v_offs_img = 126
|
||||
elif len(map_textures) > 2:
|
||||
img_rows = 2
|
||||
img_columns = 2
|
||||
scl = 0.35
|
||||
h_offs_img = 24
|
||||
v_offs_img = 110
|
||||
elif len(map_textures) > 1:
|
||||
img_rows = 2
|
||||
img_columns = 1
|
||||
scl = 0.5
|
||||
h_offs_img = 47
|
||||
v_offs_img = 105
|
||||
else:
|
||||
img_rows = 1
|
||||
img_columns = 1
|
||||
scl = 0.75
|
||||
h_offs_img = 20
|
||||
v_offs_img = 65
|
||||
|
||||
v = None
|
||||
for row in range(img_rows):
|
||||
for col in range(img_columns):
|
||||
tex_index = row * img_columns + col
|
||||
if tex_index < len(map_textures):
|
||||
entry = map_texture_entries[tex_index]
|
||||
|
||||
owned = not (
|
||||
(
|
||||
'is_unowned_map' in entry
|
||||
and entry['is_unowned_map']
|
||||
)
|
||||
or (
|
||||
'is_unowned_game' in entry
|
||||
and entry['is_unowned_game']
|
||||
)
|
||||
)
|
||||
|
||||
tex_name = map_textures[tex_index]
|
||||
h = pos[0] + h_offs_img + scl * 250 * col
|
||||
v = pos[1] + v_offs_img - scl * 130 * row
|
||||
map_images.append(
|
||||
ba.imagewidget(
|
||||
parent=self._subcontainer,
|
||||
size=(scl * 250.0, scl * 125.0),
|
||||
position=(h, v),
|
||||
texture=ba.gettexture(tex_name),
|
||||
opacity=1.0 if owned else 0.25,
|
||||
draw_controller=btn,
|
||||
model_opaque=model_opaque,
|
||||
model_transparent=model_transparent,
|
||||
mask_texture=mask_tex,
|
||||
)
|
||||
)
|
||||
if not owned:
|
||||
ba.imagewidget(
|
||||
parent=self._subcontainer,
|
||||
size=(scl * 100.0, scl * 100.0),
|
||||
position=(h + scl * 75, v + scl * 10),
|
||||
texture=ba.gettexture('lock'),
|
||||
draw_controller=btn,
|
||||
)
|
||||
if v is not None:
|
||||
v -= scl * 130.0
|
||||
|
||||
except Exception:
|
||||
ba.print_exception('Error listing playlist maps.')
|
||||
|
||||
if not map_images:
|
||||
ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
text='???',
|
||||
scale=1.5,
|
||||
size=(0, 0),
|
||||
color=(1, 1, 1, 0.5),
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
draw_controller=btn,
|
||||
position=(
|
||||
pos[0] + button_width * 0.5,
|
||||
pos[1] + button_height * 0.5,
|
||||
),
|
||||
)
|
||||
|
||||
index += 1
|
||||
|
||||
if index >= count:
|
||||
break
|
||||
if index >= count:
|
||||
break
|
||||
self._customize_button = btn = ba.buttonwidget(
|
||||
parent=self._subcontainer,
|
||||
size=(100, 30),
|
||||
position=(34 + h_offs_bottom, 50),
|
||||
text_scale=0.6,
|
||||
label=ba.Lstr(resource='customizeText'),
|
||||
on_activate_call=self._on_customize_press,
|
||||
color=(0.54, 0.52, 0.67),
|
||||
textcolor=(0.7, 0.65, 0.7),
|
||||
autoselect=True,
|
||||
)
|
||||
ba.widget(edit=btn, show_buffer_top=22, show_buffer_bottom=28)
|
||||
self._restore_state()
|
||||
|
||||
def on_play_options_window_run_game(self) -> None:
|
||||
"""(internal)"""
|
||||
if not self._root_widget:
|
||||
return
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_left')
|
||||
|
||||
def _on_playlist_select(self, playlist_name: str) -> None:
|
||||
self._selected_playlist = playlist_name
|
||||
|
||||
def _update(self) -> None:
|
||||
|
||||
# make sure config exists
|
||||
if self._config_name_full not in ba.app.config:
|
||||
ba.app.config[self._config_name_full] = {}
|
||||
|
||||
cfg = ba.app.config[self._config_name_full]
|
||||
if cfg != self._last_config:
|
||||
self._last_config = copy.deepcopy(cfg)
|
||||
self._refresh()
|
||||
|
||||
def _on_playlist_press(self, button: ba.Widget, playlist_name: str) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.playoptions import PlayOptionsWindow
|
||||
|
||||
# Make sure the target playlist still exists.
|
||||
exists = (
|
||||
playlist_name == '__default__'
|
||||
or playlist_name in ba.app.config.get(self._config_name_full, {})
|
||||
)
|
||||
if not exists:
|
||||
return
|
||||
|
||||
self._save_state()
|
||||
PlayOptionsWindow(
|
||||
sessiontype=self._sessiontype,
|
||||
scale_origin=button.get_screen_space_center(),
|
||||
playlist=playlist_name,
|
||||
delegate=self,
|
||||
)
|
||||
|
||||
def _on_customize_press(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.playlist.customizebrowser import (
|
||||
PlaylistCustomizeBrowserWindow,
|
||||
)
|
||||
|
||||
self._save_state()
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_left')
|
||||
ba.app.ui.set_main_menu_window(
|
||||
PlaylistCustomizeBrowserWindow(
|
||||
origin_widget=self._customize_button,
|
||||
sessiontype=self._sessiontype,
|
||||
).get_root_widget()
|
||||
)
|
||||
|
||||
def _on_back_press(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.play import PlayWindow
|
||||
|
||||
# Store our selected playlist if that's changed.
|
||||
if self._selected_playlist is not None:
|
||||
prev_sel = ba.app.config.get(
|
||||
self._pvars.config_name + ' Playlist Selection'
|
||||
)
|
||||
if self._selected_playlist != prev_sel:
|
||||
cfg = ba.app.config
|
||||
cfg[
|
||||
self._pvars.config_name + ' Playlist Selection'
|
||||
] = self._selected_playlist
|
||||
cfg.commit()
|
||||
|
||||
self._save_state()
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget, transition=self._transition_out
|
||||
)
|
||||
ba.app.ui.set_main_menu_window(
|
||||
PlayWindow(transition='in_left').get_root_widget()
|
||||
)
|
||||
|
||||
def _save_state(self) -> None:
|
||||
try:
|
||||
sel = self._root_widget.get_selected_child()
|
||||
if sel == self._back_button:
|
||||
sel_name = 'Back'
|
||||
elif sel == self._scrollwidget:
|
||||
assert self._subcontainer is not None
|
||||
subsel = self._subcontainer.get_selected_child()
|
||||
if subsel == self._customize_button:
|
||||
sel_name = 'Customize'
|
||||
else:
|
||||
sel_name = 'Scroll'
|
||||
else:
|
||||
raise Exception('unrecognized selected widget')
|
||||
ba.app.ui.window_states[type(self)] = sel_name
|
||||
except Exception:
|
||||
ba.print_exception(f'Error saving state for {self}.')
|
||||
|
||||
def _restore_state(self) -> None:
|
||||
try:
|
||||
sel_name = ba.app.ui.window_states.get(type(self))
|
||||
if sel_name == 'Back':
|
||||
sel = self._back_button
|
||||
elif sel_name == 'Scroll':
|
||||
sel = self._scrollwidget
|
||||
elif sel_name == 'Customize':
|
||||
sel = self._scrollwidget
|
||||
ba.containerwidget(
|
||||
edit=self._subcontainer,
|
||||
selected_child=self._customize_button,
|
||||
visible_child=self._customize_button,
|
||||
)
|
||||
else:
|
||||
sel = self._scrollwidget
|
||||
ba.containerwidget(edit=self._root_widget, selected_child=sel)
|
||||
except Exception:
|
||||
ba.print_exception(f'Error restoring state for {self}.')
|
||||
715
dist/ba_data/python/bastd/ui/playlist/customizebrowser.py
vendored
Normal file
715
dist/ba_data/python/bastd/ui/playlist/customizebrowser.py
vendored
Normal file
|
|
@ -0,0 +1,715 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides UI for viewing/creating/editing playlists."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import ba
|
||||
import ba.internal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
class PlaylistCustomizeBrowserWindow(ba.Window):
|
||||
"""Window for viewing a playlist."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sessiontype: type[ba.Session],
|
||||
transition: str = 'in_right',
|
||||
select_playlist: str | None = None,
|
||||
origin_widget: ba.Widget | None = None,
|
||||
):
|
||||
# Yes this needs tidying.
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui import playlist
|
||||
|
||||
scale_origin: tuple[float, float] | None
|
||||
if origin_widget is not None:
|
||||
self._transition_out = 'out_scale'
|
||||
scale_origin = origin_widget.get_screen_space_center()
|
||||
transition = 'in_scale'
|
||||
else:
|
||||
self._transition_out = 'out_right'
|
||||
scale_origin = None
|
||||
|
||||
self._sessiontype = sessiontype
|
||||
self._pvars = playlist.PlaylistTypeVars(sessiontype)
|
||||
self._max_playlists = 30
|
||||
self._r = 'gameListWindow'
|
||||
uiscale = ba.app.ui.uiscale
|
||||
self._width = 750.0 if uiscale is ba.UIScale.SMALL else 650.0
|
||||
x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0
|
||||
self._height = (
|
||||
380.0
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 420.0
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 500.0
|
||||
)
|
||||
top_extra = 20.0 if uiscale is ba.UIScale.SMALL else 0.0
|
||||
|
||||
super().__init__(
|
||||
root_widget=ba.containerwidget(
|
||||
size=(self._width, self._height + top_extra),
|
||||
transition=transition,
|
||||
scale_origin_stack_offset=scale_origin,
|
||||
scale=(
|
||||
2.05
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 1.5
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 1.0
|
||||
),
|
||||
stack_offset=(0, -10)
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else (0, 0),
|
||||
)
|
||||
)
|
||||
|
||||
self._back_button = back_button = btn = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(43 + x_inset, self._height - 60),
|
||||
size=(160, 68),
|
||||
scale=0.77,
|
||||
autoselect=True,
|
||||
text_scale=1.3,
|
||||
label=ba.Lstr(resource='backText'),
|
||||
button_type='back',
|
||||
)
|
||||
|
||||
ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
position=(0, self._height - 47),
|
||||
size=(self._width, 25),
|
||||
text=ba.Lstr(
|
||||
resource=self._r + '.titleText',
|
||||
subs=[('${TYPE}', self._pvars.window_title_name)],
|
||||
),
|
||||
color=ba.app.ui.heading_color,
|
||||
maxwidth=290,
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
)
|
||||
|
||||
ba.buttonwidget(
|
||||
edit=btn,
|
||||
button_type='backSmall',
|
||||
size=(60, 60),
|
||||
label=ba.charstr(ba.SpecialChar.BACK),
|
||||
)
|
||||
|
||||
v = self._height - 59.0
|
||||
h = 41 + x_inset
|
||||
b_color = (0.6, 0.53, 0.63)
|
||||
b_textcolor = (0.75, 0.7, 0.8)
|
||||
self._lock_images: list[ba.Widget] = []
|
||||
lock_tex = ba.gettexture('lock')
|
||||
|
||||
scl = (
|
||||
1.1
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 1.27
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 1.57
|
||||
)
|
||||
scl *= 0.63
|
||||
v -= 65.0 * scl
|
||||
new_button = btn = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(h, v),
|
||||
size=(90, 58.0 * scl),
|
||||
on_activate_call=self._new_playlist,
|
||||
color=b_color,
|
||||
autoselect=True,
|
||||
button_type='square',
|
||||
textcolor=b_textcolor,
|
||||
text_scale=0.7,
|
||||
label=ba.Lstr(
|
||||
resource='newText', fallback_resource=self._r + '.newText'
|
||||
),
|
||||
)
|
||||
self._lock_images.append(
|
||||
ba.imagewidget(
|
||||
parent=self._root_widget,
|
||||
size=(30, 30),
|
||||
draw_controller=btn,
|
||||
position=(h - 10, v + 58.0 * scl - 28),
|
||||
texture=lock_tex,
|
||||
)
|
||||
)
|
||||
|
||||
v -= 65.0 * scl
|
||||
self._edit_button = edit_button = btn = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(h, v),
|
||||
size=(90, 58.0 * scl),
|
||||
on_activate_call=self._edit_playlist,
|
||||
color=b_color,
|
||||
autoselect=True,
|
||||
textcolor=b_textcolor,
|
||||
button_type='square',
|
||||
text_scale=0.7,
|
||||
label=ba.Lstr(
|
||||
resource='editText', fallback_resource=self._r + '.editText'
|
||||
),
|
||||
)
|
||||
self._lock_images.append(
|
||||
ba.imagewidget(
|
||||
parent=self._root_widget,
|
||||
size=(30, 30),
|
||||
draw_controller=btn,
|
||||
position=(h - 10, v + 58.0 * scl - 28),
|
||||
texture=lock_tex,
|
||||
)
|
||||
)
|
||||
|
||||
v -= 65.0 * scl
|
||||
duplicate_button = btn = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(h, v),
|
||||
size=(90, 58.0 * scl),
|
||||
on_activate_call=self._duplicate_playlist,
|
||||
color=b_color,
|
||||
autoselect=True,
|
||||
textcolor=b_textcolor,
|
||||
button_type='square',
|
||||
text_scale=0.7,
|
||||
label=ba.Lstr(
|
||||
resource='duplicateText',
|
||||
fallback_resource=self._r + '.duplicateText',
|
||||
),
|
||||
)
|
||||
self._lock_images.append(
|
||||
ba.imagewidget(
|
||||
parent=self._root_widget,
|
||||
size=(30, 30),
|
||||
draw_controller=btn,
|
||||
position=(h - 10, v + 58.0 * scl - 28),
|
||||
texture=lock_tex,
|
||||
)
|
||||
)
|
||||
|
||||
v -= 65.0 * scl
|
||||
delete_button = btn = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(h, v),
|
||||
size=(90, 58.0 * scl),
|
||||
on_activate_call=self._delete_playlist,
|
||||
color=b_color,
|
||||
autoselect=True,
|
||||
textcolor=b_textcolor,
|
||||
button_type='square',
|
||||
text_scale=0.7,
|
||||
label=ba.Lstr(
|
||||
resource='deleteText', fallback_resource=self._r + '.deleteText'
|
||||
),
|
||||
)
|
||||
self._lock_images.append(
|
||||
ba.imagewidget(
|
||||
parent=self._root_widget,
|
||||
size=(30, 30),
|
||||
draw_controller=btn,
|
||||
position=(h - 10, v + 58.0 * scl - 28),
|
||||
texture=lock_tex,
|
||||
)
|
||||
)
|
||||
v -= 65.0 * scl
|
||||
self._import_button = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(h, v),
|
||||
size=(90, 58.0 * scl),
|
||||
on_activate_call=self._import_playlist,
|
||||
color=b_color,
|
||||
autoselect=True,
|
||||
textcolor=b_textcolor,
|
||||
button_type='square',
|
||||
text_scale=0.7,
|
||||
label=ba.Lstr(resource='importText'),
|
||||
)
|
||||
v -= 65.0 * scl
|
||||
btn = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(h, v),
|
||||
size=(90, 58.0 * scl),
|
||||
on_activate_call=self._share_playlist,
|
||||
color=b_color,
|
||||
autoselect=True,
|
||||
textcolor=b_textcolor,
|
||||
button_type='square',
|
||||
text_scale=0.7,
|
||||
label=ba.Lstr(resource='shareText'),
|
||||
)
|
||||
self._lock_images.append(
|
||||
ba.imagewidget(
|
||||
parent=self._root_widget,
|
||||
size=(30, 30),
|
||||
draw_controller=btn,
|
||||
position=(h - 10, v + 58.0 * scl - 28),
|
||||
texture=lock_tex,
|
||||
)
|
||||
)
|
||||
|
||||
v = self._height - 75
|
||||
self._scroll_height = self._height - 119
|
||||
scrollwidget = ba.scrollwidget(
|
||||
parent=self._root_widget,
|
||||
position=(140 + x_inset, v - self._scroll_height),
|
||||
size=(self._width - (180 + 2 * x_inset), self._scroll_height + 10),
|
||||
highlight=False,
|
||||
)
|
||||
ba.widget(edit=back_button, right_widget=scrollwidget)
|
||||
self._columnwidget = ba.columnwidget(
|
||||
parent=scrollwidget, border=2, margin=0
|
||||
)
|
||||
|
||||
h = 145
|
||||
|
||||
self._do_randomize_val = ba.app.config.get(
|
||||
self._pvars.config_name + ' Playlist Randomize', 0
|
||||
)
|
||||
|
||||
h += 210
|
||||
|
||||
for btn in [new_button, delete_button, edit_button, duplicate_button]:
|
||||
ba.widget(edit=btn, right_widget=scrollwidget)
|
||||
ba.widget(
|
||||
edit=scrollwidget,
|
||||
left_widget=new_button,
|
||||
right_widget=ba.internal.get_special_widget('party_button')
|
||||
if ba.app.ui.use_toolbars
|
||||
else None,
|
||||
)
|
||||
|
||||
# make sure config exists
|
||||
self._config_name_full = self._pvars.config_name + ' Playlists'
|
||||
|
||||
if self._config_name_full not in ba.app.config:
|
||||
ba.app.config[self._config_name_full] = {}
|
||||
|
||||
self._selected_playlist_name: str | None = None
|
||||
self._selected_playlist_index: int | None = None
|
||||
self._playlist_widgets: list[ba.Widget] = []
|
||||
|
||||
self._refresh(select_playlist=select_playlist)
|
||||
|
||||
ba.buttonwidget(edit=back_button, on_activate_call=self._back)
|
||||
ba.containerwidget(edit=self._root_widget, cancel_button=back_button)
|
||||
|
||||
ba.containerwidget(edit=self._root_widget, selected_child=scrollwidget)
|
||||
|
||||
# Keep our lock images up to date/etc.
|
||||
self._update_timer = ba.Timer(
|
||||
1.0,
|
||||
ba.WeakCall(self._update),
|
||||
timetype=ba.TimeType.REAL,
|
||||
repeat=True,
|
||||
)
|
||||
self._update()
|
||||
|
||||
def _update(self) -> None:
|
||||
have = ba.app.accounts_v1.have_pro_options()
|
||||
for lock in self._lock_images:
|
||||
ba.imagewidget(edit=lock, opacity=0.0 if have else 1.0)
|
||||
|
||||
def _back(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.playlist import browser
|
||||
|
||||
if self._selected_playlist_name is not None:
|
||||
cfg = ba.app.config
|
||||
cfg[
|
||||
self._pvars.config_name + ' Playlist Selection'
|
||||
] = self._selected_playlist_name
|
||||
cfg.commit()
|
||||
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget, transition=self._transition_out
|
||||
)
|
||||
ba.app.ui.set_main_menu_window(
|
||||
browser.PlaylistBrowserWindow(
|
||||
transition='in_left', sessiontype=self._sessiontype
|
||||
).get_root_widget()
|
||||
)
|
||||
|
||||
def _select(self, name: str, index: int) -> None:
|
||||
self._selected_playlist_name = name
|
||||
self._selected_playlist_index = index
|
||||
|
||||
def _run_selected_playlist(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
ba.internal.unlock_all_input()
|
||||
try:
|
||||
ba.internal.new_host_session(self._sessiontype)
|
||||
except Exception:
|
||||
from bastd import mainmenu
|
||||
|
||||
ba.print_exception(f'Error running session {self._sessiontype}.')
|
||||
|
||||
# Drop back into a main menu session.
|
||||
ba.internal.new_host_session(mainmenu.MainMenuSession)
|
||||
|
||||
def _choose_playlist(self) -> None:
|
||||
if self._selected_playlist_name is None:
|
||||
return
|
||||
self._save_playlist_selection()
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_left')
|
||||
ba.internal.fade_screen(False, endcall=self._run_selected_playlist)
|
||||
ba.internal.lock_all_input()
|
||||
|
||||
def _refresh(self, select_playlist: str | None = None) -> None:
|
||||
from efro.util import asserttype
|
||||
|
||||
old_selection = self._selected_playlist_name
|
||||
|
||||
# If there was no prev selection, look in prefs.
|
||||
if old_selection is None:
|
||||
old_selection = ba.app.config.get(
|
||||
self._pvars.config_name + ' Playlist Selection'
|
||||
)
|
||||
|
||||
old_selection_index = self._selected_playlist_index
|
||||
|
||||
# Delete old.
|
||||
while self._playlist_widgets:
|
||||
self._playlist_widgets.pop().delete()
|
||||
|
||||
items = list(ba.app.config[self._config_name_full].items())
|
||||
|
||||
# Make sure everything is unicode now.
|
||||
items = [
|
||||
(i[0].decode(), i[1]) if not isinstance(i[0], str) else i
|
||||
for i in items
|
||||
]
|
||||
|
||||
items.sort(key=lambda x: asserttype(x[0], str).lower())
|
||||
|
||||
items = [['__default__', None]] + items # Default is always first.
|
||||
index = 0
|
||||
for pname, _ in items:
|
||||
assert pname is not None
|
||||
txtw = ba.textwidget(
|
||||
parent=self._columnwidget,
|
||||
size=(self._width - 40, 30),
|
||||
maxwidth=self._width - 110,
|
||||
text=self._get_playlist_display_name(pname),
|
||||
h_align='left',
|
||||
v_align='center',
|
||||
color=(0.6, 0.6, 0.7, 1.0)
|
||||
if pname == '__default__'
|
||||
else (0.85, 0.85, 0.85, 1),
|
||||
always_highlight=True,
|
||||
on_select_call=ba.Call(self._select, pname, index),
|
||||
on_activate_call=ba.Call(self._edit_button.activate),
|
||||
selectable=True,
|
||||
)
|
||||
ba.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50)
|
||||
|
||||
# Hitting up from top widget should jump to 'back'
|
||||
if index == 0:
|
||||
ba.widget(edit=txtw, up_widget=self._back_button)
|
||||
|
||||
self._playlist_widgets.append(txtw)
|
||||
|
||||
# Select this one if the user requested it.
|
||||
if select_playlist is not None:
|
||||
if pname == select_playlist:
|
||||
ba.columnwidget(
|
||||
edit=self._columnwidget,
|
||||
selected_child=txtw,
|
||||
visible_child=txtw,
|
||||
)
|
||||
else:
|
||||
# Select this one if it was previously selected.
|
||||
# Go by index if there's one.
|
||||
if old_selection_index is not None:
|
||||
if index == old_selection_index:
|
||||
ba.columnwidget(
|
||||
edit=self._columnwidget,
|
||||
selected_child=txtw,
|
||||
visible_child=txtw,
|
||||
)
|
||||
else: # Otherwise look by name.
|
||||
if pname == old_selection:
|
||||
ba.columnwidget(
|
||||
edit=self._columnwidget,
|
||||
selected_child=txtw,
|
||||
visible_child=txtw,
|
||||
)
|
||||
|
||||
index += 1
|
||||
|
||||
def _save_playlist_selection(self) -> None:
|
||||
# Store the selected playlist in prefs.
|
||||
# This serves dual purposes of letting us re-select it next time
|
||||
# if we want and also lets us pass it to the game (since we reset
|
||||
# the whole python environment that's not actually easy).
|
||||
cfg = ba.app.config
|
||||
cfg[
|
||||
self._pvars.config_name + ' Playlist Selection'
|
||||
] = self._selected_playlist_name
|
||||
cfg[
|
||||
self._pvars.config_name + ' Playlist Randomize'
|
||||
] = self._do_randomize_val
|
||||
cfg.commit()
|
||||
|
||||
def _new_playlist(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.playlist.editcontroller import PlaylistEditController
|
||||
from bastd.ui.purchase import PurchaseWindow
|
||||
|
||||
if not ba.app.accounts_v1.have_pro_options():
|
||||
PurchaseWindow(items=['pro'])
|
||||
return
|
||||
|
||||
# Clamp at our max playlist number.
|
||||
if len(ba.app.config[self._config_name_full]) > self._max_playlists:
|
||||
ba.screenmessage(
|
||||
ba.Lstr(
|
||||
translate=(
|
||||
'serverResponses',
|
||||
'Max number of playlists reached.',
|
||||
)
|
||||
),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
|
||||
# In case they cancel so we can return to this state.
|
||||
self._save_playlist_selection()
|
||||
|
||||
# Kick off the edit UI.
|
||||
PlaylistEditController(sessiontype=self._sessiontype)
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_left')
|
||||
|
||||
def _edit_playlist(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.playlist.editcontroller import PlaylistEditController
|
||||
from bastd.ui.purchase import PurchaseWindow
|
||||
|
||||
if not ba.app.accounts_v1.have_pro_options():
|
||||
PurchaseWindow(items=['pro'])
|
||||
return
|
||||
if self._selected_playlist_name is None:
|
||||
return
|
||||
if self._selected_playlist_name == '__default__':
|
||||
ba.playsound(ba.getsound('error'))
|
||||
ba.screenmessage(ba.Lstr(resource=self._r + '.cantEditDefaultText'))
|
||||
return
|
||||
self._save_playlist_selection()
|
||||
PlaylistEditController(
|
||||
existing_playlist_name=self._selected_playlist_name,
|
||||
sessiontype=self._sessiontype,
|
||||
)
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_left')
|
||||
|
||||
def _do_delete_playlist(self) -> None:
|
||||
ba.internal.add_transaction(
|
||||
{
|
||||
'type': 'REMOVE_PLAYLIST',
|
||||
'playlistType': self._pvars.config_name,
|
||||
'playlistName': self._selected_playlist_name,
|
||||
}
|
||||
)
|
||||
ba.internal.run_transactions()
|
||||
ba.playsound(ba.getsound('shieldDown'))
|
||||
|
||||
# (we don't use len()-1 here because the default list adds one)
|
||||
assert self._selected_playlist_index is not None
|
||||
if self._selected_playlist_index > len(
|
||||
ba.app.config[self._pvars.config_name + ' Playlists']
|
||||
):
|
||||
self._selected_playlist_index = len(
|
||||
ba.app.config[self._pvars.config_name + ' Playlists']
|
||||
)
|
||||
self._refresh()
|
||||
|
||||
def _import_playlist(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.playlist import share
|
||||
|
||||
# Gotta be signed in for this to work.
|
||||
if ba.internal.get_v1_account_state() != 'signed_in':
|
||||
ba.screenmessage(
|
||||
ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)
|
||||
)
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
|
||||
share.SharePlaylistImportWindow(
|
||||
origin_widget=self._import_button,
|
||||
on_success_callback=ba.WeakCall(self._on_playlist_import_success),
|
||||
)
|
||||
|
||||
def _on_playlist_import_success(self) -> None:
|
||||
self._refresh()
|
||||
|
||||
def _on_share_playlist_response(self, name: str, response: Any) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.playlist import share
|
||||
|
||||
if response is None:
|
||||
ba.screenmessage(
|
||||
ba.Lstr(resource='internal.unavailableNoConnectionText'),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
share.SharePlaylistResultsWindow(name, response)
|
||||
|
||||
def _share_playlist(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.purchase import PurchaseWindow
|
||||
|
||||
if not ba.app.accounts_v1.have_pro_options():
|
||||
PurchaseWindow(items=['pro'])
|
||||
return
|
||||
|
||||
# Gotta be signed in for this to work.
|
||||
if ba.internal.get_v1_account_state() != 'signed_in':
|
||||
ba.screenmessage(
|
||||
ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)
|
||||
)
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
if self._selected_playlist_name == '__default__':
|
||||
ba.playsound(ba.getsound('error'))
|
||||
ba.screenmessage(
|
||||
ba.Lstr(resource=self._r + '.cantShareDefaultText'),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
return
|
||||
|
||||
if self._selected_playlist_name is None:
|
||||
return
|
||||
|
||||
ba.internal.add_transaction(
|
||||
{
|
||||
'type': 'SHARE_PLAYLIST',
|
||||
'expire_time': time.time() + 5,
|
||||
'playlistType': self._pvars.config_name,
|
||||
'playlistName': self._selected_playlist_name,
|
||||
},
|
||||
callback=ba.WeakCall(
|
||||
self._on_share_playlist_response, self._selected_playlist_name
|
||||
),
|
||||
)
|
||||
ba.internal.run_transactions()
|
||||
ba.screenmessage(ba.Lstr(resource='sharingText'))
|
||||
|
||||
def _delete_playlist(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.purchase import PurchaseWindow
|
||||
from bastd.ui.confirm import ConfirmWindow
|
||||
|
||||
if not ba.app.accounts_v1.have_pro_options():
|
||||
PurchaseWindow(items=['pro'])
|
||||
return
|
||||
|
||||
if self._selected_playlist_name is None:
|
||||
return
|
||||
if self._selected_playlist_name == '__default__':
|
||||
ba.playsound(ba.getsound('error'))
|
||||
ba.screenmessage(
|
||||
ba.Lstr(resource=self._r + '.cantDeleteDefaultText')
|
||||
)
|
||||
else:
|
||||
ConfirmWindow(
|
||||
ba.Lstr(
|
||||
resource=self._r + '.deleteConfirmText',
|
||||
subs=[('${LIST}', self._selected_playlist_name)],
|
||||
),
|
||||
self._do_delete_playlist,
|
||||
450,
|
||||
150,
|
||||
)
|
||||
|
||||
def _get_playlist_display_name(self, playlist: str) -> ba.Lstr:
|
||||
if playlist == '__default__':
|
||||
return self._pvars.default_list_name
|
||||
return (
|
||||
playlist
|
||||
if isinstance(playlist, ba.Lstr)
|
||||
else ba.Lstr(value=playlist)
|
||||
)
|
||||
|
||||
def _duplicate_playlist(self) -> None:
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.purchase import PurchaseWindow
|
||||
|
||||
if not ba.app.accounts_v1.have_pro_options():
|
||||
PurchaseWindow(items=['pro'])
|
||||
return
|
||||
if self._selected_playlist_name is None:
|
||||
return
|
||||
plst: list[dict[str, Any]] | None
|
||||
if self._selected_playlist_name == '__default__':
|
||||
plst = self._pvars.get_default_list_call()
|
||||
else:
|
||||
plst = ba.app.config[self._config_name_full].get(
|
||||
self._selected_playlist_name
|
||||
)
|
||||
if plst is None:
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
|
||||
# clamp at our max playlist number
|
||||
if len(ba.app.config[self._config_name_full]) > self._max_playlists:
|
||||
ba.screenmessage(
|
||||
ba.Lstr(
|
||||
translate=(
|
||||
'serverResponses',
|
||||
'Max number of playlists reached.',
|
||||
)
|
||||
),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
|
||||
copy_text = ba.Lstr(resource='copyOfText').evaluate()
|
||||
# get just 'Copy' or whatnot
|
||||
copy_word = copy_text.replace('${NAME}', '').strip()
|
||||
# find a valid dup name that doesn't exist
|
||||
|
||||
test_index = 1
|
||||
base_name = self._get_playlist_display_name(
|
||||
self._selected_playlist_name
|
||||
).evaluate()
|
||||
|
||||
# If it looks like a copy, strip digits and spaces off the end.
|
||||
if copy_word in base_name:
|
||||
while base_name[-1].isdigit() or base_name[-1] == ' ':
|
||||
base_name = base_name[:-1]
|
||||
while True:
|
||||
if copy_word in base_name:
|
||||
test_name = base_name
|
||||
else:
|
||||
test_name = copy_text.replace('${NAME}', base_name)
|
||||
if test_index > 1:
|
||||
test_name += ' ' + str(test_index)
|
||||
if test_name not in ba.app.config[self._config_name_full]:
|
||||
break
|
||||
test_index += 1
|
||||
|
||||
ba.internal.add_transaction(
|
||||
{
|
||||
'type': 'ADD_PLAYLIST',
|
||||
'playlistType': self._pvars.config_name,
|
||||
'playlistName': test_name,
|
||||
'playlist': copy.deepcopy(plst),
|
||||
}
|
||||
)
|
||||
ba.internal.run_transactions()
|
||||
|
||||
ba.playsound(ba.getsound('gunCocking'))
|
||||
self._refresh(select_playlist=test_name)
|
||||
467
dist/ba_data/python/bastd/ui/playlist/edit.py
vendored
Normal file
467
dist/ba_data/python/bastd/ui/playlist/edit.py
vendored
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides a window for editing individual game playlists."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
import ba
|
||||
import ba.internal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bastd.ui.playlist.editcontroller import PlaylistEditController
|
||||
|
||||
|
||||
class PlaylistEditWindow(ba.Window):
|
||||
"""Window for editing an individual game playlist."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
editcontroller: PlaylistEditController,
|
||||
transition: str = 'in_right',
|
||||
):
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-locals
|
||||
prev_selection: str | None
|
||||
self._editcontroller = editcontroller
|
||||
self._r = 'editGameListWindow'
|
||||
prev_selection = self._editcontroller.get_edit_ui_selection()
|
||||
|
||||
uiscale = ba.app.ui.uiscale
|
||||
self._width = 770 if uiscale is ba.UIScale.SMALL else 670
|
||||
x_inset = 50 if uiscale is ba.UIScale.SMALL else 0
|
||||
self._height = (
|
||||
400
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 470
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 540
|
||||
)
|
||||
|
||||
top_extra = 20 if uiscale is ba.UIScale.SMALL else 0
|
||||
super().__init__(
|
||||
root_widget=ba.containerwidget(
|
||||
size=(self._width, self._height + top_extra),
|
||||
transition=transition,
|
||||
scale=(
|
||||
2.0
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 1.3
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 1.0
|
||||
),
|
||||
stack_offset=(0, -16)
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else (0, 0),
|
||||
)
|
||||
)
|
||||
cancel_button = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(35 + x_inset, self._height - 60),
|
||||
scale=0.8,
|
||||
size=(175, 60),
|
||||
autoselect=True,
|
||||
label=ba.Lstr(resource='cancelText'),
|
||||
text_scale=1.2,
|
||||
)
|
||||
save_button = btn = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(self._width - (195 + x_inset), self._height - 60),
|
||||
scale=0.8,
|
||||
size=(190, 60),
|
||||
autoselect=True,
|
||||
left_widget=cancel_button,
|
||||
label=ba.Lstr(resource='saveText'),
|
||||
text_scale=1.2,
|
||||
)
|
||||
|
||||
if ba.app.ui.use_toolbars:
|
||||
ba.widget(
|
||||
edit=btn,
|
||||
right_widget=ba.internal.get_special_widget('party_button'),
|
||||
)
|
||||
|
||||
ba.widget(
|
||||
edit=cancel_button,
|
||||
left_widget=cancel_button,
|
||||
right_widget=save_button,
|
||||
)
|
||||
|
||||
ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
position=(-10, self._height - 50),
|
||||
size=(self._width, 25),
|
||||
text=ba.Lstr(resource=self._r + '.titleText'),
|
||||
color=ba.app.ui.title_color,
|
||||
scale=1.05,
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
maxwidth=270,
|
||||
)
|
||||
|
||||
v = self._height - 115.0
|
||||
|
||||
self._scroll_width = self._width - (205 + 2 * x_inset)
|
||||
|
||||
ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
text=ba.Lstr(resource=self._r + '.listNameText'),
|
||||
position=(196 + x_inset, v + 31),
|
||||
maxwidth=150,
|
||||
color=(0.8, 0.8, 0.8, 0.5),
|
||||
size=(0, 0),
|
||||
scale=0.75,
|
||||
h_align='right',
|
||||
v_align='center',
|
||||
)
|
||||
|
||||
self._text_field = ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
position=(210 + x_inset, v + 7),
|
||||
size=(self._scroll_width - 53, 43),
|
||||
text=self._editcontroller.getname(),
|
||||
h_align='left',
|
||||
v_align='center',
|
||||
max_chars=40,
|
||||
autoselect=True,
|
||||
color=(0.9, 0.9, 0.9, 1.0),
|
||||
description=ba.Lstr(resource=self._r + '.listNameText'),
|
||||
editable=True,
|
||||
padding=4,
|
||||
on_return_press_call=self._save_press_with_sound,
|
||||
)
|
||||
ba.widget(edit=cancel_button, down_widget=self._text_field)
|
||||
|
||||
self._list_widgets: list[ba.Widget] = []
|
||||
|
||||
h = 40 + x_inset
|
||||
v = self._height - 172.0
|
||||
|
||||
b_color = (0.6, 0.53, 0.63)
|
||||
b_textcolor = (0.75, 0.7, 0.8)
|
||||
|
||||
v -= 2.0
|
||||
v += 63
|
||||
|
||||
scl = (
|
||||
1.03
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 1.36
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 1.74
|
||||
)
|
||||
v -= 63.0 * scl
|
||||
|
||||
add_game_button = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(h, v),
|
||||
size=(110, 61.0 * scl),
|
||||
on_activate_call=self._add,
|
||||
on_select_call=ba.Call(self._set_ui_selection, 'add_button'),
|
||||
autoselect=True,
|
||||
button_type='square',
|
||||
color=b_color,
|
||||
textcolor=b_textcolor,
|
||||
text_scale=0.8,
|
||||
label=ba.Lstr(resource=self._r + '.addGameText'),
|
||||
)
|
||||
ba.widget(edit=add_game_button, up_widget=self._text_field)
|
||||
v -= 63.0 * scl
|
||||
|
||||
self._edit_button = edit_game_button = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(h, v),
|
||||
size=(110, 61.0 * scl),
|
||||
on_activate_call=self._edit,
|
||||
on_select_call=ba.Call(self._set_ui_selection, 'editButton'),
|
||||
autoselect=True,
|
||||
button_type='square',
|
||||
color=b_color,
|
||||
textcolor=b_textcolor,
|
||||
text_scale=0.8,
|
||||
label=ba.Lstr(resource=self._r + '.editGameText'),
|
||||
)
|
||||
v -= 63.0 * scl
|
||||
|
||||
remove_game_button = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(h, v),
|
||||
size=(110, 61.0 * scl),
|
||||
text_scale=0.8,
|
||||
on_activate_call=self._remove,
|
||||
autoselect=True,
|
||||
button_type='square',
|
||||
color=b_color,
|
||||
textcolor=b_textcolor,
|
||||
label=ba.Lstr(resource=self._r + '.removeGameText'),
|
||||
)
|
||||
v -= 40
|
||||
h += 9
|
||||
ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(h, v),
|
||||
size=(42, 35),
|
||||
on_activate_call=self._move_up,
|
||||
label=ba.charstr(ba.SpecialChar.UP_ARROW),
|
||||
button_type='square',
|
||||
color=b_color,
|
||||
textcolor=b_textcolor,
|
||||
autoselect=True,
|
||||
repeat=True,
|
||||
)
|
||||
h += 52
|
||||
ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(h, v),
|
||||
size=(42, 35),
|
||||
on_activate_call=self._move_down,
|
||||
autoselect=True,
|
||||
button_type='square',
|
||||
color=b_color,
|
||||
textcolor=b_textcolor,
|
||||
label=ba.charstr(ba.SpecialChar.DOWN_ARROW),
|
||||
repeat=True,
|
||||
)
|
||||
|
||||
v = self._height - 100
|
||||
scroll_height = self._height - 155
|
||||
scrollwidget = ba.scrollwidget(
|
||||
parent=self._root_widget,
|
||||
position=(160 + x_inset, v - scroll_height),
|
||||
highlight=False,
|
||||
on_select_call=ba.Call(self._set_ui_selection, 'gameList'),
|
||||
size=(self._scroll_width, (scroll_height - 15)),
|
||||
)
|
||||
ba.widget(
|
||||
edit=scrollwidget,
|
||||
left_widget=add_game_button,
|
||||
right_widget=scrollwidget,
|
||||
)
|
||||
self._columnwidget = ba.columnwidget(
|
||||
parent=scrollwidget, border=2, margin=0
|
||||
)
|
||||
ba.widget(edit=self._columnwidget, up_widget=self._text_field)
|
||||
|
||||
for button in [add_game_button, edit_game_button, remove_game_button]:
|
||||
ba.widget(
|
||||
edit=button, left_widget=button, right_widget=scrollwidget
|
||||
)
|
||||
|
||||
self._refresh()
|
||||
|
||||
ba.buttonwidget(edit=cancel_button, on_activate_call=self._cancel)
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget,
|
||||
cancel_button=cancel_button,
|
||||
selected_child=scrollwidget,
|
||||
)
|
||||
|
||||
ba.buttonwidget(edit=save_button, on_activate_call=self._save_press)
|
||||
ba.containerwidget(edit=self._root_widget, start_button=save_button)
|
||||
|
||||
if prev_selection == 'add_button':
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget, selected_child=add_game_button
|
||||
)
|
||||
elif prev_selection == 'editButton':
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget, selected_child=edit_game_button
|
||||
)
|
||||
elif prev_selection == 'gameList':
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget, selected_child=scrollwidget
|
||||
)
|
||||
|
||||
def _set_ui_selection(self, selection: str) -> None:
|
||||
self._editcontroller.set_edit_ui_selection(selection)
|
||||
|
||||
def _cancel(self) -> None:
|
||||
from bastd.ui.playlist.customizebrowser import (
|
||||
PlaylistCustomizeBrowserWindow,
|
||||
)
|
||||
|
||||
ba.playsound(ba.getsound('powerdown01'))
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_right')
|
||||
ba.app.ui.set_main_menu_window(
|
||||
PlaylistCustomizeBrowserWindow(
|
||||
transition='in_left',
|
||||
sessiontype=self._editcontroller.get_session_type(),
|
||||
select_playlist=(
|
||||
self._editcontroller.get_existing_playlist_name()
|
||||
),
|
||||
).get_root_widget()
|
||||
)
|
||||
|
||||
def _add(self) -> None:
|
||||
# Store list name then tell the session to perform an add.
|
||||
self._editcontroller.setname(
|
||||
cast(str, ba.textwidget(query=self._text_field))
|
||||
)
|
||||
self._editcontroller.add_game_pressed()
|
||||
|
||||
def _edit(self) -> None:
|
||||
# Store list name then tell the session to perform an add.
|
||||
self._editcontroller.setname(
|
||||
cast(str, ba.textwidget(query=self._text_field))
|
||||
)
|
||||
self._editcontroller.edit_game_pressed()
|
||||
|
||||
def _save_press(self) -> None:
|
||||
from bastd.ui.playlist.customizebrowser import (
|
||||
PlaylistCustomizeBrowserWindow,
|
||||
)
|
||||
|
||||
new_name = cast(str, ba.textwidget(query=self._text_field))
|
||||
if (
|
||||
new_name != self._editcontroller.get_existing_playlist_name()
|
||||
and new_name
|
||||
in ba.app.config[
|
||||
self._editcontroller.get_config_name() + ' Playlists'
|
||||
]
|
||||
):
|
||||
ba.screenmessage(
|
||||
ba.Lstr(resource=self._r + '.cantSaveAlreadyExistsText')
|
||||
)
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
if not new_name:
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
if not self._editcontroller.get_playlist():
|
||||
ba.screenmessage(
|
||||
ba.Lstr(resource=self._r + '.cantSaveEmptyListText')
|
||||
)
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
|
||||
# We couldn't actually replace the default list anyway, but disallow
|
||||
# using its exact name to avoid confusion.
|
||||
if new_name == self._editcontroller.get_default_list_name().evaluate():
|
||||
ba.screenmessage(
|
||||
ba.Lstr(resource=self._r + '.cantOverwriteDefaultText')
|
||||
)
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
|
||||
# If we had an old one, delete it.
|
||||
if self._editcontroller.get_existing_playlist_name() is not None:
|
||||
ba.internal.add_transaction(
|
||||
{
|
||||
'type': 'REMOVE_PLAYLIST',
|
||||
'playlistType': self._editcontroller.get_config_name(),
|
||||
'playlistName': (
|
||||
self._editcontroller.get_existing_playlist_name()
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
ba.internal.add_transaction(
|
||||
{
|
||||
'type': 'ADD_PLAYLIST',
|
||||
'playlistType': self._editcontroller.get_config_name(),
|
||||
'playlistName': new_name,
|
||||
'playlist': self._editcontroller.get_playlist(),
|
||||
}
|
||||
)
|
||||
ba.internal.run_transactions()
|
||||
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_right')
|
||||
ba.playsound(ba.getsound('gunCocking'))
|
||||
ba.app.ui.set_main_menu_window(
|
||||
PlaylistCustomizeBrowserWindow(
|
||||
transition='in_left',
|
||||
sessiontype=self._editcontroller.get_session_type(),
|
||||
select_playlist=new_name,
|
||||
).get_root_widget()
|
||||
)
|
||||
|
||||
def _save_press_with_sound(self) -> None:
|
||||
ba.playsound(ba.getsound('swish'))
|
||||
self._save_press()
|
||||
|
||||
def _select(self, index: int) -> None:
|
||||
self._editcontroller.set_selected_index(index)
|
||||
|
||||
def _refresh(self) -> None:
|
||||
from ba.internal import getclass
|
||||
|
||||
# Need to grab this here as rebuilding the list will
|
||||
# change it otherwise.
|
||||
old_selection_index = self._editcontroller.get_selected_index()
|
||||
|
||||
while self._list_widgets:
|
||||
self._list_widgets.pop().delete()
|
||||
for index, pentry in enumerate(self._editcontroller.get_playlist()):
|
||||
|
||||
try:
|
||||
cls = getclass(pentry['type'], subclassof=ba.GameActivity)
|
||||
desc = cls.get_settings_display_string(pentry)
|
||||
except Exception:
|
||||
ba.print_exception()
|
||||
desc = "(invalid: '" + pentry['type'] + "')"
|
||||
|
||||
txtw = ba.textwidget(
|
||||
parent=self._columnwidget,
|
||||
size=(self._width - 80, 30),
|
||||
on_select_call=ba.Call(self._select, index),
|
||||
always_highlight=True,
|
||||
color=(0.8, 0.8, 0.8, 1.0),
|
||||
padding=0,
|
||||
maxwidth=self._scroll_width * 0.93,
|
||||
text=desc,
|
||||
on_activate_call=self._edit_button.activate,
|
||||
v_align='center',
|
||||
selectable=True,
|
||||
)
|
||||
ba.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50)
|
||||
|
||||
# Wanna be able to jump up to the text field from the top one.
|
||||
if index == 0:
|
||||
ba.widget(edit=txtw, up_widget=self._text_field)
|
||||
self._list_widgets.append(txtw)
|
||||
if old_selection_index == index:
|
||||
ba.columnwidget(
|
||||
edit=self._columnwidget,
|
||||
selected_child=txtw,
|
||||
visible_child=txtw,
|
||||
)
|
||||
|
||||
def _move_down(self) -> None:
|
||||
playlist = self._editcontroller.get_playlist()
|
||||
index = self._editcontroller.get_selected_index()
|
||||
if index >= len(playlist) - 1:
|
||||
return
|
||||
tmp = playlist[index]
|
||||
playlist[index] = playlist[index + 1]
|
||||
playlist[index + 1] = tmp
|
||||
index += 1
|
||||
self._editcontroller.set_playlist(playlist)
|
||||
self._editcontroller.set_selected_index(index)
|
||||
self._refresh()
|
||||
|
||||
def _move_up(self) -> None:
|
||||
playlist = self._editcontroller.get_playlist()
|
||||
index = self._editcontroller.get_selected_index()
|
||||
if index < 1:
|
||||
return
|
||||
tmp = playlist[index]
|
||||
playlist[index] = playlist[index - 1]
|
||||
playlist[index - 1] = tmp
|
||||
index -= 1
|
||||
self._editcontroller.set_playlist(playlist)
|
||||
self._editcontroller.set_selected_index(index)
|
||||
self._refresh()
|
||||
|
||||
def _remove(self) -> None:
|
||||
playlist = self._editcontroller.get_playlist()
|
||||
index = self._editcontroller.get_selected_index()
|
||||
if not playlist:
|
||||
return
|
||||
del playlist[index]
|
||||
if index >= len(playlist):
|
||||
index = len(playlist) - 1
|
||||
self._editcontroller.set_playlist(playlist)
|
||||
self._editcontroller.set_selected_index(index)
|
||||
ba.playsound(ba.getsound('shieldDown'))
|
||||
self._refresh()
|
||||
236
dist/ba_data/python/bastd/ui/playlist/editcontroller.py
vendored
Normal file
236
dist/ba_data/python/bastd/ui/playlist/editcontroller.py
vendored
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Defines a controller for wrangling playlist edit UIs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
class PlaylistEditController:
|
||||
"""Coordinates various UIs involved in playlist editing."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sessiontype: type[ba.Session],
|
||||
existing_playlist_name: str | None = None,
|
||||
transition: str = 'in_right',
|
||||
playlist: list[dict[str, Any]] | None = None,
|
||||
playlist_name: str | None = None,
|
||||
):
|
||||
from ba.internal import preload_map_preview_media, filter_playlist
|
||||
from bastd.ui.playlist import PlaylistTypeVars
|
||||
from bastd.ui.playlist.edit import PlaylistEditWindow
|
||||
|
||||
appconfig = ba.app.config
|
||||
|
||||
# Since we may be showing our map list momentarily,
|
||||
# lets go ahead and preload all map preview textures.
|
||||
preload_map_preview_media()
|
||||
self._sessiontype = sessiontype
|
||||
|
||||
self._editing_game = False
|
||||
self._editing_game_type: type[ba.GameActivity] | None = None
|
||||
self._pvars = PlaylistTypeVars(sessiontype)
|
||||
self._existing_playlist_name = existing_playlist_name
|
||||
self._config_name_full = self._pvars.config_name + ' Playlists'
|
||||
|
||||
# Make sure config exists.
|
||||
if self._config_name_full not in appconfig:
|
||||
appconfig[self._config_name_full] = {}
|
||||
|
||||
self._selected_index = 0
|
||||
if existing_playlist_name:
|
||||
self._name = existing_playlist_name
|
||||
|
||||
# Filter out invalid games.
|
||||
self._playlist = filter_playlist(
|
||||
appconfig[self._pvars.config_name + ' Playlists'][
|
||||
existing_playlist_name
|
||||
],
|
||||
sessiontype=sessiontype,
|
||||
remove_unowned=False,
|
||||
name=existing_playlist_name,
|
||||
)
|
||||
self._edit_ui_selection = None
|
||||
else:
|
||||
if playlist is not None:
|
||||
self._playlist = playlist
|
||||
else:
|
||||
self._playlist = []
|
||||
if playlist_name is not None:
|
||||
self._name = playlist_name
|
||||
else:
|
||||
|
||||
# Find a good unused name.
|
||||
i = 1
|
||||
while True:
|
||||
self._name = (
|
||||
self._pvars.default_new_list_name.evaluate()
|
||||
+ ((' ' + str(i)) if i > 1 else '')
|
||||
)
|
||||
if (
|
||||
self._name
|
||||
not in appconfig[self._pvars.config_name + ' Playlists']
|
||||
):
|
||||
break
|
||||
i += 1
|
||||
|
||||
# Also we want it to start with 'add' highlighted since its empty
|
||||
# and that's all they can do.
|
||||
self._edit_ui_selection = 'add_button'
|
||||
|
||||
ba.app.ui.set_main_menu_window(
|
||||
PlaylistEditWindow(
|
||||
editcontroller=self, transition=transition
|
||||
).get_root_widget()
|
||||
)
|
||||
|
||||
def get_config_name(self) -> str:
|
||||
"""(internal)"""
|
||||
return self._pvars.config_name
|
||||
|
||||
def get_existing_playlist_name(self) -> str | None:
|
||||
"""(internal)"""
|
||||
return self._existing_playlist_name
|
||||
|
||||
def get_edit_ui_selection(self) -> str | None:
|
||||
"""(internal)"""
|
||||
return self._edit_ui_selection
|
||||
|
||||
def set_edit_ui_selection(self, selection: str) -> None:
|
||||
"""(internal)"""
|
||||
self._edit_ui_selection = selection
|
||||
|
||||
def getname(self) -> str:
|
||||
"""(internal)"""
|
||||
return self._name
|
||||
|
||||
def setname(self, name: str) -> None:
|
||||
"""(internal)"""
|
||||
self._name = name
|
||||
|
||||
def get_playlist(self) -> list[dict[str, Any]]:
|
||||
"""Return the current state of the edited playlist."""
|
||||
return copy.deepcopy(self._playlist)
|
||||
|
||||
def set_playlist(self, playlist: list[dict[str, Any]]) -> None:
|
||||
"""Set the playlist contents."""
|
||||
self._playlist = copy.deepcopy(playlist)
|
||||
|
||||
def get_session_type(self) -> type[ba.Session]:
|
||||
"""Return the ba.Session type for this edit-session."""
|
||||
return self._sessiontype
|
||||
|
||||
def get_selected_index(self) -> int:
|
||||
"""Return the index of the selected playlist."""
|
||||
return self._selected_index
|
||||
|
||||
def get_default_list_name(self) -> ba.Lstr:
|
||||
"""(internal)"""
|
||||
return self._pvars.default_list_name
|
||||
|
||||
def set_selected_index(self, index: int) -> None:
|
||||
"""Sets the selected playlist index."""
|
||||
self._selected_index = index
|
||||
|
||||
def add_game_pressed(self) -> None:
|
||||
"""(internal)"""
|
||||
from bastd.ui.playlist.addgame import PlaylistAddGameWindow
|
||||
|
||||
ba.app.ui.clear_main_menu_window(transition='out_left')
|
||||
ba.app.ui.set_main_menu_window(
|
||||
PlaylistAddGameWindow(editcontroller=self).get_root_widget()
|
||||
)
|
||||
|
||||
def edit_game_pressed(self) -> None:
|
||||
"""Should be called by supplemental UIs when a game is to be edited."""
|
||||
from ba.internal import getclass
|
||||
|
||||
if not self._playlist:
|
||||
return
|
||||
self._show_edit_ui(
|
||||
gametype=getclass(
|
||||
self._playlist[self._selected_index]['type'],
|
||||
subclassof=ba.GameActivity,
|
||||
),
|
||||
settings=self._playlist[self._selected_index],
|
||||
)
|
||||
|
||||
def add_game_cancelled(self) -> None:
|
||||
"""(internal)"""
|
||||
from bastd.ui.playlist.edit import PlaylistEditWindow
|
||||
|
||||
ba.app.ui.clear_main_menu_window(transition='out_right')
|
||||
ba.app.ui.set_main_menu_window(
|
||||
PlaylistEditWindow(
|
||||
editcontroller=self, transition='in_left'
|
||||
).get_root_widget()
|
||||
)
|
||||
|
||||
def _show_edit_ui(
|
||||
self, gametype: type[ba.GameActivity], settings: dict[str, Any] | None
|
||||
) -> None:
|
||||
self._editing_game = settings is not None
|
||||
self._editing_game_type = gametype
|
||||
assert self._sessiontype is not None
|
||||
gametype.create_settings_ui(
|
||||
self._sessiontype, copy.deepcopy(settings), self._edit_game_done
|
||||
)
|
||||
|
||||
def add_game_type_selected(self, gametype: type[ba.GameActivity]) -> None:
|
||||
"""(internal)"""
|
||||
self._show_edit_ui(gametype=gametype, settings=None)
|
||||
|
||||
def _edit_game_done(self, config: dict[str, Any] | None) -> None:
|
||||
from bastd.ui.playlist.edit import PlaylistEditWindow
|
||||
from bastd.ui.playlist.addgame import PlaylistAddGameWindow
|
||||
from ba.internal import get_type_name
|
||||
|
||||
if config is None:
|
||||
# If we were editing, go back to our list.
|
||||
if self._editing_game:
|
||||
ba.playsound(ba.getsound('powerdown01'))
|
||||
ba.app.ui.clear_main_menu_window(transition='out_right')
|
||||
ba.app.ui.set_main_menu_window(
|
||||
PlaylistEditWindow(
|
||||
editcontroller=self, transition='in_left'
|
||||
).get_root_widget()
|
||||
)
|
||||
|
||||
# Otherwise we were adding; go back to the add type choice list.
|
||||
else:
|
||||
ba.app.ui.clear_main_menu_window(transition='out_right')
|
||||
ba.app.ui.set_main_menu_window(
|
||||
PlaylistAddGameWindow(
|
||||
editcontroller=self, transition='in_left'
|
||||
).get_root_widget()
|
||||
)
|
||||
else:
|
||||
# Make sure type is in there.
|
||||
assert self._editing_game_type is not None
|
||||
config['type'] = get_type_name(self._editing_game_type)
|
||||
|
||||
if self._editing_game:
|
||||
self._playlist[self._selected_index] = copy.deepcopy(config)
|
||||
else:
|
||||
# Add a new entry to the playlist.
|
||||
insert_index = min(
|
||||
len(self._playlist), self._selected_index + 1
|
||||
)
|
||||
self._playlist.insert(insert_index, copy.deepcopy(config))
|
||||
self._selected_index = insert_index
|
||||
|
||||
ba.playsound(ba.getsound('gunCocking'))
|
||||
ba.app.ui.clear_main_menu_window(transition='out_right')
|
||||
ba.app.ui.set_main_menu_window(
|
||||
PlaylistEditWindow(
|
||||
editcontroller=self, transition='in_left'
|
||||
).get_root_widget()
|
||||
)
|
||||
595
dist/ba_data/python/bastd/ui/playlist/editgame.py
vendored
Normal file
595
dist/ba_data/python/bastd/ui/playlist/editgame.py
vendored
Normal file
|
|
@ -0,0 +1,595 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides UI for editing a game config."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import random
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
import ba
|
||||
import ba.internal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
class PlaylistEditGameWindow(ba.Window):
|
||||
"""Window for editing a game config."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
gametype: type[ba.GameActivity],
|
||||
sessiontype: type[ba.Session],
|
||||
config: dict[str, Any] | None,
|
||||
completion_call: Callable[[dict[str, Any] | None], Any],
|
||||
default_selection: str | None = None,
|
||||
transition: str = 'in_right',
|
||||
edit_info: dict[str, Any] | None = None,
|
||||
):
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-locals
|
||||
from ba.internal import (
|
||||
get_unowned_maps,
|
||||
get_filtered_map_name,
|
||||
get_map_class,
|
||||
get_map_display_string,
|
||||
)
|
||||
|
||||
self._gametype = gametype
|
||||
self._sessiontype = sessiontype
|
||||
|
||||
# If we're within an editing session we get passed edit_info
|
||||
# (returning from map selection window, etc).
|
||||
if edit_info is not None:
|
||||
self._edit_info = edit_info
|
||||
|
||||
# ..otherwise determine whether we're adding or editing a game based
|
||||
# on whether an existing config was passed to us.
|
||||
else:
|
||||
if config is None:
|
||||
self._edit_info = {'editType': 'add'}
|
||||
else:
|
||||
self._edit_info = {'editType': 'edit'}
|
||||
|
||||
self._r = 'gameSettingsWindow'
|
||||
|
||||
valid_maps = gametype.get_supported_maps(sessiontype)
|
||||
if not valid_maps:
|
||||
ba.screenmessage(ba.Lstr(resource='noValidMapsErrorText'))
|
||||
raise Exception('No valid maps')
|
||||
|
||||
self._settings_defs = gametype.get_available_settings(sessiontype)
|
||||
self._completion_call = completion_call
|
||||
|
||||
# To start with, pick a random map out of the ones we own.
|
||||
unowned_maps = get_unowned_maps()
|
||||
valid_maps_owned = [m for m in valid_maps if m not in unowned_maps]
|
||||
if valid_maps_owned:
|
||||
self._map = valid_maps[random.randrange(len(valid_maps_owned))]
|
||||
|
||||
# Hmmm.. we own none of these maps.. just pick a random un-owned one
|
||||
# I guess.. should this ever happen?
|
||||
else:
|
||||
self._map = valid_maps[random.randrange(len(valid_maps))]
|
||||
|
||||
is_add = self._edit_info['editType'] == 'add'
|
||||
|
||||
# If there's a valid map name in the existing config, use that.
|
||||
try:
|
||||
if (
|
||||
config is not None
|
||||
and 'settings' in config
|
||||
and 'map' in config['settings']
|
||||
):
|
||||
filtered_map_name = get_filtered_map_name(
|
||||
config['settings']['map']
|
||||
)
|
||||
if filtered_map_name in valid_maps:
|
||||
self._map = filtered_map_name
|
||||
except Exception:
|
||||
ba.print_exception('Error getting map for editor.')
|
||||
|
||||
if config is not None and 'settings' in config:
|
||||
self._settings = config['settings']
|
||||
else:
|
||||
self._settings = {}
|
||||
|
||||
self._choice_selections: dict[str, int] = {}
|
||||
|
||||
uiscale = ba.app.ui.uiscale
|
||||
width = 720 if uiscale is ba.UIScale.SMALL else 620
|
||||
x_inset = 50 if uiscale is ba.UIScale.SMALL else 0
|
||||
height = (
|
||||
365
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 460
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 550
|
||||
)
|
||||
spacing = 52
|
||||
y_extra = 15
|
||||
y_extra2 = 21
|
||||
|
||||
map_tex_name = get_map_class(self._map).get_preview_texture_name()
|
||||
if map_tex_name is None:
|
||||
raise Exception('no map preview tex found for' + self._map)
|
||||
map_tex = ba.gettexture(map_tex_name)
|
||||
|
||||
top_extra = 20 if uiscale is ba.UIScale.SMALL else 0
|
||||
super().__init__(
|
||||
root_widget=ba.containerwidget(
|
||||
size=(width, height + top_extra),
|
||||
transition=transition,
|
||||
scale=(
|
||||
2.19
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 1.35
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 1.0
|
||||
),
|
||||
stack_offset=(0, -17)
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else (0, 0),
|
||||
)
|
||||
)
|
||||
|
||||
btn = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(45 + x_inset, height - 82 + y_extra2),
|
||||
size=(180, 70) if is_add else (180, 65),
|
||||
label=ba.Lstr(resource='backText')
|
||||
if is_add
|
||||
else ba.Lstr(resource='cancelText'),
|
||||
button_type='back' if is_add else None,
|
||||
autoselect=True,
|
||||
scale=0.75,
|
||||
text_scale=1.3,
|
||||
on_activate_call=ba.Call(self._cancel),
|
||||
)
|
||||
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
|
||||
|
||||
add_button = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(width - (193 + x_inset), height - 82 + y_extra2),
|
||||
size=(200, 65),
|
||||
scale=0.75,
|
||||
text_scale=1.3,
|
||||
label=ba.Lstr(resource=self._r + '.addGameText')
|
||||
if is_add
|
||||
else ba.Lstr(resource='doneText'),
|
||||
)
|
||||
|
||||
if ba.app.ui.use_toolbars:
|
||||
pbtn = ba.internal.get_special_widget('party_button')
|
||||
ba.widget(edit=add_button, right_widget=pbtn, up_widget=pbtn)
|
||||
|
||||
ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
position=(-8, height - 70 + y_extra2),
|
||||
size=(width, 25),
|
||||
text=gametype.get_display_string(),
|
||||
color=ba.app.ui.title_color,
|
||||
maxwidth=235,
|
||||
scale=1.1,
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
)
|
||||
|
||||
map_height = 100
|
||||
|
||||
scroll_height = map_height + 10 # map select and margin
|
||||
|
||||
# Calc our total height we'll need
|
||||
scroll_height += spacing * len(self._settings_defs)
|
||||
|
||||
scroll_width = width - (86 + 2 * x_inset)
|
||||
self._scrollwidget = ba.scrollwidget(
|
||||
parent=self._root_widget,
|
||||
position=(44 + x_inset, 35 + y_extra),
|
||||
size=(scroll_width, height - 116),
|
||||
highlight=False,
|
||||
claims_left_right=True,
|
||||
claims_tab=True,
|
||||
selection_loops_to_parent=True,
|
||||
)
|
||||
self._subcontainer = ba.containerwidget(
|
||||
parent=self._scrollwidget,
|
||||
size=(scroll_width, scroll_height),
|
||||
background=False,
|
||||
claims_left_right=True,
|
||||
claims_tab=True,
|
||||
selection_loops_to_parent=True,
|
||||
)
|
||||
|
||||
v = scroll_height - 5
|
||||
h = -40
|
||||
|
||||
# Keep track of all the selectable widgets we make so we can wire
|
||||
# them up conveniently.
|
||||
widget_column: list[list[ba.Widget]] = []
|
||||
|
||||
# Map select button.
|
||||
ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(h + 49, v - 63),
|
||||
size=(100, 30),
|
||||
maxwidth=110,
|
||||
text=ba.Lstr(resource='mapText'),
|
||||
h_align='left',
|
||||
color=(0.8, 0.8, 0.8, 1.0),
|
||||
v_align='center',
|
||||
)
|
||||
|
||||
ba.imagewidget(
|
||||
parent=self._subcontainer,
|
||||
size=(256 * 0.7, 125 * 0.7),
|
||||
position=(h + 261 - 128 + 128.0 * 0.56, v - 90),
|
||||
texture=map_tex,
|
||||
model_opaque=ba.getmodel('level_select_button_opaque'),
|
||||
model_transparent=ba.getmodel('level_select_button_transparent'),
|
||||
mask_texture=ba.gettexture('mapPreviewMask'),
|
||||
)
|
||||
map_button = btn = ba.buttonwidget(
|
||||
parent=self._subcontainer,
|
||||
size=(140, 60),
|
||||
position=(h + 448, v - 72),
|
||||
on_activate_call=ba.Call(self._select_map),
|
||||
scale=0.7,
|
||||
label=ba.Lstr(resource='mapSelectText'),
|
||||
)
|
||||
widget_column.append([btn])
|
||||
|
||||
ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(h + 363 - 123, v - 114),
|
||||
size=(100, 30),
|
||||
flatness=1.0,
|
||||
shadow=1.0,
|
||||
scale=0.55,
|
||||
maxwidth=256 * 0.7 * 0.8,
|
||||
text=get_map_display_string(self._map),
|
||||
h_align='center',
|
||||
color=(0.6, 1.0, 0.6, 1.0),
|
||||
v_align='center',
|
||||
)
|
||||
v -= map_height
|
||||
|
||||
for setting in self._settings_defs:
|
||||
value = setting.default
|
||||
value_type = type(value)
|
||||
|
||||
# Now, if there's an existing value for it in the config,
|
||||
# override with that.
|
||||
try:
|
||||
if (
|
||||
config is not None
|
||||
and 'settings' in config
|
||||
and setting.name in config['settings']
|
||||
):
|
||||
value = value_type(config['settings'][setting.name])
|
||||
except Exception:
|
||||
ba.print_exception()
|
||||
|
||||
# Shove the starting value in there to start.
|
||||
self._settings[setting.name] = value
|
||||
|
||||
name_translated = self._get_localized_setting_name(setting.name)
|
||||
|
||||
mw1 = 280
|
||||
mw2 = 70
|
||||
|
||||
# Handle types with choices specially:
|
||||
if isinstance(setting, ba.ChoiceSetting):
|
||||
for choice in setting.choices:
|
||||
if len(choice) != 2:
|
||||
raise ValueError(
|
||||
"Expected 2-member tuples for 'choices'; got: "
|
||||
+ repr(choice)
|
||||
)
|
||||
if not isinstance(choice[0], str):
|
||||
raise TypeError(
|
||||
'First value for choice tuple must be a str; got: '
|
||||
+ repr(choice)
|
||||
)
|
||||
if not isinstance(choice[1], value_type):
|
||||
raise TypeError(
|
||||
'Choice type does not match default value; choice:'
|
||||
+ repr(choice)
|
||||
+ '; setting:'
|
||||
+ repr(setting)
|
||||
)
|
||||
if value_type not in (int, float):
|
||||
raise TypeError(
|
||||
'Choice type setting must have int or float default; '
|
||||
'got: ' + repr(setting)
|
||||
)
|
||||
|
||||
# Start at the choice corresponding to the default if possible.
|
||||
self._choice_selections[setting.name] = 0
|
||||
for index, choice in enumerate(setting.choices):
|
||||
if choice[1] == value:
|
||||
self._choice_selections[setting.name] = index
|
||||
break
|
||||
|
||||
v -= spacing
|
||||
ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(h + 50, v),
|
||||
size=(100, 30),
|
||||
maxwidth=mw1,
|
||||
text=name_translated,
|
||||
h_align='left',
|
||||
color=(0.8, 0.8, 0.8, 1.0),
|
||||
v_align='center',
|
||||
)
|
||||
txt = ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(h + 509 - 95, v),
|
||||
size=(0, 28),
|
||||
text=self._get_localized_setting_name(
|
||||
setting.choices[self._choice_selections[setting.name]][
|
||||
0
|
||||
]
|
||||
),
|
||||
editable=False,
|
||||
color=(0.6, 1.0, 0.6, 1.0),
|
||||
maxwidth=mw2,
|
||||
h_align='right',
|
||||
v_align='center',
|
||||
padding=2,
|
||||
)
|
||||
btn1 = ba.buttonwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(h + 509 - 50 - 1, v),
|
||||
size=(28, 28),
|
||||
label='<',
|
||||
autoselect=True,
|
||||
on_activate_call=ba.Call(
|
||||
self._choice_inc, setting.name, txt, setting, -1
|
||||
),
|
||||
repeat=True,
|
||||
)
|
||||
btn2 = ba.buttonwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(h + 509 + 5, v),
|
||||
size=(28, 28),
|
||||
label='>',
|
||||
autoselect=True,
|
||||
on_activate_call=ba.Call(
|
||||
self._choice_inc, setting.name, txt, setting, 1
|
||||
),
|
||||
repeat=True,
|
||||
)
|
||||
widget_column.append([btn1, btn2])
|
||||
|
||||
elif isinstance(setting, (ba.IntSetting, ba.FloatSetting)):
|
||||
v -= spacing
|
||||
min_value = setting.min_value
|
||||
max_value = setting.max_value
|
||||
increment = setting.increment
|
||||
ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(h + 50, v),
|
||||
size=(100, 30),
|
||||
text=name_translated,
|
||||
h_align='left',
|
||||
color=(0.8, 0.8, 0.8, 1.0),
|
||||
v_align='center',
|
||||
maxwidth=mw1,
|
||||
)
|
||||
txt = ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(h + 509 - 95, v),
|
||||
size=(0, 28),
|
||||
text=str(value),
|
||||
editable=False,
|
||||
color=(0.6, 1.0, 0.6, 1.0),
|
||||
maxwidth=mw2,
|
||||
h_align='right',
|
||||
v_align='center',
|
||||
padding=2,
|
||||
)
|
||||
btn1 = ba.buttonwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(h + 509 - 50 - 1, v),
|
||||
size=(28, 28),
|
||||
label='-',
|
||||
autoselect=True,
|
||||
on_activate_call=ba.Call(
|
||||
self._inc,
|
||||
txt,
|
||||
min_value,
|
||||
max_value,
|
||||
-increment,
|
||||
value_type,
|
||||
setting.name,
|
||||
),
|
||||
repeat=True,
|
||||
)
|
||||
btn2 = ba.buttonwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(h + 509 + 5, v),
|
||||
size=(28, 28),
|
||||
label='+',
|
||||
autoselect=True,
|
||||
on_activate_call=ba.Call(
|
||||
self._inc,
|
||||
txt,
|
||||
min_value,
|
||||
max_value,
|
||||
increment,
|
||||
value_type,
|
||||
setting.name,
|
||||
),
|
||||
repeat=True,
|
||||
)
|
||||
widget_column.append([btn1, btn2])
|
||||
|
||||
elif value_type == bool:
|
||||
v -= spacing
|
||||
ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(h + 50, v),
|
||||
size=(100, 30),
|
||||
text=name_translated,
|
||||
h_align='left',
|
||||
color=(0.8, 0.8, 0.8, 1.0),
|
||||
v_align='center',
|
||||
maxwidth=mw1,
|
||||
)
|
||||
txt = ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
position=(h + 509 - 95, v),
|
||||
size=(0, 28),
|
||||
text=ba.Lstr(resource='onText')
|
||||
if value
|
||||
else ba.Lstr(resource='offText'),
|
||||
editable=False,
|
||||
color=(0.6, 1.0, 0.6, 1.0),
|
||||
maxwidth=mw2,
|
||||
h_align='right',
|
||||
v_align='center',
|
||||
padding=2,
|
||||
)
|
||||
cbw = ba.checkboxwidget(
|
||||
parent=self._subcontainer,
|
||||
text='',
|
||||
position=(h + 505 - 50 - 5, v - 2),
|
||||
size=(200, 30),
|
||||
autoselect=True,
|
||||
textcolor=(0.8, 0.8, 0.8),
|
||||
value=value,
|
||||
on_value_change_call=ba.Call(
|
||||
self._check_value_change, setting.name, txt
|
||||
),
|
||||
)
|
||||
widget_column.append([cbw])
|
||||
|
||||
else:
|
||||
raise Exception()
|
||||
|
||||
# Ok now wire up the column.
|
||||
try:
|
||||
prev_widgets: list[ba.Widget] | None = None
|
||||
for cwdg in widget_column:
|
||||
if prev_widgets is not None:
|
||||
# Wire our rightmost to their rightmost.
|
||||
ba.widget(edit=prev_widgets[-1], down_widget=cwdg[-1])
|
||||
ba.widget(cwdg[-1], up_widget=prev_widgets[-1])
|
||||
|
||||
# Wire our leftmost to their leftmost.
|
||||
ba.widget(edit=prev_widgets[0], down_widget=cwdg[0])
|
||||
ba.widget(cwdg[0], up_widget=prev_widgets[0])
|
||||
prev_widgets = cwdg
|
||||
except Exception:
|
||||
ba.print_exception(
|
||||
'Error wiring up game-settings-select widget column.'
|
||||
)
|
||||
|
||||
ba.buttonwidget(edit=add_button, on_activate_call=ba.Call(self._add))
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget,
|
||||
selected_child=add_button,
|
||||
start_button=add_button,
|
||||
)
|
||||
|
||||
if default_selection == 'map':
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget, selected_child=self._scrollwidget
|
||||
)
|
||||
ba.containerwidget(
|
||||
edit=self._subcontainer, selected_child=map_button
|
||||
)
|
||||
|
||||
def _get_localized_setting_name(self, name: str) -> ba.Lstr:
|
||||
return ba.Lstr(translate=('settingNames', name))
|
||||
|
||||
def _select_map(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.playlist.mapselect import PlaylistMapSelectWindow
|
||||
|
||||
# Replace ourself with the map-select UI.
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_left')
|
||||
ba.app.ui.set_main_menu_window(
|
||||
PlaylistMapSelectWindow(
|
||||
self._gametype,
|
||||
self._sessiontype,
|
||||
copy.deepcopy(self._getconfig()),
|
||||
self._edit_info,
|
||||
self._completion_call,
|
||||
).get_root_widget()
|
||||
)
|
||||
|
||||
def _choice_inc(
|
||||
self,
|
||||
setting_name: str,
|
||||
widget: ba.Widget,
|
||||
setting: ba.ChoiceSetting,
|
||||
increment: int,
|
||||
) -> None:
|
||||
choices = setting.choices
|
||||
if increment > 0:
|
||||
self._choice_selections[setting_name] = min(
|
||||
len(choices) - 1, self._choice_selections[setting_name] + 1
|
||||
)
|
||||
else:
|
||||
self._choice_selections[setting_name] = max(
|
||||
0, self._choice_selections[setting_name] - 1
|
||||
)
|
||||
ba.textwidget(
|
||||
edit=widget,
|
||||
text=self._get_localized_setting_name(
|
||||
choices[self._choice_selections[setting_name]][0]
|
||||
),
|
||||
)
|
||||
self._settings[setting_name] = choices[
|
||||
self._choice_selections[setting_name]
|
||||
][1]
|
||||
|
||||
def _cancel(self) -> None:
|
||||
self._completion_call(None)
|
||||
|
||||
def _check_value_change(
|
||||
self, setting_name: str, widget: ba.Widget, value: int
|
||||
) -> None:
|
||||
ba.textwidget(
|
||||
edit=widget,
|
||||
text=ba.Lstr(resource='onText')
|
||||
if value
|
||||
else ba.Lstr(resource='offText'),
|
||||
)
|
||||
self._settings[setting_name] = value
|
||||
|
||||
def _getconfig(self) -> dict[str, Any]:
|
||||
settings = copy.deepcopy(self._settings)
|
||||
settings['map'] = self._map
|
||||
return {'settings': settings}
|
||||
|
||||
def _add(self) -> None:
|
||||
self._completion_call(copy.deepcopy(self._getconfig()))
|
||||
|
||||
def _inc(
|
||||
self,
|
||||
ctrl: ba.Widget,
|
||||
min_val: int | float,
|
||||
max_val: int | float,
|
||||
increment: int | float,
|
||||
setting_type: type,
|
||||
setting_name: str,
|
||||
) -> None:
|
||||
if setting_type == float:
|
||||
val = float(cast(str, ba.textwidget(query=ctrl)))
|
||||
else:
|
||||
val = int(cast(str, ba.textwidget(query=ctrl)))
|
||||
val += increment
|
||||
val = max(min_val, min(val, max_val))
|
||||
if setting_type == float:
|
||||
ba.textwidget(edit=ctrl, text=str(round(val, 2)))
|
||||
elif setting_type == int:
|
||||
ba.textwidget(edit=ctrl, text=str(int(val)))
|
||||
else:
|
||||
raise TypeError('invalid vartype: ' + str(setting_type))
|
||||
self._settings[setting_name] = val
|
||||
308
dist/ba_data/python/bastd/ui/playlist/mapselect.py
vendored
Normal file
308
dist/ba_data/python/bastd/ui/playlist/mapselect.py
vendored
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides UI for selecting maps in playlists."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import ba
|
||||
import ba.internal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
class PlaylistMapSelectWindow(ba.Window):
|
||||
"""Window to select a map."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
gametype: type[ba.GameActivity],
|
||||
sessiontype: type[ba.Session],
|
||||
config: dict[str, Any],
|
||||
edit_info: dict[str, Any],
|
||||
completion_call: Callable[[dict[str, Any] | None], Any],
|
||||
transition: str = 'in_right',
|
||||
):
|
||||
from ba.internal import get_filtered_map_name
|
||||
|
||||
self._gametype = gametype
|
||||
self._sessiontype = sessiontype
|
||||
self._config = config
|
||||
self._completion_call = completion_call
|
||||
self._edit_info = edit_info
|
||||
self._maps: list[tuple[str, ba.Texture]] = []
|
||||
try:
|
||||
self._previous_map = get_filtered_map_name(
|
||||
config['settings']['map']
|
||||
)
|
||||
except Exception:
|
||||
self._previous_map = ''
|
||||
|
||||
uiscale = ba.app.ui.uiscale
|
||||
width = 715 if uiscale is ba.UIScale.SMALL else 615
|
||||
x_inset = 50 if uiscale is ba.UIScale.SMALL else 0
|
||||
height = (
|
||||
400
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 480
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 600
|
||||
)
|
||||
|
||||
top_extra = 20 if uiscale is ba.UIScale.SMALL else 0
|
||||
super().__init__(
|
||||
root_widget=ba.containerwidget(
|
||||
size=(width, height + top_extra),
|
||||
transition=transition,
|
||||
scale=(
|
||||
2.17
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 1.3
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 1.0
|
||||
),
|
||||
stack_offset=(0, -27)
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else (0, 0),
|
||||
)
|
||||
)
|
||||
|
||||
self._cancel_button = btn = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(38 + x_inset, height - 67),
|
||||
size=(140, 50),
|
||||
scale=0.9,
|
||||
text_scale=1.0,
|
||||
autoselect=True,
|
||||
label=ba.Lstr(resource='cancelText'),
|
||||
on_activate_call=self._cancel,
|
||||
)
|
||||
|
||||
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
|
||||
ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
position=(width * 0.5, height - 46),
|
||||
size=(0, 0),
|
||||
maxwidth=260,
|
||||
scale=1.1,
|
||||
text=ba.Lstr(
|
||||
resource='mapSelectTitleText',
|
||||
subs=[('${GAME}', self._gametype.get_display_string())],
|
||||
),
|
||||
color=ba.app.ui.title_color,
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
)
|
||||
v = height - 70
|
||||
self._scroll_width = width - (80 + 2 * x_inset)
|
||||
self._scroll_height = height - 140
|
||||
|
||||
self._scrollwidget = ba.scrollwidget(
|
||||
parent=self._root_widget,
|
||||
position=(40 + x_inset, v - self._scroll_height),
|
||||
size=(self._scroll_width, self._scroll_height),
|
||||
)
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget, selected_child=self._scrollwidget
|
||||
)
|
||||
ba.containerwidget(edit=self._scrollwidget, claims_left_right=True)
|
||||
|
||||
self._subcontainer: ba.Widget | None = None
|
||||
self._refresh()
|
||||
|
||||
def _refresh(self, select_get_more_maps_button: bool = False) -> None:
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-locals
|
||||
from ba.internal import (
|
||||
get_unowned_maps,
|
||||
get_map_class,
|
||||
get_map_display_string,
|
||||
)
|
||||
|
||||
# Kill old.
|
||||
if self._subcontainer is not None:
|
||||
self._subcontainer.delete()
|
||||
|
||||
model_opaque = ba.getmodel('level_select_button_opaque')
|
||||
model_transparent = ba.getmodel('level_select_button_transparent')
|
||||
|
||||
self._maps = []
|
||||
map_list = self._gametype.get_supported_maps(self._sessiontype)
|
||||
map_list_sorted = list(map_list)
|
||||
map_list_sorted.sort()
|
||||
unowned_maps = get_unowned_maps()
|
||||
|
||||
for mapname in map_list_sorted:
|
||||
|
||||
# Disallow ones we don't own.
|
||||
if mapname in unowned_maps:
|
||||
continue
|
||||
map_tex_name = get_map_class(mapname).get_preview_texture_name()
|
||||
if map_tex_name is not None:
|
||||
try:
|
||||
map_tex = ba.gettexture(map_tex_name)
|
||||
self._maps.append((mapname, map_tex))
|
||||
except Exception:
|
||||
print(f'Invalid map preview texture: "{map_tex_name}".')
|
||||
else:
|
||||
print('Error: no map preview texture for map:', mapname)
|
||||
|
||||
count = len(self._maps)
|
||||
columns = 2
|
||||
rows = int(math.ceil(float(count) / columns))
|
||||
button_width = 220
|
||||
button_height = button_width * 0.5
|
||||
button_buffer_h = 16
|
||||
button_buffer_v = 19
|
||||
self._sub_width = self._scroll_width * 0.95
|
||||
self._sub_height = (
|
||||
5 + rows * (button_height + 2 * button_buffer_v) + 100
|
||||
)
|
||||
self._subcontainer = ba.containerwidget(
|
||||
parent=self._scrollwidget,
|
||||
size=(self._sub_width, self._sub_height),
|
||||
background=False,
|
||||
)
|
||||
index = 0
|
||||
mask_texture = ba.gettexture('mapPreviewMask')
|
||||
h_offs = 130 if len(self._maps) == 1 else 0
|
||||
for y in range(rows):
|
||||
for x in range(columns):
|
||||
pos = (
|
||||
x * (button_width + 2 * button_buffer_h)
|
||||
+ button_buffer_h
|
||||
+ h_offs,
|
||||
self._sub_height
|
||||
- (y + 1) * (button_height + 2 * button_buffer_v)
|
||||
+ 12,
|
||||
)
|
||||
btn = ba.buttonwidget(
|
||||
parent=self._subcontainer,
|
||||
button_type='square',
|
||||
size=(button_width, button_height),
|
||||
autoselect=True,
|
||||
texture=self._maps[index][1],
|
||||
mask_texture=mask_texture,
|
||||
model_opaque=model_opaque,
|
||||
model_transparent=model_transparent,
|
||||
label='',
|
||||
color=(1, 1, 1),
|
||||
on_activate_call=ba.Call(
|
||||
self._select_with_delay, self._maps[index][0]
|
||||
),
|
||||
position=pos,
|
||||
)
|
||||
if x == 0:
|
||||
ba.widget(edit=btn, left_widget=self._cancel_button)
|
||||
if y == 0:
|
||||
ba.widget(edit=btn, up_widget=self._cancel_button)
|
||||
if x == columns - 1 and ba.app.ui.use_toolbars:
|
||||
ba.widget(
|
||||
edit=btn,
|
||||
right_widget=ba.internal.get_special_widget(
|
||||
'party_button'
|
||||
),
|
||||
)
|
||||
|
||||
ba.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60)
|
||||
if self._maps[index][0] == self._previous_map:
|
||||
ba.containerwidget(
|
||||
edit=self._subcontainer,
|
||||
selected_child=btn,
|
||||
visible_child=btn,
|
||||
)
|
||||
name = get_map_display_string(self._maps[index][0])
|
||||
ba.textwidget(
|
||||
parent=self._subcontainer,
|
||||
text=name,
|
||||
position=(pos[0] + button_width * 0.5, pos[1] - 12),
|
||||
size=(0, 0),
|
||||
scale=0.5,
|
||||
maxwidth=button_width,
|
||||
draw_controller=btn,
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
color=(0.8, 0.8, 0.8, 0.8),
|
||||
)
|
||||
index += 1
|
||||
|
||||
if index >= count:
|
||||
break
|
||||
if index >= count:
|
||||
break
|
||||
self._get_more_maps_button = btn = ba.buttonwidget(
|
||||
parent=self._subcontainer,
|
||||
size=(self._sub_width * 0.8, 60),
|
||||
position=(self._sub_width * 0.1, 30),
|
||||
label=ba.Lstr(resource='mapSelectGetMoreMapsText'),
|
||||
on_activate_call=self._on_store_press,
|
||||
color=(0.6, 0.53, 0.63),
|
||||
textcolor=(0.75, 0.7, 0.8),
|
||||
autoselect=True,
|
||||
)
|
||||
ba.widget(edit=btn, show_buffer_top=30, show_buffer_bottom=30)
|
||||
if select_get_more_maps_button:
|
||||
ba.containerwidget(
|
||||
edit=self._subcontainer, selected_child=btn, visible_child=btn
|
||||
)
|
||||
|
||||
def _on_store_press(self) -> None:
|
||||
from bastd.ui import account
|
||||
from bastd.ui.store.browser import StoreBrowserWindow
|
||||
|
||||
if ba.internal.get_v1_account_state() != 'signed_in':
|
||||
account.show_sign_in_prompt()
|
||||
return
|
||||
StoreBrowserWindow(
|
||||
modal=True,
|
||||
show_tab=StoreBrowserWindow.TabID.MAPS,
|
||||
on_close_call=self._on_store_close,
|
||||
origin_widget=self._get_more_maps_button,
|
||||
)
|
||||
|
||||
def _on_store_close(self) -> None:
|
||||
self._refresh(select_get_more_maps_button=True)
|
||||
|
||||
def _select(self, map_name: str) -> None:
|
||||
from bastd.ui.playlist.editgame import PlaylistEditGameWindow
|
||||
|
||||
self._config['settings']['map'] = map_name
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_right')
|
||||
ba.app.ui.set_main_menu_window(
|
||||
PlaylistEditGameWindow(
|
||||
self._gametype,
|
||||
self._sessiontype,
|
||||
self._config,
|
||||
self._completion_call,
|
||||
default_selection='map',
|
||||
transition='in_left',
|
||||
edit_info=self._edit_info,
|
||||
).get_root_widget()
|
||||
)
|
||||
|
||||
def _select_with_delay(self, map_name: str) -> None:
|
||||
ba.internal.lock_all_input()
|
||||
ba.timer(0.1, ba.internal.unlock_all_input, timetype=ba.TimeType.REAL)
|
||||
ba.timer(
|
||||
0.1, ba.WeakCall(self._select, map_name), timetype=ba.TimeType.REAL
|
||||
)
|
||||
|
||||
def _cancel(self) -> None:
|
||||
from bastd.ui.playlist.editgame import PlaylistEditGameWindow
|
||||
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_right')
|
||||
ba.app.ui.set_main_menu_window(
|
||||
PlaylistEditGameWindow(
|
||||
self._gametype,
|
||||
self._sessiontype,
|
||||
self._config,
|
||||
self._completion_call,
|
||||
default_selection='map',
|
||||
transition='in_left',
|
||||
edit_info=self._edit_info,
|
||||
).get_root_widget()
|
||||
)
|
||||
159
dist/ba_data/python/bastd/ui/playlist/share.py
vendored
Normal file
159
dist/ba_data/python/bastd/ui/playlist/share.py
vendored
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""UI functionality for importing shared playlists."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import ba
|
||||
import ba.internal
|
||||
from bastd.ui import promocode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
class SharePlaylistImportWindow(promocode.PromoCodeWindow):
|
||||
"""Window for importing a shared playlist."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
origin_widget: ba.Widget | None = None,
|
||||
on_success_callback: Callable[[], Any] | None = None,
|
||||
):
|
||||
promocode.PromoCodeWindow.__init__(
|
||||
self, modal=True, origin_widget=origin_widget
|
||||
)
|
||||
self._on_success_callback = on_success_callback
|
||||
|
||||
def _on_import_response(self, response: dict[str, Any] | None) -> None:
|
||||
if response is None:
|
||||
ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0))
|
||||
ba.playsound(ba.getsound('error'))
|
||||
return
|
||||
|
||||
if response['playlistType'] == 'Team Tournament':
|
||||
playlist_type_name = ba.Lstr(resource='playModes.teamsText')
|
||||
elif response['playlistType'] == 'Free-for-All':
|
||||
playlist_type_name = ba.Lstr(resource='playModes.freeForAllText')
|
||||
else:
|
||||
playlist_type_name = ba.Lstr(value=response['playlistType'])
|
||||
|
||||
ba.screenmessage(
|
||||
ba.Lstr(
|
||||
resource='importPlaylistSuccessText',
|
||||
subs=[
|
||||
('${TYPE}', playlist_type_name),
|
||||
('${NAME}', response['playlistName']),
|
||||
],
|
||||
),
|
||||
color=(0, 1, 0),
|
||||
)
|
||||
ba.playsound(ba.getsound('gunCocking'))
|
||||
if self._on_success_callback is not None:
|
||||
self._on_success_callback()
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget, transition=self._transition_out
|
||||
)
|
||||
|
||||
def _do_enter(self) -> None:
|
||||
ba.internal.add_transaction(
|
||||
{
|
||||
'type': 'IMPORT_PLAYLIST',
|
||||
'expire_time': time.time() + 5,
|
||||
'code': ba.textwidget(query=self._text_field),
|
||||
},
|
||||
callback=ba.WeakCall(self._on_import_response),
|
||||
)
|
||||
ba.internal.run_transactions()
|
||||
ba.screenmessage(ba.Lstr(resource='importingText'))
|
||||
|
||||
|
||||
class SharePlaylistResultsWindow(ba.Window):
|
||||
"""Window for sharing playlists."""
|
||||
|
||||
def __init__(
|
||||
self, name: str, data: str, origin: tuple[float, float] = (0.0, 0.0)
|
||||
):
|
||||
del origin # unused arg
|
||||
self._width = 450
|
||||
self._height = 300
|
||||
uiscale = ba.app.ui.uiscale
|
||||
super().__init__(
|
||||
root_widget=ba.containerwidget(
|
||||
size=(self._width, self._height),
|
||||
color=(0.45, 0.63, 0.15),
|
||||
transition='in_scale',
|
||||
scale=(
|
||||
1.8
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 1.35
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 1.0
|
||||
),
|
||||
)
|
||||
)
|
||||
ba.playsound(ba.getsound('cashRegister'))
|
||||
ba.playsound(ba.getsound('swish'))
|
||||
|
||||
self._cancel_button = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
scale=0.7,
|
||||
position=(40, self._height - 40),
|
||||
size=(50, 50),
|
||||
label='',
|
||||
on_activate_call=self.close,
|
||||
autoselect=True,
|
||||
color=(0.45, 0.63, 0.15),
|
||||
icon=ba.gettexture('crossOut'),
|
||||
iconscale=1.2,
|
||||
)
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget, cancel_button=self._cancel_button
|
||||
)
|
||||
|
||||
ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
position=(self._width * 0.5, self._height * 0.745),
|
||||
size=(0, 0),
|
||||
color=ba.app.ui.infotextcolor,
|
||||
scale=1.0,
|
||||
flatness=1.0,
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
text=ba.Lstr(
|
||||
resource='exportSuccessText', subs=[('${NAME}', name)]
|
||||
),
|
||||
maxwidth=self._width * 0.85,
|
||||
)
|
||||
|
||||
ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
position=(self._width * 0.5, self._height * 0.645),
|
||||
size=(0, 0),
|
||||
color=ba.app.ui.infotextcolor,
|
||||
scale=0.6,
|
||||
flatness=1.0,
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
text=ba.Lstr(resource='importPlaylistCodeInstructionsText'),
|
||||
maxwidth=self._width * 0.85,
|
||||
)
|
||||
|
||||
ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
position=(self._width * 0.5, self._height * 0.4),
|
||||
size=(0, 0),
|
||||
color=(1.0, 3.0, 1.0),
|
||||
scale=2.3,
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
text=data,
|
||||
maxwidth=self._width * 0.85,
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the window."""
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_scale')
|
||||
Loading…
Add table
Add a link
Reference in a new issue