|
|
|
public partial class PickupController : Node3D
|
|
|
|
{
|
|
|
|
static readonly Color OutlinePickup = Colors.White with { A = 0.75f };
|
|
|
|
static readonly Color OutlineYesPlace = Colors.Green with { A = 0.75f };
|
|
|
|
static readonly Color OutlineNoPlace = Colors.Red with { A = 0.75f };
|
|
|
|
|
|
|
|
|
|
|
|
[Export] public float PickupDistance { get; set; } = 2.0f;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary> Item that is currently being targeted by the mouse cursor. </summary>
|
|
|
|
public Item TargetedItem { get; private set; }
|
|
|
|
|
|
|
|
/// <summary> Item currently being held. </summary>
|
|
|
|
public Item HeldItem => GetChildren().OfType<Item>().FirstOrDefault();
|
|
|
|
// TODO: Support holding multiple items.
|
|
|
|
|
|
|
|
/// <summary> Whether any items are being held. </summary>
|
|
|
|
public bool HasItemsHeld => HeldItem != null;
|
|
|
|
|
|
|
|
|
|
|
|
Node3D _preview; // Placement preview of the item - a duplicate of its model.
|
|
|
|
Grid _targetedGrid; // Grid currently being hovered over.
|
|
|
|
|
|
|
|
Player _player;
|
|
|
|
ShaderMaterial _outlineMaterial;
|
|
|
|
public override void _Ready()
|
|
|
|
{
|
|
|
|
_player = GetParent<Player>();
|
|
|
|
_outlineMaterial = Load<ShaderMaterial>("res://assets/shaders/outline_material.tres");
|
|
|
|
ChildEnteredTree += OnChildEnteredTree;
|
|
|
|
ChildExitingTree += OnChildExitingTree;
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void _UnhandledInput(InputEvent @event)
|
|
|
|
{
|
|
|
|
if (!_player.IsLocal) return;
|
|
|
|
|
|
|
|
if (@event.IsActionPressed("interact_pickup")
|
|
|
|
&& (TargetedItem != null) && !HasItemsHeld)
|
|
|
|
{
|
|
|
|
TargetedItem.Manager.TryPickup(TargetedItem);
|
|
|
|
GetViewport().SetInputAsHandled();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (@event.IsActionPressed("interact_place")
|
|
|
|
&& HasItemsHeld)
|
|
|
|
{
|
|
|
|
if (_preview.Visible && (_targetedGrid != null)) {
|
|
|
|
var inverse = _targetedGrid.GlobalTransform.AffineInverse();
|
|
|
|
HeldItem.Manager.TryPlace(HeldItem,
|
|
|
|
_targetedGrid, inverse * _preview.GlobalTransform);
|
|
|
|
} else
|
|
|
|
HeldItem.Manager.TryThrow(HeldItem);
|
|
|
|
GetViewport().SetInputAsHandled();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void _PhysicsProcess(double delta)
|
|
|
|
{
|
|
|
|
if (!_player.IsLocal) return;
|
|
|
|
|
|
|
|
if (HasItemsHeld) {
|
|
|
|
// This ray will be blocked by static and dynamic objects.
|
|
|
|
const PhysicsLayer Mask = PhysicsLayer.Place | PhysicsLayer.Static | PhysicsLayer.Dynamic;
|
|
|
|
|
|
|
|
if ((RayToMouseCursor(Mask) is RayResult ray) && (ray.Collider is Grid grid)
|
|
|
|
// Not pointing at item's own grid, or one of its nested grids.
|
|
|
|
&& (grid.GetParent() != HeldItem) && !grid.ContainsItem(HeldItem))
|
|
|
|
{
|
|
|
|
var inverseTransform = grid.GlobalTransform.AffineInverse();
|
|
|
|
var inverseBasis = grid.GlobalBasis.Inverse();
|
|
|
|
|
|
|
|
var pos = inverseTransform * ray.Position;
|
|
|
|
var normal = inverseBasis * ray.Normal;
|
|
|
|
|
|
|
|
if (grid.CanPlaceAgainst(HeldItem, pos, normal)) {
|
|
|
|
var transform = new Transform3D(inverseBasis * HeldItem.GlobalBasis, pos);
|
|
|
|
transform = grid.Snap(transform, normal, HeldItem);
|
|
|
|
var canPlace = grid.CanPlaceAt(HeldItem, transform);
|
|
|
|
|
|
|
|
var outlineColor = canPlace ? OutlineYesPlace : OutlineNoPlace;
|
|
|
|
_outlineMaterial.SetShaderParameter("line_color", outlineColor);
|
|
|
|
|
|
|
|
_preview.GlobalTransform = grid.GlobalTransform * transform;
|
|
|
|
_preview.Visible = true;
|
|
|
|
_targetedGrid = canPlace ? grid : null;
|
|
|
|
} else {
|
|
|
|
_preview.Visible = false;
|
|
|
|
_targetedGrid = null;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
_preview.Visible = false;
|
|
|
|
_targetedGrid = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
var interactable = RayToMouseCursor(PhysicsLayer.Pickup)?.Collider;
|
|
|
|
|
|
|
|
// Remove the outline from the previously looked-at item.
|
|
|
|
if (TargetedItem != null) SetMeshLayerOutline(TargetedItem.Model, OutlineMode.Disable);
|
|
|
|
// If the ray hits anything and the object hit is an item, set it as current.
|
|
|
|
TargetedItem = interactable as Item;
|
|
|
|
// Add the outline to the currently looked-at item.
|
|
|
|
if (TargetedItem != null) SetMeshLayerOutline(TargetedItem.Model, OutlineMode.Enable);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void OnChildEnteredTree(Node node)
|
|
|
|
{
|
|
|
|
if (node is not Item item) return;
|
|
|
|
|
|
|
|
// TODO: Will be able to hold multiple items in the future. Map item => preview.
|
|
|
|
AddChild(_preview = CreatePlacementPreview(item));
|
|
|
|
|
|
|
|
// TODO: Move this elsewhere. Or just redo outline handling.
|
|
|
|
SetMeshLayerOutline(item.Model, OutlineMode.Disable);
|
|
|
|
}
|
|
|
|
|
|
|
|
void OnChildExitingTree(Node node)
|
|
|
|
{
|
|
|
|
if (node is not Item) return;
|
|
|
|
|
|
|
|
// Reset the color of the outline shader material.
|
|
|
|
_outlineMaterial.SetShaderParameter("line_color", OutlinePickup);
|
|
|
|
|
|
|
|
_preview.QueueFree();
|
|
|
|
_preview = null;
|
|
|
|
_targetedGrid = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary> Creates a clone of the item's model, to be rendered as a placement preview. </summary>
|
|
|
|
static Node3D CreatePlacementPreview(Item item)
|
|
|
|
{
|
|
|
|
var preview = (Node3D)item.Model.Duplicate(0);
|
|
|
|
preview.Name = "PlacementPreview";
|
|
|
|
preview.TopLevel = true; // Only use global space transformations.
|
|
|
|
preview.Visible = false; // Starts out not visible.
|
|
|
|
SetMeshLayerOutline(preview, OutlineMode.Exclusive);
|
|
|
|
return preview;
|
|
|
|
}
|
|
|
|
|
|
|
|
record class RayResult(CollisionObject3D Collider, Vector3 Position, Vector3 Normal);
|
|
|
|
RayResult RayToMouseCursor(PhysicsLayer collisionMask)
|
|
|
|
{
|
|
|
|
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)collisionMask;
|
|
|
|
query.CollideWithAreas = true;
|
|
|
|
// Exclude the `CurrentItem` from collision checking if it's being held.
|
|
|
|
query.Exclude = HasItemsHeld ? [ HeldItem.GetRid() ] : [];
|
|
|
|
|
|
|
|
var result = GetWorld3D().DirectSpaceState.IntersectRay(query);
|
|
|
|
return (result.Count > 0) ? new(
|
|
|
|
// FIXME: Unable to cast object of type 'ReplacePalette' to type 'Godot.CollisionObject3D'.
|
|
|
|
result["collider"].As<CollisionObject3D>(),
|
|
|
|
(Vector3)result["position"],
|
|
|
|
(Vector3)result["normal"]
|
|
|
|
) : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|