using Godot; // The in-VR menu: a MenuScreen (Godot Control UI in a SubViewport on a thin 0.36x0.24 m // panel) showing MainMenu. This node is the XR glue: summon/dismiss with the LEFT ax_button (X; right A is jump), // grip-drag (direct or along the laser), re-orient toward the head, feed both controllers' // pointer rays into the screen, click sound + haptics. PARENTED UNDER XROrigin3D (fixed in the // player's room; game locomotion carries it). public partial class WorldPanel : Node3D { private const float GripThreshold = 0.6f; private const float TriggerThreshold = 0.6f; private const float DirectGrabRadius = 0.45f; private const float SummonDistance = 0.3f; private const float HeadRecallDistance = 0.45f; private const float HeadRecallDrop = 0.12f; private static WorldPanel _instance; private XRController3D _left; private XRController3D _right; private XRCamera3D _camera; private Locomotion _locomotion; private PropShop _propShop; private AudioStreamPlayer3D _click; private AudioStreamPlayer3D _ping; private MenuScreen _screen; private MainMenu _menu; private readonly bool[] _dragging = new bool[2]; private readonly bool[] _directDrag = new bool[2]; private readonly float[] _laserDist = new float[2]; private readonly Vector3[] _directOffset = new Vector3[2]; private readonly bool[] _summonPrev = new bool[2]; private readonly MenuScreen.PointerInput[] _inputs = new MenuScreen.PointerInput[2]; public override void _Ready() { _instance = this; _left = GetNode("../LeftHand"); _right = GetNode("../RightHand"); _camera = GetNode("../XRCamera3D"); _locomotion = GetNode("../../Locomotion"); _propShop = GetNode("../../PropShop"); _screen = new MenuScreen { Name = "Screen" }; AddChild(_screen); _menu = new MainMenu { Name = "MainMenu" }; _screen.SetContent(_menu); _screen.PointerPressed += OnPointerPressed; _screen.Haptic += (hand, amplitude, seconds) => (hand == 0 ? _left : _right).TriggerHapticPulse("haptic", 0.0, amplitude, seconds, 0.0); _menu.ReturnToSpawnRequested += () => { _locomotion.ReturnToSpawn(); ResetInteractionState(); PlaceInFrontOfHead(); _menu.Log("Returned to spawn"); }; _menu.ResetPropsRequested += () => { _propShop.Respawn(); GetNodeOrNull("../../Playground")?.Reset(); GetNodeOrNull("../../Crag")?.Reset(); _menu.Log("Props reset"); }; _menu.PingRequested += () => { GD.Print("WorldPanel: ping"); _ping.Play(); _menu.Log("Ping"); }; _menu.CloseRequested += () => SetPanelActive(false); _menu.Clicked += () => _click.Play(); _click = MakeTone(900f, 25, 0.35f, 1f); _ping = MakeTone(1320f, 4, 0.3f, 3f); _menu.ClickVolumeSlider.ValueChanged += v => _click.VolumeDb = Mathf.LinearToDb((float)v / 70f); _menu.StrengthChanged += v => PlayerRagdoll.Strength = v; _menu.SmoothTurnToggled += on => PlayerRig.SmoothTurn = on; _menu.SpectatorToggled += PlayerRig.SetSpectator; SetPanelActive(false); } // This controller currently points at (or touches) the menu — its stick scrolls the menu, // so locomotion must ignore it. public static bool IsPointing(XRController3D controller) { if (_instance == null || !_instance.Visible) return false; int hand = controller == _instance._left ? 0 : controller == _instance._right ? 1 : -1; return hand >= 0 && _instance._screen.GetMode(hand) != MenuScreen.PointerMode.None; } public static bool IsBlocking(XRController3D controller) => _instance != null && _instance.Visible && _instance.GlobalPosition.DistanceTo(controller.GlobalPosition) < DirectGrabRadius; public static bool IsPanelCollider(Node node) => _instance != null && _instance.Visible && node != null && (node == _instance._screen.Body || node.GetParent() == _instance._screen.Body); // Laser length to the menu for this controller, if the controller is currently using its // laser (not touch) on the menu. Locomotion.UpdatePointer draws the beam from this. public static bool TryGetLaserDistance(XRController3D controller, out float distance) { distance = 0f; if (_instance == null || !_instance.Visible) return false; int hand = controller == _instance._left ? 0 : controller == _instance._right ? 1 : -1; return hand >= 0 && _instance._screen.TryGetLaserDistance(hand, out distance); } public override void _Process(double delta) { PollSummon(_left, 0); // left X only: right A is jump if (Visible) { UpdateDrag(_left, 0); UpdateDrag(_right, 1); } _inputs[0] = ReadPointer(_left, 0); _inputs[1] = ReadPointer(_right, 1); _screen.UpdatePointers(_inputs, delta); } private MenuScreen.PointerInput ReadPointer(XRController3D controller, int hand) => new() { // A hand dragging the panel doesn't also point at it. Valid = !_dragging[hand] && controller.GetIsActive(), Origin = controller.GlobalPosition, Direction = -controller.GlobalTransform.Basis.Z, Trigger = controller.GetFloat("trigger") > TriggerThreshold, Stick = controller.GetVector2("primary"), }; private void OnPointerPressed(int hand, MenuScreen.PointerMode mode) { XRController3D controller = hand == 0 ? _left : _right; // Short tick on contact: the "physical press" feel for the touch cursor. controller.TriggerHapticPulse("haptic", 0.0, mode == MenuScreen.PointerMode.Touch ? 0.35 : 0.2, 0.02, 0.0); } private void PollSummon(XRController3D controller, int hand) { bool summon = controller.IsButtonPressed("ax_button"); if (summon && !_summonPrev[hand]) { if (!Visible) PlaceInFrontOf(controller); SetPanelActive(!Visible); GD.Print($"WorldPanel: {(Visible ? "summoned" : "dismissed")} via ax_button ({(hand == 0 ? "left" : "right")})"); } _summonPrev[hand] = summon; } private void SetPanelActive(bool active) { ResetInteractionState(); Visible = active; _screen.SetActive(active); if (active) _menu.RefreshCalibration(); } private void PlaceInFrontOf(XRController3D controller) => OrientTo(controller.GlobalPosition - controller.GlobalTransform.Basis.Z * SummonDistance); private void PlaceInFrontOfHead() { Vector3 forward = -_camera.GlobalTransform.Basis.Z; forward.Y = 0f; forward = forward.LengthSquared() < 0.0001f ? Vector3.Forward : forward.Normalized(); OrientTo(_camera.GlobalPosition + forward * HeadRecallDistance + Vector3.Down * HeadRecallDrop); } private static Basis FacingBasis(Vector3 from, Vector3 head, out bool ok) { ok = false; Vector3 look = head - from; if (look.LengthSquared() < 0.0001f) return Basis.Identity; Vector3 zAxis = look.Normalized(); Vector3 xAxis = Vector3.Up.Cross(zAxis); if (xAxis.LengthSquared() < 0.0001f) return Basis.Identity; xAxis = xAxis.Normalized(); Vector3 yAxis = zAxis.Cross(xAxis).Normalized(); ok = true; return new Basis(xAxis, yAxis, zAxis); } private void OrientTo(Vector3 pos) { Basis basis = FacingBasis(pos, _camera.GlobalPosition, out bool ok); if (ok) GlobalTransform = new Transform3D(basis, pos); } private void OrientTowardCamera() { Basis basis = FacingBasis(GlobalPosition, _camera.GlobalPosition, out bool ok); if (ok) GlobalBasis = GlobalBasis.Slerp(basis, 0.35f).Orthonormalized(); } private AudioStreamPlayer3D MakeTone(float frequency, int durationDivisor, float amplitude, float decayPower) { const int rate = 44100; int samples = rate / durationDivisor; byte[] data = new byte[samples * 2]; for (int i = 0; i < samples; i++) { float envelope = Mathf.Pow(1f - (float)i / samples, decayPower); float value = Mathf.Sin(Mathf.Tau * frequency * i / rate) * envelope * amplitude; short sample = (short)(value * short.MaxValue); data[i * 2] = (byte)sample; data[i * 2 + 1] = (byte)(sample >> 8); } AudioStreamPlayer3D player = new() { Stream = new AudioStreamWav { Format = AudioStreamWav.FormatEnum.Format16Bits, MixRate = rate, Stereo = false, Data = data, }, }; AddChild(player); return player; } private void UpdateDrag(XRController3D controller, int hand) { Vector3 from = controller.GlobalPosition; Vector3 dir = -controller.GlobalTransform.Basis.Z; bool laserOnPanel = _screen.GetMode(hand) == MenuScreen.PointerMode.Laser; bool grip = controller.GetFloat("grip") > GripThreshold && !PropGrabber.IsGrabbing(controller); if (grip && !_dragging[hand] && (laserOnPanel || GlobalPosition.DistanceTo(from) < DirectGrabRadius)) { _dragging[hand] = true; _directDrag[hand] = !laserOnPanel; if (_directDrag[hand]) _directOffset[hand] = GlobalPosition - from; else _laserDist[hand] = Mathf.Max(0.2f, (GlobalPosition - from).Dot(dir)); GD.Print($"WorldPanel: grab started ({(_directDrag[hand] ? "direct" : "laser")}, {(hand == 0 ? "left" : "right")})"); } if (!grip && _dragging[hand]) { _dragging[hand] = false; GD.Print("WorldPanel: grab released"); } if (_dragging[hand]) { GlobalPosition = _directDrag[hand] ? from + _directOffset[hand] : from + dir * _laserDist[hand]; OrientTowardCamera(); } } private void ResetInteractionState() { for (int hand = 0; hand < 2; hand++) _dragging[hand] = false; _screen?.ReleaseAll(); } }