Files
VR-Mess/scripts/WorldPanel.cs
T
FoxyzandClaude Opus 5.5 e821728fba Body step 2.6: landing dip, jump, stick crouch, no flip looking down, smooth view
- Look-down flip: body facing from BodyMath.HeadFacing (visor + crown), robust past vertical;
  stick direction uses it too; crouch lean pitch measured against the facing.
- Landing: falling legs touch down only within their reach, so leg strength stops the body
  below its stance (dip scales with impact, big falls fold the legs). Rise eased so gravity
  can stop it at the stance; legs untuck speed-limited. Fixed the vertical play-space
  correction leaking into ground control's feed-forward (a bounce).
- Jump on right A: countermovement dip + leg push over the stroke (crouch = higher).
- Stick crouch on right stick Y (RagdollDriver.StickCrouch: virtual floor offset).
- Menu summon on left X only.
- XR origin drawn with the bodies' physics interpolation (camera/body desync).
- RagdollSpike: landing/fall/jump/look-down/stick-crouch phases, `-- only=` filter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:25:15 +03:00

275 lines
10 KiB
C#

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<XRController3D>("../LeftHand");
_right = GetNode<XRController3D>("../RightHand");
_camera = GetNode<XRCamera3D>("../XRCamera3D");
_locomotion = GetNode<Locomotion>("../../Locomotion");
_propShop = GetNode<PropShop>("../../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>("../../Playground")?.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();
}
}