rokojori_action_library/Runtime/Interactions/LockOn/LockOnPointer.cs

290 lines
6.1 KiB
C#
Raw Normal View History

2026-10-06 18:50:28 +00:00
using Godot;
using System.Collections.Generic;
using Rokojori.Extensions;
namespace Rokojori
{
public enum LockOnCycleMode
{
None,
ClosestCycle,
Directional
}
[Tool]
[GlobalClass]
public partial class LockOnPointer : Pointer
{
[Export]
public Sensor engageSensor;
[Export]
public bool releaseWhenTargetInvalid = true;
[Export]
public Action onLockEngaged;
[Export]
public Action onLockReleased;
[ExportGroup( "Cycling" )]
[Export]
public LockOnCycleMode cycleMode = LockOnCycleMode.None;
[Export]
public Sensor cycleSensor;
[Export]
public DirectionalCycleInput[] directionalCycleInputs = [];
[Export]
public Node3D closestVisualizer;
[Export]
public Action onClosestFound;
[Export]
public Action onClosestLost;
[Export]
public Action onClosestChanged;
[ExportGroup("Read Only")]
[Export]
public Pointable closestCandidate;
bool _engaged = false;
public bool isEngaged => _engaged;
protected override void _OnTrigger()
{
if ( caster == null )
{
return;
}
UpdateClosestCandidate();
var wasLocked = pointable != null;
if ( visualizer != null && pointable != null )
{
visualizer.CopyGlobalPoseFrom( pointable.pointerPosition );
}
if ( engageSensor != null )
{
_engaged = engageSensor.isActive;
if ( ! _engaged )
{
if ( pointable != null )
{
SetPointable( null );
}
}
else if ( pointable == null )
{
SetPointable( closestCandidate );
}
else if ( releaseWhenTargetInvalid && ! IsStillValidTarget( pointable ) )
{
SetPointable( null );
}
else
{
HandleCycling();
}
}
// Fires off the pointable transition (not the raw engage-sensor edge) so it also covers the
// releaseWhenTargetInvalid auto-release path above, and so pointable already holds its new
// value (or null) by the time a listener - e.g. SetThirdPersonCameraLockTarget - reads it.
var isNowLocked = pointable != null;
if ( isNowLocked != wasLocked )
{
Action.TriggerSafe( isNowLocked ? onLockEngaged : onLockReleased );
}
}
void UpdateClosestCandidate()
{
var next = FindClosestCandidate();
if ( next == closestCandidate )
{
if ( closestVisualizer != null && closestCandidate != null )
{
closestVisualizer.CopyGlobalPoseFrom( closestCandidate.pointerPosition );
}
return;
}
closestCandidate = next;
onClosestChanged?.Trigger();
if ( closestCandidate == null )
{
onClosestLost?.Trigger();
}
else
{
onClosestFound?.Trigger();
}
if ( closestVisualizer != null && closestCandidate != null )
{
closestVisualizer.CopyGlobalPoseFrom( closestCandidate.pointerPosition );
}
}
void HandleCycling()
{
if ( cycleMode == LockOnCycleMode.ClosestCycle )
{
if ( cycleSensor != null && cycleSensor.isDown )
{
SetPointable( FindNextClosestCandidate() );
}
return;
}
if ( cycleMode != LockOnCycleMode.Directional || directionalCycleInputs == null )
{
return;
}
for ( int i = 0; i < directionalCycleInputs.Length; i++ )
{
var input = directionalCycleInputs[ i ];
if ( input == null || input.sensor == null || ! input.sensor.isDown )
{
continue;
}
var next = FindDirectionalCandidate( input.direction );
if ( next != null )
{
SetPointable( next );
}
return;
}
}
Pointable FindClosestCandidate()
{
for ( int i = 0; i < caster.NumColliders(); i++ )
{
var p = Nodes.Find<Pointable>( caster.GetCollider( i ) );
if ( p != null && p.enabled )
{
return p;
}
}
return null;
}
Pointable FindNextClosestCandidate()
{
var currentIndex = -1;
var candidates = new List<Pointable>();
for ( int i = 0; i < caster.NumColliders(); i++ )
{
var p = Nodes.Find<Pointable>( caster.GetCollider( i ) );
if ( p == null || ! p.enabled )
{
continue;
}
if ( p == pointable )
{
currentIndex = candidates.Count;
}
candidates.Add( p );
}
if ( candidates.Count == 0 )
{
return null;
}
if ( currentIndex == -1 )
{
return candidates[ 0 ];
}
return candidates[ ( currentIndex + 1 ) % candidates.Count ];
}
Pointable FindDirectionalCandidate( Vector2 direction )
{
if ( pointable == null )
{
return FindClosestCandidate();
}
var right = caster.GlobalTransform.Basis.X;
var up = caster.GlobalTransform.Basis.Y;
var currentPosition = pointable.GlobalPosition;
Pointable best = null;
var bestScore = float.NegativeInfinity;
for ( int i = 0; i < caster.NumColliders(); i++ )
{
var candidate = Nodes.Find<Pointable>( caster.GetCollider( i ) );
if ( candidate == null || candidate == pointable || ! candidate.enabled )
{
continue;
}
var offset = candidate.GlobalPosition - currentPosition;
var projected = new Vector2( offset.Dot( right ), offset.Dot( up ) );
if ( projected.LengthSquared() < 0.0001f )
{
continue;
}
var score = projected.Normalized().Dot( direction );
if ( score > bestScore )
{
bestScore = score;
best = candidate;
}
}
return best;
}
bool IsStillValidTarget( Pointable p )
{
for ( int i = 0; i < caster.NumColliders(); i++ )
{
if ( Nodes.Find<Pointable>( caster.GetCollider( i ) ) == p )
{
return true;
}
}
return false;
}
}
}