Inventory management focused game written in Godot / C#
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

179 lines
6.1 KiB

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 };
Node3D _preview; // Placement preview of the item - a duplicate of its model.
Grid _grid; // Grid currently being hovered over.
public Item CurrentItem { get; private set; }
public bool HasItemsHeld => GetChildCount() > 0;
[Export] public float PickupDistance { get; set; } = 2.0f;
Player _player;
Node3D _world;
ShaderMaterial _outlineShaderMaterial;
public override void _Ready()
{
_player = GetParent<Player>();
_world = GetNode<Node3D>("/root/Game/Workshop"); // TODO: Find a better way to get the world.
_outlineShaderMaterial = Load<ShaderMaterial>("res://assets/shaders/outline_material.tres");
}
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.
_preview = (Node3D)CurrentItem.Model.Duplicate(0);
_preview.Name = "PlacementPreview";
_preview.TopLevel = true;
_preview.Visible = false;
SetMeshLayerOutline(_preview, OutlineMode.Exclusive);
AddChild(_preview);
// 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);
if (_preview.Visible && _grid != null) {
_grid.AddChild(CurrentItem);
CurrentItem.GlobalTransform = _preview.GlobalTransform;
} else {
_world.AddChild(CurrentItem);
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);
}
// Reset the color of the outline shader material.
_outlineShaderMaterial.SetShaderParameter("line_color", OutlinePickup);
RemoveChild(_preview);
_preview.QueueFree();
_preview = null;
_grid = null;
GetViewport().SetInputAsHandled();
}
}
}
public override void _PhysicsProcess(double delta)
{
if (!_player.IsLocal) return;
EnsureCurrentItemValid();
static bool CanPlaceAgainst(Grid grid, Vector3 normal)
{
normal = grid.GlobalBasis.Inverse() * normal;
return normal.IsEqualApprox(Vector3.Up);
}
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)
// Make sure this is placed against the top of the grid.
&& CanPlaceAgainst(grid, ray.Normal)
// Ensure item is not being added to itself or nested items.
&& !((grid.GetParent() == CurrentItem) || grid.ContainsItem(CurrentItem))) {
var transform = CurrentItem.GlobalTransform with { Origin = ray.Position };
transform = grid.Snap(transform, ray.Normal, CurrentItem);
_preview.GlobalTransform = transform;
var canPlace = grid.CanPlaceAt(CurrentItem, transform);
var outlineColor = canPlace ? OutlineYesPlace : OutlineNoPlace;
_outlineShaderMaterial.SetShaderParameter("line_color", outlineColor);
_preview.Visible = true;
_grid = canPlace ? grid : null;
} else {
_preview.Visible = false;
_grid = null;
}
} else {
var interactable = RayToMouseCursor(PhysicsLayer.Pickup)?.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 (_preview != null) {
RemoveChild(_preview);
_preview.QueueFree();
_preview = null;
}
}
}
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 ? [ 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;
}
}
}