|
|
|
public partial class PickupController : Node3D
|
|
|
|
{
|
|
|
|
public Item CurrentItem { get; private set; }
|
|
|
|
public bool HasItemsHeld => GetChildCount() > 0;
|
|
|
|
Node3D _placementPreview;
|
|
|
|
|
|
|
|
[Export] public float PickupDistance { get; set; } = 2.0f;
|
|
|
|
|
|
|
|
Player _player;
|
|
|
|
Node3D _world;
|
|
|
|
public override void _Ready()
|
|
|
|
{
|
|
|
|
_player = GetParent<Player>();
|
|
|
|
// TODO: Find a better way to find the world.
|
|
|
|
_world = GetNode<Node3D>("/root/Game/Workshop");
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void _UnhandledInput(InputEvent @event)
|
|
|
|
{
|
|
|
|
if (!_player.IsLocal) return;
|
|
|
|
EnsureCurrentItemValid();
|
|
|
|
if (CurrentItem == null) return;
|
|
|
|
|
|
|
|
if (@event.IsActionPressed("interact_pickup")) {
|
|
|
|
if (!HasItemsHeld) {
|
|
|
|
// Create clone of the item's model, use it as placement preview.
|
|
|
|
_placementPreview = (Node3D)CurrentItem.Model.Duplicate(0);
|
|
|
|
_placementPreview.Name = "PlacementPreview";
|
|
|
|
_placementPreview.TopLevel = true;
|
|
|
|
_placementPreview.Visible = false;
|
|
|
|
SetMeshLayerOutline(_placementPreview, OutlineMode.Exclusive);
|
|
|
|
AddChild(_placementPreview);
|
|
|
|
|
|
|
|
// Parent item to the `PickupController`.
|
|
|
|
var prevRot = CurrentItem.GlobalRotation;
|
|
|
|
CurrentItem.Freeze = true;
|
|
|
|
SetMeshLayerOutline(CurrentItem.Model, OutlineMode.Disable);
|
|
|
|
CurrentItem.GetParent().RemoveChild(CurrentItem);
|
|
|
|
AddChild(CurrentItem);
|
|
|
|
CurrentItem.Position = Vector3.Zero;
|
|
|
|
CurrentItem.GlobalRotation = prevRot;
|
|
|
|
|
|
|
|
GetViewport().SetInputAsHandled();
|
|
|
|
}
|
|
|
|
} else if (@event.IsActionPressed("interact_place")) {
|
|
|
|
if (HasItemsHeld) {
|
|
|
|
// Parent item back to the world.
|
|
|
|
var prevTransform = CurrentItem.GlobalTransform;
|
|
|
|
RemoveChild(CurrentItem);
|
|
|
|
_world.AddChild(CurrentItem);
|
|
|
|
|
|
|
|
if (_placementPreview.Visible) {
|
|
|
|
CurrentItem.GlobalTransform = _placementPreview.GlobalTransform;
|
|
|
|
} else {
|
|
|
|
CurrentItem.GlobalTransform = prevTransform;
|
|
|
|
CurrentItem.Freeze = false;
|
|
|
|
|
|
|
|
// Throw item forward and up a bit.
|
|
|
|
var basis = _player.Camera.Camera.GlobalBasis;
|
|
|
|
var direction = -basis.Z + basis.Y;
|
|
|
|
CurrentItem.ApplyImpulse(direction * 2);
|
|
|
|
}
|
|
|
|
|
|
|
|
RemoveChild(_placementPreview);
|
|
|
|
_placementPreview.QueueFree();
|
|
|
|
_placementPreview = null;
|
|
|
|
|
|
|
|
GetViewport().SetInputAsHandled();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void _PhysicsProcess(double delta)
|
|
|
|
{
|
|
|
|
if (!_player.IsLocal) return;
|
|
|
|
EnsureCurrentItemValid();
|
|
|
|
|
|
|
|
if (HasItemsHeld) {
|
|
|
|
if ((RayToMouseCursor() is RayResult ray) && (ray.Collider is Grid grid)) {
|
|
|
|
// Snao rotation to nearest axis.
|
|
|
|
var localBasis = grid.GlobalBasis.Inverse() * CurrentItem.GlobalBasis;
|
|
|
|
localBasis = Basis.FromEuler(localBasis.GetEuler().Snapped(Tau / 4));
|
|
|
|
_placementPreview.GlobalBasis = grid.GlobalBasis * localBasis;
|
|
|
|
|
|
|
|
// Snap the position to the grid.
|
|
|
|
var halfSize = (Vector3)CurrentItem.Size * Grid.StepSize / 2;
|
|
|
|
var localPos = ray.Position * grid.GlobalTransform; // Get grid-local ray position.
|
|
|
|
var localNormal = ray.Normal * grid.GlobalBasis; // Get grid-local ray normal. (Pointing away from surface hit.)
|
|
|
|
var off = localBasis * halfSize.PosMod(1.0f); // Calculate an offset for grid snapping.
|
|
|
|
off[(int)localNormal.Abs().MaxAxisIndex()] = 0; // Do not include offset in the normal direction.
|
|
|
|
localPos = off + (localPos - off).Snapped(Grid.StepSize); // Snap `localPos` to nearest grid value.
|
|
|
|
var axis = (localNormal * localBasis).Abs().MaxAxisIndex(); // Find object-local axis that the normal is pointing towards.
|
|
|
|
localPos += halfSize[(int)axis] * localNormal; // Offset (push out) object by half its size.
|
|
|
|
_placementPreview.GlobalPosition = grid.GlobalTransform * localPos;
|
|
|
|
|
|
|
|
_placementPreview.Visible = true;
|
|
|
|
} else {
|
|
|
|
_placementPreview.Visible = false;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
var interactable = RayToMouseCursor()?.Collider;
|
|
|
|
|
|
|
|
// Remove the outline from the previously looked-at item.
|
|
|
|
if (CurrentItem != null) SetMeshLayerOutline(CurrentItem.Model, OutlineMode.Disable);
|
|
|
|
// If the ray hits anything and the object hit is an item, set it as current.
|
|
|
|
CurrentItem = interactable as Item;
|
|
|
|
// Add the outline to the currently looked-at item.
|
|
|
|
if (CurrentItem != null) SetMeshLayerOutline(CurrentItem.Model, OutlineMode.Enable);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void EnsureCurrentItemValid()
|
|
|
|
{
|
|
|
|
if (CurrentItem == null) return;
|
|
|
|
if (!IsInstanceValid(CurrentItem)) {
|
|
|
|
CurrentItem = null;
|
|
|
|
if (_placementPreview != null) {
|
|
|
|
RemoveChild(_placementPreview);
|
|
|
|
_placementPreview.QueueFree();
|
|
|
|
_placementPreview = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
RayResult RayToMouseCursor()
|
|
|
|
{
|
|
|
|
var camera = _player.Camera.Camera;
|
|
|
|
var mouse = GetViewport().GetMousePosition();
|
|
|
|
var from = camera.ProjectRayOrigin(mouse);
|
|
|
|
var to = from + camera.ProjectRayNormal(mouse) * PickupDistance;
|
|
|
|
|
|
|
|
var query = PhysicsRayQueryParameters3D.Create(from, to);
|
|
|
|
query.CollisionMask = (uint)PhysicsLayer.Interactable;
|
|
|
|
query.CollideWithAreas = true;
|
|
|
|
// Exclude the `CurrentItem` from collision checking if it's being held.
|
|
|
|
query.Exclude = HasItemsHeld ? [ CurrentItem.GetRid() ] : [];
|
|
|
|
|
|
|
|
var result = GetWorld3D().DirectSpaceState.IntersectRay(query);
|
|
|
|
return (result.Count > 0) ? new(result) : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
class RayResult(Dictionary dict)
|
|
|
|
{
|
|
|
|
public CollisionObject3D Collider { get; } = dict["collider"].As<CollisionObject3D>();
|
|
|
|
public Vector3 Position { get; } = (Vector3)dict["position"];
|
|
|
|
public Vector3 Normal { get; } = (Vector3)dict["normal"];
|
|
|
|
}
|
|
|
|
|
|
|
|
enum OutlineMode { Disable, Enable, Exclusive }
|
|
|
|
static void SetMeshLayerOutline(Node3D parent, OutlineMode mode)
|
|
|
|
{
|
|
|
|
var children = parent.FindChildren("*", "MeshInstance3D", owned: false);
|
|
|
|
foreach (var mesh in children.Cast<MeshInstance3D>())
|
|
|
|
switch (mode) {
|
|
|
|
case OutlineMode.Disable: mesh.Layers &= ~(uint)RenderLayer.Outline; break;
|
|
|
|
case OutlineMode.Enable: mesh.Layers |= (uint)RenderLayer.Outline; break;
|
|
|
|
case OutlineMode.Exclusive: mesh.Layers = (uint)RenderLayer.Outline; break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|