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( caster.GetCollider( i ) ); if ( p != null && p.enabled ) { return p; } } return null; } Pointable FindNextClosestCandidate() { var currentIndex = -1; var candidates = new List(); for ( int i = 0; i < caster.NumColliders(); i++ ) { var p = Nodes.Find( 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( 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( caster.GetCollider( i ) ) == p ) { return true; } } return false; } } }