mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-22 23:15:49 +08:00
610 lines
21 KiB
Python
610 lines
21 KiB
Python
# Released under the MIT License. See LICENSE for details.
|
|
#
|
|
"""Implements the main menu window."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, override
|
|
import logging
|
|
|
|
import bauiv1 as bui
|
|
import bascenev1 as bs
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import Any, Callable
|
|
|
|
|
|
class MainMenuWindow(bui.MainWindow):
|
|
"""The main menu window."""
|
|
|
|
def __init__(
|
|
self,
|
|
transition: str | None = 'in_right',
|
|
origin_widget: bui.Widget | None = None,
|
|
):
|
|
|
|
# Preload some modules we use in a background thread so we won't
|
|
# have a visual hitch when the user taps them.
|
|
bui.app.threadpool_submit_no_wait(self._preload_modules)
|
|
|
|
bui.set_analytics_screen('Main Menu')
|
|
self._show_remote_app_info_on_first_launch()
|
|
|
|
# Make a vanilla container; we'll modify it to our needs in
|
|
# refresh.
|
|
super().__init__(
|
|
root_widget=bui.containerwidget(
|
|
toolbar_visibility=('menu_full_no_back')
|
|
),
|
|
transition=transition,
|
|
origin_widget=origin_widget,
|
|
)
|
|
|
|
# Grab this stuff in case it changes.
|
|
self._is_demo = bui.app.env.demo
|
|
self._is_arcade = bui.app.env.arcade
|
|
|
|
self._tdelay = 0.0
|
|
self._t_delay_inc = 0.02
|
|
self._t_delay_play = 1.7
|
|
self._use_autoselect = True
|
|
self._button_width = 200.0
|
|
self._button_height = 45.0
|
|
self._width = 100.0
|
|
self._height = 100.0
|
|
self._demo_menu_button: bui.Widget | None = None
|
|
self._gather_button: bui.Widget | None = None
|
|
self._play_button: bui.Widget | None = None
|
|
self._watch_button: bui.Widget | None = None
|
|
self._how_to_play_button: bui.Widget | None = None
|
|
self._credits_button: bui.Widget | None = None
|
|
|
|
self._refresh()
|
|
|
|
self._restore_state()
|
|
|
|
@override
|
|
def on_main_window_close(self) -> None:
|
|
self._save_state()
|
|
|
|
@override
|
|
def get_main_window_state(self) -> bui.MainWindowState:
|
|
# Support recreating our window for back/refresh purposes.
|
|
return self.do_get_main_window_state()
|
|
|
|
@classmethod
|
|
def do_get_main_window_state(cls) -> bui.MainWindowState:
|
|
"""Classmethod to gen a windowstate for the main menu."""
|
|
return bui.BasicMainWindowState(
|
|
create_call=lambda transition, origin_widget: cls(
|
|
transition=transition, origin_widget=origin_widget
|
|
)
|
|
)
|
|
|
|
@staticmethod
|
|
def _preload_modules() -> None:
|
|
"""Preload modules we use; avoids hitches (called in bg thread)."""
|
|
# pylint: disable=cyclic-import
|
|
import bauiv1lib.getremote as _unused
|
|
import bauiv1lib.confirm as _unused2
|
|
import bauiv1lib.store.button as _unused3
|
|
import bauiv1lib.account.settings as _unused5
|
|
import bauiv1lib.store.browser as _unused6
|
|
import bauiv1lib.credits as _unused7
|
|
import bauiv1lib.helpui as _unused8
|
|
import bauiv1lib.settings.allsettings as _unused9
|
|
import bauiv1lib.gather as _unused10
|
|
import bauiv1lib.watch as _unused11
|
|
import bauiv1lib.play as _unused12
|
|
|
|
def _show_remote_app_info_on_first_launch(self) -> None:
|
|
app = bui.app
|
|
assert app.classic is not None
|
|
# The first time the non-in-game menu pops up, we might wanna
|
|
# show a 'get-remote-app' dialog in front of it.
|
|
if app.classic.first_main_menu:
|
|
app.classic.first_main_menu = False
|
|
try:
|
|
force_test = False
|
|
bs.get_local_active_input_devices_count()
|
|
if (
|
|
(app.env.tv or app.classic.platform == 'mac')
|
|
and bui.app.config.get('launchCount', 0) <= 1
|
|
) or force_test:
|
|
|
|
def _check_show_bs_remote_window() -> None:
|
|
try:
|
|
from bauiv1lib.getremote import GetBSRemoteWindow
|
|
|
|
bui.getsound('swish').play()
|
|
GetBSRemoteWindow()
|
|
except Exception:
|
|
logging.exception(
|
|
'Error showing get-remote window.'
|
|
)
|
|
|
|
bui.apptimer(2.5, _check_show_bs_remote_window)
|
|
except Exception:
|
|
logging.exception('Error showing get-remote-app info.')
|
|
|
|
def get_play_button(self) -> bui.Widget | None:
|
|
"""Return the play button."""
|
|
return self._play_button
|
|
|
|
def _refresh(self) -> None:
|
|
# pylint: disable=too-many-statements
|
|
# pylint: disable=too-many-locals
|
|
|
|
classic = bui.app.classic
|
|
assert classic is not None
|
|
|
|
# Clear everything that was there.
|
|
children = self._root_widget.get_children()
|
|
for child in children:
|
|
child.delete()
|
|
|
|
self._tdelay = 0.0
|
|
self._t_delay_inc = 0.0
|
|
self._t_delay_play = 0.0
|
|
self._button_width = 200.0
|
|
self._button_height = 45.0
|
|
|
|
self._r = 'mainMenu'
|
|
|
|
app = bui.app
|
|
assert app.classic is not None
|
|
uiscale = app.ui_v1.uiscale
|
|
|
|
# Temp note about UI changes.
|
|
bui.textwidget(
|
|
parent=self._root_widget,
|
|
position=(
|
|
(-400, 400)
|
|
if uiscale is bui.UIScale.LARGE
|
|
else (
|
|
(-270, 320)
|
|
if uiscale is bui.UIScale.MEDIUM
|
|
else (-280, 280)
|
|
)
|
|
),
|
|
size=(0, 0),
|
|
scale=0.4,
|
|
flatness=1.0,
|
|
text=(
|
|
'WARNING: This build contains a revamped UI\n'
|
|
'which is still a work-in-progress. A number\n'
|
|
'of features are not currently functional or\n'
|
|
'contain bugs. To go back to the stable legacy UI,\n'
|
|
'grab version 1.7.36 from ballistica.net'
|
|
),
|
|
h_align='left',
|
|
v_align='top',
|
|
)
|
|
|
|
self._have_quit_button = app.classic.platform in (
|
|
'windows',
|
|
'mac',
|
|
'linux',
|
|
)
|
|
|
|
if not classic.did_menu_intro:
|
|
self._tdelay = 1.7
|
|
self._t_delay_inc = 0.05
|
|
self._t_delay_play = 1.7
|
|
classic.did_menu_intro = True
|
|
|
|
self._width = 400.0
|
|
self._height = 200.0
|
|
|
|
play_button_width = self._button_width * 0.65
|
|
play_button_height = self._button_height * 1.1
|
|
play_button_scale = 1.7
|
|
hspace = 20.0
|
|
side_button_width = self._button_width * 0.4
|
|
side_button_height = side_button_width
|
|
side_button_scale = 0.95
|
|
side_button_y_offs = 5.0
|
|
hspace2 = 15.0
|
|
side_button_2_width = self._button_width * 1.0
|
|
side_button_2_height = side_button_2_width * 0.3
|
|
side_button_2_y_offs = 10.0
|
|
side_button_2_scale = 0.5
|
|
|
|
if uiscale is bui.UIScale.SMALL:
|
|
root_widget_scale = 1.3
|
|
button_y_offs = -20.0
|
|
self._button_height *= 1.3
|
|
elif uiscale is bui.UIScale.MEDIUM:
|
|
root_widget_scale = 1.3
|
|
button_y_offs = -55.0
|
|
self._button_height *= 1.25
|
|
else:
|
|
root_widget_scale = 1.0
|
|
button_y_offs = -90.0
|
|
self._button_height *= 1.2
|
|
|
|
bui.containerwidget(
|
|
edit=self._root_widget,
|
|
size=(self._width, self._height),
|
|
background=False,
|
|
scale=root_widget_scale,
|
|
)
|
|
|
|
# Version/copyright info.
|
|
bui.textwidget(
|
|
parent=self._root_widget,
|
|
position=(self._width * 0.5, button_y_offs - 10),
|
|
size=(0, 0),
|
|
scale=0.4,
|
|
flatness=1.0,
|
|
color=(1, 1, 1, 0.3),
|
|
text=(
|
|
f'{app.env.engine_version}'
|
|
f' build {app.env.engine_build_number}.'
|
|
f' Copyright 2024 Eric Froemling.'
|
|
),
|
|
h_align='center',
|
|
v_align='center',
|
|
transition_delay=self._t_delay_play,
|
|
)
|
|
|
|
# In kiosk mode, provide a button to get back to the kiosk menu.
|
|
if bui.app.env.demo or bui.app.env.arcade:
|
|
# h, v, scale = positions[self._p_index]
|
|
h = self._width * 0.5
|
|
v = button_y_offs
|
|
scale = 1.0
|
|
this_b_width = self._button_width * 0.4 * scale
|
|
demo_menu_delay = (
|
|
0.0
|
|
if self._t_delay_play == 0.0
|
|
else max(0, self._t_delay_play + 0.1)
|
|
)
|
|
self._demo_menu_button = bui.buttonwidget(
|
|
parent=self._root_widget,
|
|
position=(self._width * 0.5 - this_b_width * 0.5, v + 90),
|
|
size=(this_b_width, 45),
|
|
autoselect=True,
|
|
color=(0.45, 0.55, 0.45),
|
|
textcolor=(0.7, 0.8, 0.7),
|
|
label=bui.Lstr(
|
|
resource=(
|
|
'modeArcadeText'
|
|
if bui.app.env.arcade
|
|
else 'modeDemoText'
|
|
)
|
|
),
|
|
transition_delay=demo_menu_delay,
|
|
on_activate_call=self.main_window_back,
|
|
)
|
|
else:
|
|
self._demo_menu_button = None
|
|
|
|
# Gather button
|
|
h = self._width * 0.5
|
|
h = (
|
|
self._width * 0.5
|
|
- play_button_width * play_button_scale * 0.5
|
|
- hspace
|
|
- side_button_width * side_button_scale * 0.5
|
|
)
|
|
v = button_y_offs + side_button_y_offs
|
|
self._gather_button = btn = bui.buttonwidget(
|
|
parent=self._root_widget,
|
|
position=(h - side_button_width * side_button_scale * 0.5, v),
|
|
size=(side_button_width, side_button_height),
|
|
scale=side_button_scale,
|
|
autoselect=self._use_autoselect,
|
|
button_type='square',
|
|
label='',
|
|
transition_delay=self._tdelay,
|
|
on_activate_call=self._gather_press,
|
|
)
|
|
bui.textwidget(
|
|
parent=self._root_widget,
|
|
position=(h, v + side_button_height * side_button_scale * 0.25),
|
|
size=(0, 0),
|
|
scale=0.75,
|
|
transition_delay=self._tdelay,
|
|
draw_controller=btn,
|
|
color=(0.75, 1.0, 0.7),
|
|
maxwidth=side_button_width * side_button_scale * 0.8,
|
|
text=bui.Lstr(resource='gatherWindow.titleText'),
|
|
h_align='center',
|
|
v_align='center',
|
|
)
|
|
icon_size = side_button_width * side_button_scale * 0.63
|
|
bui.imagewidget(
|
|
parent=self._root_widget,
|
|
size=(icon_size, icon_size),
|
|
draw_controller=btn,
|
|
transition_delay=self._tdelay,
|
|
position=(
|
|
h - 0.5 * icon_size,
|
|
v
|
|
+ 0.65 * side_button_height * side_button_scale
|
|
- 0.5 * icon_size,
|
|
),
|
|
texture=bui.gettexture('usersButton'),
|
|
)
|
|
|
|
h -= (
|
|
side_button_width * side_button_scale * 0.5
|
|
+ hspace2
|
|
+ side_button_2_width * side_button_2_scale
|
|
)
|
|
v = button_y_offs + side_button_2_y_offs
|
|
|
|
btn = bui.buttonwidget(
|
|
parent=self._root_widget,
|
|
position=(h, v),
|
|
autoselect=self._use_autoselect,
|
|
size=(side_button_2_width, side_button_2_height * 2.0),
|
|
button_type='square',
|
|
scale=side_button_2_scale,
|
|
label=bui.Lstr(resource=f'{self._r}.howToPlayText'),
|
|
transition_delay=self._tdelay,
|
|
on_activate_call=self._howtoplay,
|
|
)
|
|
self._how_to_play_button = btn
|
|
|
|
# Play button.
|
|
h = self._width * 0.5
|
|
v = button_y_offs
|
|
assert play_button_width is not None
|
|
assert play_button_height is not None
|
|
self._play_button = start_button = bui.buttonwidget(
|
|
parent=self._root_widget,
|
|
position=(h - play_button_width * 0.5 * play_button_scale, v),
|
|
size=(play_button_width, play_button_height),
|
|
autoselect=self._use_autoselect,
|
|
scale=play_button_scale,
|
|
text_res_scale=2.0,
|
|
label=bui.Lstr(resource='playText'),
|
|
transition_delay=self._t_delay_play,
|
|
on_activate_call=self._play_press,
|
|
)
|
|
bui.containerwidget(
|
|
edit=self._root_widget,
|
|
start_button=start_button,
|
|
selected_child=start_button,
|
|
)
|
|
|
|
self._tdelay += self._t_delay_inc
|
|
|
|
h = (
|
|
self._width * 0.5
|
|
+ play_button_width * play_button_scale * 0.5
|
|
+ hspace
|
|
+ side_button_width * side_button_scale * 0.5
|
|
)
|
|
v = button_y_offs + side_button_y_offs
|
|
self._watch_button = btn = bui.buttonwidget(
|
|
parent=self._root_widget,
|
|
position=(h - side_button_width * side_button_scale * 0.5, v),
|
|
size=(side_button_width, side_button_height),
|
|
scale=side_button_scale,
|
|
autoselect=self._use_autoselect,
|
|
button_type='square',
|
|
label='',
|
|
transition_delay=self._tdelay,
|
|
on_activate_call=self._watch_press,
|
|
)
|
|
bui.textwidget(
|
|
parent=self._root_widget,
|
|
position=(h, v + side_button_height * side_button_scale * 0.25),
|
|
size=(0, 0),
|
|
scale=0.75,
|
|
transition_delay=self._tdelay,
|
|
color=(0.75, 1.0, 0.7),
|
|
draw_controller=btn,
|
|
maxwidth=side_button_width * side_button_scale * 0.8,
|
|
text=bui.Lstr(resource='watchWindow.titleText'),
|
|
h_align='center',
|
|
v_align='center',
|
|
)
|
|
icon_size = side_button_width * side_button_scale * 0.63
|
|
bui.imagewidget(
|
|
parent=self._root_widget,
|
|
size=(icon_size, icon_size),
|
|
draw_controller=btn,
|
|
transition_delay=self._tdelay,
|
|
position=(
|
|
h - 0.5 * icon_size,
|
|
v
|
|
+ 0.65 * side_button_height * side_button_scale
|
|
- 0.5 * icon_size,
|
|
),
|
|
texture=bui.gettexture('tv'),
|
|
)
|
|
|
|
# Credits button.
|
|
self._tdelay += self._t_delay_inc
|
|
|
|
h += side_button_width * side_button_scale * 0.5 + hspace2
|
|
v = button_y_offs + side_button_2_y_offs
|
|
|
|
if self._have_quit_button:
|
|
v += 1.17 * side_button_2_height * side_button_2_scale
|
|
|
|
self._credits_button = bui.buttonwidget(
|
|
parent=self._root_widget,
|
|
position=(h, v),
|
|
button_type=None if self._have_quit_button else 'square',
|
|
size=(
|
|
side_button_2_width,
|
|
side_button_2_height * (1.0 if self._have_quit_button else 2.0),
|
|
),
|
|
scale=side_button_2_scale,
|
|
autoselect=self._use_autoselect,
|
|
label=bui.Lstr(resource=f'{self._r}.creditsText'),
|
|
transition_delay=self._tdelay,
|
|
on_activate_call=self._credits,
|
|
)
|
|
self._tdelay += self._t_delay_inc
|
|
|
|
self._quit_button: bui.Widget | None
|
|
if self._have_quit_button:
|
|
v -= 1.1 * side_button_2_height * side_button_2_scale
|
|
self._quit_button = quit_button = bui.buttonwidget(
|
|
parent=self._root_widget,
|
|
autoselect=self._use_autoselect,
|
|
position=(h, v),
|
|
size=(side_button_2_width, side_button_2_height),
|
|
scale=side_button_2_scale,
|
|
label=bui.Lstr(
|
|
resource=self._r
|
|
+ (
|
|
'.quitText'
|
|
if 'Mac' in app.classic.legacy_user_agent_string
|
|
else '.exitGameText'
|
|
)
|
|
),
|
|
on_activate_call=self._quit,
|
|
transition_delay=self._tdelay,
|
|
)
|
|
|
|
bui.containerwidget(
|
|
edit=self._root_widget, cancel_button=quit_button
|
|
)
|
|
self._tdelay += self._t_delay_inc
|
|
else:
|
|
self._quit_button = None
|
|
|
|
# If we're not in-game, have no quit button, and this is
|
|
# android, we want back presses to quit our activity.
|
|
if app.classic.platform == 'android':
|
|
|
|
def _do_quit() -> None:
|
|
bui.quit(confirm=True, quit_type=bui.QuitType.BACK)
|
|
|
|
bui.containerwidget(
|
|
edit=self._root_widget, on_cancel_call=_do_quit
|
|
)
|
|
|
|
def _quit(self) -> None:
|
|
# pylint: disable=cyclic-import
|
|
from bauiv1lib.confirm import QuitWindow
|
|
|
|
# no-op if we're not currently in control.
|
|
if not self.main_window_has_control():
|
|
return
|
|
|
|
# Note: Normally we should go through bui.quit(confirm=True) but
|
|
# invoking the window directly lets us scale it up from the
|
|
# button.
|
|
QuitWindow(origin_widget=self._quit_button)
|
|
|
|
def _credits(self) -> None:
|
|
# pylint: disable=cyclic-import
|
|
from bauiv1lib.credits import CreditsWindow
|
|
|
|
# no-op if we're not currently in control.
|
|
if not self.main_window_has_control():
|
|
return
|
|
|
|
self.main_window_replace(
|
|
CreditsWindow(origin_widget=self._credits_button),
|
|
)
|
|
|
|
def _howtoplay(self) -> None:
|
|
# pylint: disable=cyclic-import
|
|
from bauiv1lib.helpui import HelpWindow
|
|
|
|
# no-op if we're not currently in control.
|
|
if not self.main_window_has_control():
|
|
return
|
|
|
|
self.main_window_replace(
|
|
HelpWindow(origin_widget=self._how_to_play_button),
|
|
)
|
|
|
|
def _save_state(self) -> None:
|
|
try:
|
|
sel = self._root_widget.get_selected_child()
|
|
if sel == self._play_button:
|
|
sel_name = 'Start'
|
|
elif sel == self._gather_button:
|
|
sel_name = 'Gather'
|
|
elif sel == self._watch_button:
|
|
sel_name = 'Watch'
|
|
elif sel == self._how_to_play_button:
|
|
sel_name = 'HowToPlay'
|
|
elif sel == self._credits_button:
|
|
sel_name = 'Credits'
|
|
elif sel == self._quit_button:
|
|
sel_name = 'Quit'
|
|
elif sel == self._demo_menu_button:
|
|
sel_name = 'DemoMenu'
|
|
else:
|
|
print(f'Unknown widget in main menu selection: {sel}.')
|
|
sel_name = 'Start'
|
|
bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name}
|
|
except Exception:
|
|
logging.exception('Error saving state for %s.', self)
|
|
|
|
def _restore_state(self) -> None:
|
|
try:
|
|
|
|
sel: bui.Widget | None
|
|
|
|
sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get(
|
|
'sel_name'
|
|
)
|
|
assert isinstance(sel_name, (str, type(None)))
|
|
if sel_name is None:
|
|
sel_name = 'Start'
|
|
if sel_name == 'HowToPlay':
|
|
sel = self._how_to_play_button
|
|
elif sel_name == 'Gather':
|
|
sel = self._gather_button
|
|
elif sel_name == 'Watch':
|
|
sel = self._watch_button
|
|
elif sel_name == 'Credits':
|
|
sel = self._credits_button
|
|
elif sel_name == 'Quit':
|
|
sel = self._quit_button
|
|
elif sel_name == 'DemoMenu':
|
|
sel = self._demo_menu_button
|
|
else:
|
|
sel = self._play_button
|
|
if sel is not None:
|
|
bui.containerwidget(edit=self._root_widget, selected_child=sel)
|
|
|
|
except Exception:
|
|
logging.exception('Error restoring state for %s.', self)
|
|
|
|
def _gather_press(self) -> None:
|
|
# pylint: disable=cyclic-import
|
|
from bauiv1lib.gather import GatherWindow
|
|
|
|
# no-op if we're not currently in control.
|
|
if not self.main_window_has_control():
|
|
return
|
|
|
|
self.main_window_replace(
|
|
GatherWindow(origin_widget=self._gather_button)
|
|
)
|
|
|
|
def _watch_press(self) -> None:
|
|
# pylint: disable=cyclic-import
|
|
from bauiv1lib.watch import WatchWindow
|
|
|
|
# no-op if we're not currently in control.
|
|
if not self.main_window_has_control():
|
|
return
|
|
|
|
self.main_window_replace(
|
|
WatchWindow(origin_widget=self._watch_button),
|
|
)
|
|
|
|
def _play_press(self) -> None:
|
|
# pylint: disable=cyclic-import
|
|
from bauiv1lib.play import PlayWindow
|
|
|
|
# no-op if we're not currently in control.
|
|
if not self.main_window_has_control():
|
|
return
|
|
|
|
self.main_window_replace(PlayWindow(origin_widget=self._play_button))
|