cirnogodot/Scripts/Components/FSM/3DPlayer/AutoPickupModule3D.cs

152 lines
4.5 KiB
C#

using System.Collections.Generic;
using System.Linq;
using Cirno.Scripts.Interactables;
using Godot;
namespace Cirno.Scripts.Components.FSM._3DPlayer;
/// <summary>
/// Detects nearby <see cref="ItemPickup3D"/> items whose loot table contains at least one
/// auto-pickup entry and magnets them toward the player. Items are only formally collected
/// (inventory added, node freed) once they physically reach the player's position.
///
/// Radius can be increased at runtime via equipment or upgrade systems — just write to
/// <see cref="Radius"/> and the underlying collision shape is updated automatically.
/// </summary>
public partial class AutoPickupModule3D : Area3D, IModule<PlayerState, CharacterBody3D>
{
// Speed at which attracted items move toward the player (units/second).
private const float AttractionSpeed = 12f;
// Squared distance threshold at which an item is considered "collected".
private const float CollectDistanceSq = 0.15f * 0.15f;
[Export]
private float _radius = 3f;
/// <summary>
/// The radius of the auto-pickup detection sphere.
/// Assigning a new value resizes the underlying collision shape immediately.
/// </summary>
public float Radius
{
get => _radius;
set
{
_radius = value;
UpdateCollisionRadius();
}
}
private bool _enabled;
private CollisionShape3D _collisionShape;
private AudioStreamPlayer3D _pickupSound;
// Items currently being attracted toward the player.
private readonly HashSet<ItemPickup3D> _trackedItems = [];
public IStateMachine<PlayerState, CharacterBody3D> StateMachine { get; private set; }
public void Init(IStateMachine<PlayerState, CharacterBody3D> machine)
{
StateMachine = machine;
_collisionShape = GetNodeOrNull<CollisionShape3D>("CollisionShape3D") ?? CreateCollisionShape();
_pickupSound = GetNodeOrNull<AudioStreamPlayer3D>("PickupSound");
UpdateCollisionRadius();
}
private CollisionShape3D CreateCollisionShape()
{
var shape = new CollisionShape3D { Shape = new SphereShape3D { Radius = _radius } };
AddChild(shape);
return shape;
}
public void EnterState(PlayerState state)
{
_enabled = true;
AreaEntered += OnAreaEntered;
AreaExited += OnAreaExited;
}
public void ExitState(PlayerState state)
{
_enabled = false;
AreaEntered -= OnAreaEntered;
AreaExited -= OnAreaExited;
_trackedItems.Clear();
}
public void Process(double delta) { }
public void PhysicsProcess(double delta)
{
if (!_enabled || _trackedItems.Count == 0) return;
var playerPos = StateMachine.MainObject.GlobalPosition;
var toRemove = new List<ItemPickup3D>();
foreach (var item in _trackedItems)
{
if (!IsInstanceValid(item))
{
toRemove.Add(item);
continue;
}
var direction = playerPos - item.GlobalPosition;
var distanceSq = direction.LengthSquared();
if (distanceSq <= CollectDistanceSq)
{
item.Collect();
_pickupSound?.Play();
toRemove.Add(item);
continue;
}
// Move the item toward the player, scaling speed so it feels snappy at any distance.
item.GlobalPosition += direction.Normalized() * AttractionSpeed * (float)delta;
}
foreach (var item in toRemove)
{
_trackedItems.Remove(item);
}
}
private void OnAreaEntered(Area3D area)
{
if (!_enabled) return;
if (area is not ItemPickup3D pickup) return;
if (!pickup.AutoPickup) return;
// Only attract items whose inventory can still accept them to avoid a
// looping autopickup situation when the inventory slot is full.
var canAdd = pickup.LootTable.Aggregate(false,
(current, item) => current || InventoryManager.Instance.CanAddItem(item.ItemKey));
if (canAdd)
{
_trackedItems.Add(pickup);
}
}
private void OnAreaExited(Area3D area)
{
if (area is ItemPickup3D pickup)
{
_trackedItems.Remove(pickup);
}
}
private void UpdateCollisionRadius()
{
if (_collisionShape?.Shape is SphereShape3D sphere)
{
sphere.Radius = _radius;
}
}
}