Files
VR-Mess/scripts/MainMenu.cs
T

352 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Godot;
using System;
// The in-VR menu content: plain Godot Controls, rendered by MenuScreen into its SubViewport.
// Built in code (no .tscn) so the spike can instantiate it without XR. Actions are C# events;
// WorldPanel wires them to the game. Sized for a 960x640 viewport on a 0.36x0.24 m panel
// (~2.7 px/mm): 26 px body text ≈ 1.2° at 45 cm, 64 px buttons ≈ 24 mm touch targets.
public partial class MainMenu : PanelContainer
{
public const string CalibrationGroup = "calibration";
private const int FontSize = 26;
private const int ButtonHeight = 64;
private const int MaxLogLines = 40;
public event Action ReturnToSpawnRequested;
public event Action ResetPropsRequested;
public event Action PingRequested;
public event Action CloseRequested;
public event Action Clicked;
public event Action<bool> SpectatorToggled;
public event Action<bool> SmoothTurnToggled;
public event Action<bool> LowLagTrackingToggled;
public event Action<float> StrengthChanged;
public TabContainer Tabs { get; private set; }
public Button ReturnToSpawnButton { get; private set; }
public Button RecalibrateButton { get; private set; }
public Button ResetPropsButton { get; private set; }
public Button PingButton { get; private set; }
public Button CloseButton { get; private set; }
public Button HeaderCloseButton { get; private set; }
public HSlider ClickVolumeSlider { get; private set; }
public HSlider StrengthSlider { get; private set; }
public Button SpectatorButton { get; private set; }
public Button TurnModeButton { get; private set; }
public ScrollContainer LogScroll { get; private set; }
private Label _fpsLabel;
private Label _calibrationStatus;
private VBoxContainer _logLines;
private double _refreshTimer;
public MainMenu()
{
Theme = BuildTheme();
BuildLayout();
}
public override void _Ready() => RefreshCalibration();
public override void _Process(double delta)
{
_refreshTimer -= delta;
if (_refreshTimer > 0)
return;
_refreshTimer = 0.25;
_fpsLabel.Text = $"{Engine.GetFramesPerSecond():0} fps";
RefreshCalibration();
}
// Contract with the body-calibration work: a node in group "calibration" exposing
// Recalibrate(). Absent → button disabled. We never create that node here.
public Node FindCalibration()
{
if (!IsInsideTree())
return null;
Node node = GetTree().GetFirstNodeInGroup(CalibrationGroup);
return node != null && node.HasMethod("Recalibrate") ? node : null;
}
public void RefreshCalibration()
{
bool available = FindCalibration() != null;
RecalibrateButton.Disabled = !available;
_calibrationStatus.Text = available ? "" : "Not available";
}
public void Log(string line)
{
_logLines.AddChild(new Label { Text = line, AutowrapMode = TextServer.AutowrapMode.WordSmart });
while (_logLines.GetChildCount() > MaxLogLines)
{
Node oldest = _logLines.GetChild(0);
_logLines.RemoveChild(oldest);
oldest.QueueFree();
}
}
private void BuildLayout()
{
VBoxContainer root = new();
root.AddThemeConstantOverride("separation", 10);
AddChild(root);
HBoxContainer header = new();
header.AddThemeConstantOverride("separation", 16);
root.AddChild(header);
Label title = new() { Text = "VR Messs", SizeFlagsHorizontal = SizeFlags.ExpandFill, VerticalAlignment = VerticalAlignment.Center };
title.AddThemeFontSizeOverride("font_size", 30);
header.AddChild(title);
_fpsLabel = new Label { Text = "-- fps", VerticalAlignment = VerticalAlignment.Center };
_fpsLabel.AddThemeColorOverride("font_color", new Color(0.6f, 0.68f, 0.8f));
header.AddChild(_fpsLabel);
HeaderCloseButton = MakeButton("Close", () => CloseRequested?.Invoke());
HeaderCloseButton.CustomMinimumSize = new Vector2(120, 52);
header.AddChild(HeaderCloseButton);
Tabs = new TabContainer { SizeFlagsVertical = SizeFlags.ExpandFill, TabFocusMode = FocusModeEnum.None };
Tabs.TabChanged += _ => Clicked?.Invoke();
root.AddChild(Tabs);
// Player
VBoxContainer player = MakePage("Player");
ReturnToSpawnButton = MakeButton("Return to spawn", () => ReturnToSpawnRequested?.Invoke());
player.AddChild(ReturnToSpawnButton);
HBoxContainer calibRow = new();
calibRow.AddThemeConstantOverride("separation", 16);
RecalibrateButton = MakeButton("Recalibrate body", Recalibrate);
RecalibrateButton.SizeFlagsHorizontal = SizeFlags.ExpandFill;
calibRow.AddChild(RecalibrateButton);
_calibrationStatus = new Label { VerticalAlignment = VerticalAlignment.Center };
_calibrationStatus.AddThemeColorOverride("font_color", new Color(0.6f, 0.6f, 0.65f));
calibRow.AddChild(_calibrationStatus);
player.AddChild(calibRow);
// Nimso's single strength value: every muscle and the hand guides scale with it.
HBoxContainer strengthRow = new();
strengthRow.AddThemeConstantOverride("separation", 20);
Label strengthLabel = new() { Text = "Strength 1.00×", VerticalAlignment = VerticalAlignment.Center, CustomMinimumSize = new Vector2(220, 0) };
strengthRow.AddChild(strengthLabel);
StrengthSlider = new HSlider
{
MinValue = 0.25,
MaxValue = 3,
Step = 0.05,
Value = 1,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
CustomMinimumSize = new Vector2(0, 48),
FocusMode = FocusModeEnum.None,
};
StrengthSlider.ValueChanged += v =>
{
strengthLabel.Text = $"Strength {v:0.00}×";
StrengthChanged?.Invoke((float)v);
};
strengthRow.AddChild(StrengthSlider);
player.AddChild(strengthRow);
HBoxContainer modeRow = new();
modeRow.AddThemeConstantOverride("separation", 16);
TurnModeButton = MakeToggle("Turning: snap", "Turning: smooth", on => SmoothTurnToggled?.Invoke(on));
modeRow.AddChild(TurnModeButton);
// Spectator: no body; fly around by grabbing the air (the old drag movement).
SpectatorButton = MakeToggle("Spectator: off", "Spectator: on (grab-fly)", on => SpectatorToggled?.Invoke(on));
modeRow.AddChild(SpectatorButton);
player.AddChild(modeRow);
// Compare in the headset: tracking timed by the render clock (even motion) or by arrival (less lag).
HBoxContainer trackingRow = new();
trackingRow.AddChild(MakeToggle("Tracking: steady", "Tracking: low lag", on => LowLagTrackingToggled?.Invoke(on)));
player.AddChild(trackingRow);
Tabs.AddChild(player);
// World
VBoxContainer world = MakePage("World");
ResetPropsButton = MakeButton("Reset props", () => ResetPropsRequested?.Invoke());
world.AddChild(ResetPropsButton);
PingButton = MakeButton("Ping sound", () => PingRequested?.Invoke());
world.AddChild(PingButton);
Tabs.AddChild(world);
// System
VBoxContainer system = MakePage("System");
HBoxContainer volumeRow = new();
volumeRow.AddThemeConstantOverride("separation", 20);
volumeRow.AddChild(new Label { Text = "Click volume", VerticalAlignment = VerticalAlignment.Center, CustomMinimumSize = new Vector2(200, 0) });
ClickVolumeSlider = new HSlider
{
MinValue = 0,
MaxValue = 100,
Step = 1,
Value = 70,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
CustomMinimumSize = new Vector2(0, 48),
FocusMode = FocusModeEnum.None,
};
volumeRow.AddChild(ClickVolumeSlider);
system.AddChild(volumeRow);
CloseButton = MakeButton("Close menu", () => CloseRequested?.Invoke());
system.AddChild(CloseButton);
Label logTitle = new() { Text = "Log (swipe or thumbstick to scroll)" };
logTitle.AddThemeColorOverride("font_color", new Color(0.6f, 0.68f, 0.8f));
system.AddChild(logTitle);
LogScroll = new ScrollContainer
{
SizeFlagsVertical = SizeFlags.ExpandFill,
HorizontalScrollMode = ScrollContainer.ScrollMode.Disabled,
};
_logLines = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
_logLines.AddThemeConstantOverride("separation", 2);
LogScroll.AddChild(_logLines);
system.AddChild(LogScroll);
Tabs.AddChild(system);
}
private void Recalibrate()
{
Node calibration = FindCalibration();
if (calibration == null)
{
RefreshCalibration();
return;
}
GD.Print("MainMenu: Recalibrate body → calibration.Recalibrate()");
calibration.Call("Recalibrate");
}
private static VBoxContainer MakePage(string title)
{
VBoxContainer page = new() { Name = title };
page.AddThemeConstantOverride("separation", 14);
return page;
}
private Button MakeToggle(string offText, string onText, Action<bool> changed)
{
Button button = new()
{
Text = offText,
ToggleMode = true,
CustomMinimumSize = new Vector2(0, ButtonHeight),
SizeFlagsHorizontal = SizeFlags.ExpandFill,
FocusMode = FocusModeEnum.None,
};
button.Toggled += on =>
{
button.Text = on ? onText : offText;
Clicked?.Invoke();
Callable.From(() => changed(on)).CallDeferred();
};
return button;
}
private Button MakeButton(string text, Action action)
{
Button button = new()
{
Text = text,
CustomMinimumSize = new Vector2(0, ButtonHeight),
FocusMode = FocusModeEnum.None,
};
button.Pressed += () =>
{
Clicked?.Invoke();
// Deferred: actions may hide/move the panel; never do that from inside the
// input event MenuScreen is injecting.
Callable.From(action).CallDeferred();
};
return button;
}
private static StyleBoxFlat Box(Color bg, int radius, float marginX, float marginY, Color? border = null, int borderWidth = 0)
{
StyleBoxFlat box = new() { BgColor = bg };
box.SetCornerRadiusAll(radius);
box.ContentMarginLeft = marginX;
box.ContentMarginRight = marginX;
box.ContentMarginTop = marginY;
box.ContentMarginBottom = marginY;
if (border is Color c)
{
box.BorderColor = c;
box.SetBorderWidthAll(borderWidth);
}
return box;
}
private static Theme BuildTheme()
{
Color accent = new(0.35f, 0.6f, 0.95f);
Theme theme = new() { DefaultFontSize = FontSize };
theme.SetStylebox("panel", "PanelContainer", Box(new Color(0.07f, 0.08f, 0.11f), 0, 18, 14));
theme.SetStylebox("normal", "Button", Box(new Color(0.17f, 0.2f, 0.27f), 10, 20, 8));
theme.SetStylebox("hover", "Button", Box(new Color(0.24f, 0.31f, 0.44f), 10, 20, 8, accent, 3));
theme.SetStylebox("pressed", "Button", Box(new Color(0.33f, 0.5f, 0.78f), 10, 20, 8, new Color(0.7f, 0.85f, 1f), 3));
theme.SetStylebox("hover_pressed", "Button", Box(new Color(0.33f, 0.5f, 0.78f), 10, 20, 8, new Color(0.7f, 0.85f, 1f), 3));
theme.SetStylebox("disabled", "Button", Box(new Color(0.11f, 0.12f, 0.15f), 10, 20, 8));
theme.SetStylebox("focus", "Button", new StyleBoxEmpty());
theme.SetColor("font_color", "Button", new Color(0.92f, 0.94f, 0.98f));
theme.SetColor("font_hover_color", "Button", Colors.White);
theme.SetColor("font_pressed_color", "Button", Colors.White);
theme.SetColor("font_disabled_color", "Button", new Color(0.45f, 0.47f, 0.52f));
theme.SetStylebox("panel", "TabContainer", Box(new Color(0.1f, 0.12f, 0.16f), 10, 18, 16));
StyleBoxFlat tabSelected = Box(new Color(0.1f, 0.12f, 0.16f), 0, 28, 12);
tabSelected.CornerRadiusTopLeft = 10;
tabSelected.CornerRadiusTopRight = 10;
tabSelected.BorderColor = accent;
tabSelected.BorderWidthTop = 4;
theme.SetStylebox("tab_selected", "TabContainer", tabSelected);
StyleBoxFlat tabUnselected = Box(new Color(0.13f, 0.15f, 0.2f), 0, 28, 12);
tabUnselected.CornerRadiusTopLeft = 10;
tabUnselected.CornerRadiusTopRight = 10;
theme.SetStylebox("tab_unselected", "TabContainer", tabUnselected);
StyleBoxFlat tabHovered = Box(new Color(0.2f, 0.26f, 0.37f), 0, 28, 12);
tabHovered.CornerRadiusTopLeft = 10;
tabHovered.CornerRadiusTopRight = 10;
theme.SetStylebox("tab_hovered", "TabContainer", tabHovered);
theme.SetStylebox("tab_focus", "TabContainer", new StyleBoxEmpty());
theme.SetConstant("tab_separation", "TabContainer", 6);
theme.SetFontSize("font_size", "TabContainer", FontSize);
// Slider: thick track, big round grabber (default icons are ~16 px — too small in VR).
StyleBoxFlat track = Box(new Color(0.17f, 0.2f, 0.27f), 6, 0, 6);
theme.SetStylebox("slider", "HSlider", track);
theme.SetStylebox("grabber_area", "HSlider", Box(accent, 6, 0, 6));
theme.SetStylebox("grabber_area_highlight", "HSlider", Box(accent.Lightened(0.2f), 6, 0, 6));
theme.SetIcon("grabber", "HSlider", Circle(36, new Color(0.9f, 0.94f, 1f)));
theme.SetIcon("grabber_highlight", "HSlider", Circle(36, Colors.White));
theme.SetIcon("grabber_disabled", "HSlider", Circle(36, new Color(0.4f, 0.4f, 0.45f)));
// Wide scrollbar so it is touchable.
theme.SetStylebox("scroll", "VScrollBar", Box(new Color(0.13f, 0.15f, 0.2f), 8, 14, 0));
theme.SetStylebox("grabber", "VScrollBar", Box(new Color(0.3f, 0.36f, 0.48f), 8, 14, 0));
theme.SetStylebox("grabber_highlight", "VScrollBar", Box(new Color(0.4f, 0.5f, 0.68f), 8, 14, 0));
theme.SetStylebox("grabber_pressed", "VScrollBar", Box(accent, 8, 14, 0));
theme.SetColor("font_color", "Label", new Color(0.88f, 0.9f, 0.95f));
return theme;
}
private static ImageTexture Circle(int size, Color color)
{
Image image = Image.CreateEmpty(size, size, false, Image.Format.Rgba8);
float r = size * 0.5f;
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
float d = new Vector2(x + 0.5f - r, y + 0.5f - r).Length();
float a = Mathf.Clamp(r - d, 0f, 1f); // 1 px anti-aliased edge
image.SetPixel(x, y, new Color(color, a));
}
}
return ImageTexture.CreateFromImage(image);
}
}