using System.Collections.Generic; using System.Linq; using Cirno.Scripts.Interactables; using Godot; namespace Cirno.Scripts.Components.FSM._3DPlayer; /// /// Detects nearby 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 /// and the underlying collision shape is updated automatically. /// public partial class AutoPickupModule3D : Area3D, IModule { // 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; /// /// The radius of the auto-pickup detection sphere. /// Assigning a new value resizes the underlying collision shape immediately. /// 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 _trackedItems = []; public IStateMachine StateMachine { get; private set; } public void Init(IStateMachine machine) { StateMachine = machine; _collisionShape = GetNodeOrNull("CollisionShape3D") ?? CreateCollisionShape(); _pickupSound = GetNodeOrNull("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(); 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; } } }