290 lines
6.1 KiB
C#
290 lines
6.1 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|