using Godot; using System.Collections.Generic; 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 TipHoverRadius = 0.07f; private const float PressContactDistance = 0.045f; private const ulong PressCooldownMs = 400; private const float TipLength = 0.12f; private const float RayLength = 50f; private static WorldPanel _instance; private XRController3D _left; private XRController3D _right; private XRCamera3D _camera; private Locomotion _locomotion; private PropShop _propShop; private AudioStreamPlayer3D _click; private StaticBody3D _panelBody; private struct Knob { public string Name; public Node3D Root; public MeshInstance3D Mesh; public StandardMaterial3D Material; public Callable Action; public ulong LastPressMs; } private readonly List _knobs = new(); 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[] _triggerPrev = new bool[2]; private readonly bool[] _summonPrev = new bool[2]; private readonly int[] _hover = new int[2] { -1, -1 }; private readonly bool[] _hoverPanel = new bool[2]; public override void _Ready() { _instance = this; _left = GetNode("../LeftHand"); _right = GetNode("../RightHand"); _camera = GetNode("../XRCamera3D"); _locomotion = GetNode("../../Locomotion"); _propShop = GetNode("../../PropShop"); Visible = false; BuildPanel(); BuildClickSound(); SetPanelActive(false); } 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.HasMeta("knob_index") || node == _instance._panelBody || node.GetParent() == _instance._panelBody); public override void _Process(double delta) { PollSummon(_left, 0); PollSummon(_right, 1); if (!Visible) return; UpdateHand(_left, 0); UpdateHand(_right, 1); } 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) { if (!active) ResetInteractionState(); Visible = active; SetShapesEnabled(this, active); } private static void SetShapesEnabled(Node root, bool enabled) { foreach (Node child in root.GetChildren()) { if (child is CollisionShape3D shape) shape.SetDeferred(CollisionShape3D.PropertyName.Disabled, !enabled); SetShapesEnabled(child, enabled); } } private void PlaceInFrontOf(XRController3D controller) => OrientTo(controller.GlobalPosition - controller.GlobalTransform.Basis.Z * 0.5f); private void PlaceInFrontOfHead() => OrientTo(_camera.GlobalPosition - _camera.GlobalTransform.Basis.Z * 0.5f); private void OrientTo(Vector3 pos) { Vector3 look = _camera.GlobalPosition - pos; if (look.LengthSquared() < 0.0001f) return; Vector3 zAxis = look.Normalized(); Vector3 xAxis = Vector3.Up.Cross(zAxis); if (xAxis.LengthSquared() < 0.0001f) return; xAxis = xAxis.Normalized(); Vector3 yAxis = zAxis.Cross(xAxis).Normalized(); GlobalTransform = new Transform3D(new Basis(xAxis, yAxis, zAxis), pos); } private void OrientTowardCamera() { Vector3 look = _camera.GlobalPosition - GlobalPosition; if (look.LengthSquared() < 0.0001f) return; Vector3 zAxis = look.Normalized(); Vector3 xAxis = Vector3.Up.Cross(zAxis); if (xAxis.LengthSquared() < 0.0001f) return; xAxis = xAxis.Normalized(); Vector3 yAxis = zAxis.Cross(xAxis).Normalized(); GlobalBasis = GlobalBasis.Slerp(new Basis(xAxis, yAxis, zAxis), 0.35f).Orthonormalized(); } private void BuildPanel() { Node3D plate = new() { Name = "Plate" }; AddChild(plate); StandardMaterial3D plateMaterial = new() { AlbedoColor = new Color(0.08f, 0.09f, 0.12f), EmissionEnabled = true, Emission = new Color(0.1f, 0.2f, 0.35f), EmissionEnergyMultiplier = 0.4f, }; MeshInstance3D plateMesh = new() { Mesh = new BoxMesh { Size = new Vector3(0.6f, 0.34f, 0.02f) } }; plateMesh.SetSurfaceOverrideMaterial(0, plateMaterial); plate.AddChild(plateMesh); _panelBody = new StaticBody3D { Name = "PanelBody", CollisionLayer = 2, CollisionMask = 0, }; _panelBody.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(0.6f, 0.34f, 0.06f) } }); plate.AddChild(_panelBody); Vector3[] positions = { new(-0.21f, 0.06f, 0.03f), new(-0.07f, 0.06f, 0.03f), new(0.07f, 0.06f, 0.03f), new(0.21f, 0.06f, 0.03f), }; string[] names = { "Spawn", "Ping", "Reset", "Close" }; Color[] colors = { new(0.25f, 0.6f, 1f), new(0.95f, 0.75f, 0.25f), new(0.2f, 0.8f, 0.35f), new(0.9f, 0.3f, 0.3f), }; Callable[] actions = { Callable.From(() => { _locomotion.ReturnToSpawn(); ResetInteractionState(); PlaceInFrontOfHead(); }), Callable.From(Ping), Callable.From(() => _propShop.Respawn()), Callable.From(Close), }; for (int i = 0; i < names.Length; i++) { StandardMaterial3D material = new() { AlbedoColor = colors[i], EmissionEnabled = true, Emission = colors[i], EmissionEnergyMultiplier = 1.2f, }; MeshInstance3D mesh = new() { Mesh = new CylinderMesh { TopRadius = 0.032f, BottomRadius = 0.032f, Height = 0.025f }, RotationDegrees = new Vector3(90, 0, 0), }; mesh.SetSurfaceOverrideMaterial(0, material); StaticBody3D body = new() { CollisionLayer = 2, CollisionMask = 0, }; body.SetMeta("knob_index", i); body.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(0.09f, 0.09f, 0.06f) } }); Node3D root = new() { Position = positions[i] }; root.AddChild(mesh); root.AddChild(body); Label3D label = new() { Text = names[i], FontSize = 96, PixelSize = 0.00025f, OutlineSize = 6, Position = new Vector3(0, -0.08f, 0.015f), }; root.AddChild(label); plate.AddChild(root); _knobs.Add(new Knob { Name = names[i], Root = root, Mesh = mesh, Material = material, Action = actions[i], }); } } private void BuildClickSound() { const int rate = 44100; const int samples = rate / 25; byte[] data = new byte[samples * 2]; for (int i = 0; i < samples; i++) { float envelope = 1f - (float)i / samples; float value = Mathf.Sin(Mathf.Tau * 900f * i / rate) * envelope * 0.35f; short sample = (short)(value * short.MaxValue); data[i * 2] = (byte)sample; data[i * 2 + 1] = (byte)(sample >> 8); } _click = new AudioStreamPlayer3D { Stream = new AudioStreamWav { Format = AudioStreamWav.FormatEnum.Format16Bits, MixRate = rate, Stereo = false, Data = data, }, }; AddChild(_click); } private void UpdateHand(XRController3D controller, int hand) { Vector3 from = controller.GlobalPosition; Vector3 dir = -controller.GlobalTransform.Basis.Z; Vector3 tip = from + dir * TipLength; UpdateHover(controller, hand, from, dir, tip); bool grip = controller.GetFloat("grip") > GripThreshold && !PropGrabber.IsGrabbing(controller); if (grip && !_dragging[hand] && (_hoverPanel[hand] || GlobalPosition.DistanceTo(from) < DirectGrabRadius)) { _dragging[hand] = true; _directDrag[hand] = !_hoverPanel[hand]; 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(); } bool trigger = controller.GetFloat("trigger") > TriggerThreshold; if (trigger && !_triggerPrev[hand] && _hover[hand] >= 0) Press(_hover[hand]); _triggerPrev[hand] = trigger; if (_hover[hand] >= 0) { int hovered = _hover[hand]; Knob knob = _knobs[hovered]; if (knob.Root.GlobalPosition.DistanceTo(tip) < PressContactDistance) { ulong now = Time.GetTicksMsec(); if (now - knob.LastPressMs > PressCooldownMs) { knob.LastPressMs = now; _knobs[hovered] = knob; Press(hovered); } } } } private void UpdateHover(XRController3D controller, int hand, Vector3 from, Vector3 dir, Vector3 tip) { int knob = -1; bool panel = false; var query = PhysicsRayQueryParameters3D.Create(from, from + dir * RayLength); var hit = GetViewport().GetWorld3D().DirectSpaceState.IntersectRay(query); if (hit.TryGetValue("collider", out Variant collider) && collider.As() is Node node) { if (node.HasMeta("knob_index")) knob = (int)node.GetMeta("knob_index"); else panel = IsPanelCollider(node) || node.GetParent() == _panelBody; } if (knob < 0) { for (int i = 0; i < _knobs.Count; i++) { if (_knobs[i].Root.GlobalPosition.DistanceTo(tip) < TipHoverRadius) { knob = i; break; } } } if (knob != _hover[hand]) { if (_hover[hand] >= 0) _knobs[_hover[hand]].Material.EmissionEnergyMultiplier = 1.2f; _hover[hand] = knob; if (knob >= 0) _knobs[knob].Material.EmissionEnergyMultiplier = 2.8f; } _hoverPanel[hand] = panel; } private void Press(int index) { Knob knob = _knobs[index]; GD.Print($"WorldPanel: pressed '{knob.Name}'"); _click.Play(); Tween tween = CreateTween(); tween.TweenProperty(knob.Mesh, "position:z", -0.014f, 0.05); tween.Parallel().TweenProperty(knob.Material, "emission_energy_multiplier", 4.0f, 0.05); tween.TweenProperty(knob.Mesh, "position:z", 0f, 0.12); tween.Parallel().TweenProperty(knob.Material, "emission_energy_multiplier", 1.2f, 0.3); knob.Action.Call(); } private void Ping() => GD.Print("WorldPanel: ping"); private void Close() => SetPanelActive(false); private void ResetInteractionState() { for (int hand = 0; hand < 2; hand++) { _dragging[hand] = false; _hoverPanel[hand] = false; _triggerPrev[hand] = false; if (_hover[hand] >= 0) _knobs[_hover[hand]].Material.EmissionEnergyMultiplier = 1.2f; _hover[hand] = -1; } } }