294 lines
6.6 KiB
C#
294 lines
6.6 KiB
C#
|
|
using Godot;
|
||
|
|
|
||
|
|
using Rokojori.Extensions;
|
||
|
|
namespace Rokojori
|
||
|
|
{
|
||
|
|
// Single owner of lock state. LockOnPointer only tracks candidates and the current selection;
|
||
|
|
// ThirdPersonCamera (or anything else) only reads isLocked/lockTarget from here.
|
||
|
|
[Tool]
|
||
|
|
[GlobalClass]
|
||
|
|
public partial class CameraLocker : Action
|
||
|
|
{
|
||
|
|
[Export]
|
||
|
|
public LockOnPointer source;
|
||
|
|
|
||
|
|
[Export]
|
||
|
|
public Sensor engageSensor;
|
||
|
|
|
||
|
|
[ExportGroup( "Release" )]
|
||
|
|
[Export]
|
||
|
|
public float releaseDistance = 0f;
|
||
|
|
|
||
|
|
[Export]
|
||
|
|
public Node3D releaseDistanceOrigin;
|
||
|
|
|
||
|
|
[ExportGroup( "Cycling" )]
|
||
|
|
[Export]
|
||
|
|
public LockOnCyclingSetup cyclingSetup;
|
||
|
|
|
||
|
|
[Export]
|
||
|
|
public LockOnCyclingSetup lockedCyclingSetup;
|
||
|
|
|
||
|
|
[ExportGroup( "Visualizer" )]
|
||
|
|
[Export]
|
||
|
|
public Node3D visualizerMarker;
|
||
|
|
|
||
|
|
[ExportGroup( "Candidate Events" )]
|
||
|
|
[Export]
|
||
|
|
public Action onCandidateSpotted;
|
||
|
|
|
||
|
|
[Export]
|
||
|
|
public Action onCandidateUnlocked;
|
||
|
|
|
||
|
|
[Export]
|
||
|
|
public Action onCandidateVanished;
|
||
|
|
|
||
|
|
[Export]
|
||
|
|
public Action onCandidateChanged;
|
||
|
|
|
||
|
|
[Export]
|
||
|
|
public bool triggerCanidatesWhenLocked = false;
|
||
|
|
|
||
|
|
|
||
|
|
[ExportGroup( "Lock Events" )]
|
||
|
|
[Export]
|
||
|
|
public Action onLock;
|
||
|
|
|
||
|
|
[Export]
|
||
|
|
public Action onReleasedLock;
|
||
|
|
|
||
|
|
[Export]
|
||
|
|
public Action onChangeLocked;
|
||
|
|
|
||
|
|
[ExportGroup( "Read Only" )]
|
||
|
|
[Export]
|
||
|
|
public Pointable lockTarget;
|
||
|
|
|
||
|
|
public bool isLocked => lockTarget != null;
|
||
|
|
|
||
|
|
Pointable _previousCandidate;
|
||
|
|
bool _wasEngaged = false;
|
||
|
|
|
||
|
|
protected override void _OnTrigger()
|
||
|
|
{
|
||
|
|
if ( source == null )
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
source.Trigger();
|
||
|
|
|
||
|
|
UpdateCandidateEvents();
|
||
|
|
|
||
|
|
var previousTarget = lockTarget;
|
||
|
|
var wasLocked = previousTarget != null;
|
||
|
|
|
||
|
|
// Order matters: cycling may move an existing lock, engagement may start/end one,
|
||
|
|
// auto-release may end one - each only touches lockTarget for its own reason, nothing
|
||
|
|
// here resyncs it every frame just because source.pointable drifted on its own.
|
||
|
|
UpdateCycling();
|
||
|
|
UpdateEngagement();
|
||
|
|
UpdateAutoRelease();
|
||
|
|
|
||
|
|
FireLockEvents( wasLocked, previousTarget );
|
||
|
|
|
||
|
|
UpdateVisualizer();
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Release()
|
||
|
|
{
|
||
|
|
if ( lockTarget == null )
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var previousTarget = lockTarget;
|
||
|
|
lockTarget = null;
|
||
|
|
|
||
|
|
FireLockEvents( true, previousTarget );
|
||
|
|
}
|
||
|
|
|
||
|
|
void UpdateCandidateEvents()
|
||
|
|
{
|
||
|
|
if ( lockTarget != null && ! triggerCanidatesWhenLocked )
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var candidate = source.pointable;
|
||
|
|
|
||
|
|
if ( candidate == _previousCandidate )
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
_previousCandidate = candidate;
|
||
|
|
|
||
|
|
onCandidateChanged?.Trigger();
|
||
|
|
|
||
|
|
Action.TriggerSafe( candidate == null ? onCandidateVanished : onCandidateSpotted );
|
||
|
|
}
|
||
|
|
|
||
|
|
void UpdateCycling()
|
||
|
|
{
|
||
|
|
var setup = isLocked ? ( lockedCyclingSetup ?? cyclingSetup ) : ( cyclingSetup ?? lockedCyclingSetup );
|
||
|
|
|
||
|
|
if ( setup == null )
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var cycled = false;
|
||
|
|
|
||
|
|
if ( setup.cycleMode == LockOnCycleMode.ClosestCycle )
|
||
|
|
{
|
||
|
|
if ( setup.cycleSensor != null && setup.cycleSensor.isDown )
|
||
|
|
{
|
||
|
|
source.CycleNext();
|
||
|
|
cycled = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else if ( setup.cycleMode == LockOnCycleMode.Directional && setup.directionalCycleInputs != null )
|
||
|
|
{
|
||
|
|
for ( int i = 0; i < setup.directionalCycleInputs.Length; i++ )
|
||
|
|
{
|
||
|
|
var input = setup.directionalCycleInputs[ i ];
|
||
|
|
|
||
|
|
if ( input == null || input.sensor == null || ! input.sensor.isDown )
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
source.CycleInScreenSpaceDirection( input.direction );
|
||
|
|
cycled = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Cycling is the only thing that is allowed to move an already-established lock onto a
|
||
|
|
// different candidate - a deliberate, one-time assignment here, not a per-frame resync.
|
||
|
|
if ( cycled && isLocked )
|
||
|
|
{
|
||
|
|
lockTarget = source.pointable;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void UpdateEngagement()
|
||
|
|
{
|
||
|
|
var engaged = engageSensor != null && engageSensor.isActive;
|
||
|
|
var risingEdge = engaged && ! _wasEngaged;
|
||
|
|
_wasEngaged = engaged;
|
||
|
|
|
||
|
|
if ( ! engaged )
|
||
|
|
{
|
||
|
|
lockTarget = null;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Only a fresh press/toggle-on acquires a target. Once locked, the lock is held
|
||
|
|
// regardless of what source.pointable does on its own (e.g. losing/regaining the raycast
|
||
|
|
// hit) - it only changes via cycling, auto-release, or an explicit Release() call.
|
||
|
|
if ( lockTarget == null && risingEdge )
|
||
|
|
{
|
||
|
|
lockTarget = source.pointable;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void UpdateAutoRelease()
|
||
|
|
{
|
||
|
|
if ( lockTarget == null )
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( IsGone( lockTarget ) )
|
||
|
|
{
|
||
|
|
lockTarget = null;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( IsTooFar( lockTarget ) )
|
||
|
|
{
|
||
|
|
lockTarget = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Covers both an actually freed node and one that was removed from the tree without being
|
||
|
|
// freed (e.g. pooled/recycled enemies) - either way it is no longer something the lock
|
||
|
|
// should keep pointing at.
|
||
|
|
static bool IsGone( Pointable target )
|
||
|
|
{
|
||
|
|
return target == null || ! IsInstanceValid( target ) || ! target.IsInsideTree();
|
||
|
|
}
|
||
|
|
|
||
|
|
bool IsTooFar( Pointable target )
|
||
|
|
{
|
||
|
|
if ( releaseDistance <= 0f )
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
var origin = releaseDistanceOrigin ?? source.caster;
|
||
|
|
|
||
|
|
if ( origin == null )
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return origin.GlobalPosition.DistanceTo( target.GlobalPosition ) > releaseDistance;
|
||
|
|
}
|
||
|
|
|
||
|
|
void ReleaseEngageSensorIfToggled()
|
||
|
|
{
|
||
|
|
if ( engageSensor is ActivationModeSensor ams && ams.isActive )
|
||
|
|
{
|
||
|
|
ams.Release();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void FireLockEvents( bool wasLocked, Pointable previousTarget )
|
||
|
|
{
|
||
|
|
var isLockedNow = lockTarget != null;
|
||
|
|
|
||
|
|
if ( isLockedNow && ! wasLocked )
|
||
|
|
{
|
||
|
|
Action.TriggerSafe( onLock );
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( ! isLockedNow && wasLocked )
|
||
|
|
{
|
||
|
|
ReleaseEngageSensorIfToggled();
|
||
|
|
Action.TriggerSafe( onReleasedLock );
|
||
|
|
|
||
|
|
if ( source.IsCandidate( previousTarget ) )
|
||
|
|
{
|
||
|
|
Action.TriggerSafe( onCandidateUnlocked );
|
||
|
|
}
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( isLockedNow && wasLocked && lockTarget != previousTarget )
|
||
|
|
{
|
||
|
|
Action.TriggerSafe( onChangeLocked );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void UpdateVisualizer()
|
||
|
|
{
|
||
|
|
if ( visualizerMarker == null )
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var current = lockTarget ?? source.pointable;
|
||
|
|
|
||
|
|
if ( ! IsGone( current ) )
|
||
|
|
{
|
||
|
|
visualizerMarker.CopyGlobalPoseFrom( current.pointerPosition );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|