diff --git a/Runtime/Actions/LogMessage.cs b/Runtime/Actions/LogMessage.cs index 2844178..5f1dac5 100644 --- a/Runtime/Actions/LogMessage.cs +++ b/Runtime/Actions/LogMessage.cs @@ -13,6 +13,9 @@ namespace Rokojori [Export] public string message; + [Export] + public Node node; + [Export] public bool printPath; @@ -20,11 +23,27 @@ namespace Rokojori { if ( printPath ) { - this.LogInfo( message ); + if ( node != null ) + { + this.LogInfo( message, node ); + } + else + { + this.LogInfo( message ); + } + } else { - RJLog.Log( message ); + if ( node != null ) + { + RJLog.Log( message, node ); + } + else + { + RJLog.Log( message ); + } + } } } diff --git a/Runtime/Actions/Node3D/LookAt.cs b/Runtime/Actions/Node3D/LookAt.cs index 8de554b..5824a14 100644 --- a/Runtime/Actions/Node3D/LookAt.cs +++ b/Runtime/Actions/Node3D/LookAt.cs @@ -13,6 +13,8 @@ namespace Rokojori [Export] public Node3D lookTarget; + [Export] + public bool useModelFront = false; protected override void _OnTrigger() { @@ -21,7 +23,7 @@ namespace Rokojori return; } - lookFrom.LookAt( lookTarget.GlobalPosition ); + lookFrom.LookAt( lookTarget.GlobalPosition, Vector3.Up, useModelFront ); } } } \ No newline at end of file diff --git a/Runtime/Actions/Node3D/SetNode3D.cs b/Runtime/Actions/Node3D/SetNode3D.cs new file mode 100644 index 0000000..d551d85 --- /dev/null +++ b/Runtime/Actions/Node3D/SetNode3D.cs @@ -0,0 +1,39 @@ + +using Godot; + + +using Rokojori.Extensions; +namespace Rokojori +{ + [GlobalClass, Tool ] + public partial class SetNode3D : Action + { + [Export] + public Node source; + + [Export] + public string sourceMember; + + + [Export] + public Node target; + + [Export] + public string targetMember; + + + + protected override void _OnTrigger() + { + if ( target == null || source == null ) + { + return; + } + + var value = ReflectionHelper.GetValue( source, sourceMember ); + + ReflectionHelper.SetValue( target, targetMember, value ); + + } + } +} \ No newline at end of file diff --git a/Runtime/Actions/Node3D/SetNode3D.cs.uid b/Runtime/Actions/Node3D/SetNode3D.cs.uid new file mode 100644 index 0000000..63af8ed --- /dev/null +++ b/Runtime/Actions/Node3D/SetNode3D.cs.uid @@ -0,0 +1 @@ +uid://dnx8jc8lsfwoi diff --git a/Runtime/Actions/Values/NumberMath.cs b/Runtime/Actions/Values/NumberMath.cs new file mode 100644 index 0000000..aaa548f --- /dev/null +++ b/Runtime/Actions/Values/NumberMath.cs @@ -0,0 +1,72 @@ + +using Godot; +using System.Collections.Generic; +using System.IO; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Action.svg")] +public partial class NumberMath:Action +{ + public enum Operator + { + Set, + Add, + Substract, + Multiply, + Divide, + Modulo, + Freeze, + Unfreeze, + Reset + } + + [Export] + public NumberValue target; + + [Export] + public Operator operation = Operator.Set; + + public enum Order + { + Target_Op_Source, + Source_Op_Target + } + + [Export] + public Order order = Order.Target_Op_Source; + + [Export] + public double sourceValue; + + [Export] + public NumberValue sourceReference; + + protected override void _OnTrigger() + { + var valueRight = ( sourceReference == null ? 0 : sourceReference.value ) + sourceValue; + var valueLeft = target.value; + + if ( order == Order.Source_Op_Target ) + { + var buffer = valueLeft; + valueLeft = valueRight; + valueRight = buffer; + } + + switch ( operation ) + { + case Operator.Set: { target.value = valueRight; } break; + case Operator.Add: { target.value = valueLeft + valueRight; } break; + case Operator.Substract: { target.value = valueLeft - valueRight; } break; + case Operator.Multiply: { target.value = valueLeft * valueRight; } break; + case Operator.Divide: { target.value = valueLeft / valueRight; } break; + case Operator.Modulo: { target.value = valueLeft % valueRight; } break; + case Operator.Freeze: { target.FreezeValue(); } break; + case Operator.Unfreeze: { target.UnfreezeValue(); } break; + case Operator.Reset: { target.ResetValue(); } break; + } + } +} \ No newline at end of file diff --git a/Runtime/Actions/Values/NumberMath.cs.uid b/Runtime/Actions/Values/NumberMath.cs.uid new file mode 100644 index 0000000..817b206 --- /dev/null +++ b/Runtime/Actions/Values/NumberMath.cs.uid @@ -0,0 +1 @@ +uid://b7uiy1j04s3fu diff --git a/Runtime/Actions/Values/NumberValue.cs b/Runtime/Actions/Values/NumberValue.cs new file mode 100644 index 0000000..1a9835c --- /dev/null +++ b/Runtime/Actions/Values/NumberValue.cs @@ -0,0 +1,134 @@ + +using Godot; + + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool][GlobalClass] +public partial class NumberValue : Node +{ + protected double _value = 0; + protected bool _frozen; + + [Export] + public double value + { + get => _value; + set { _OnValueChange( value ); } + } + + public override string ToString() + { + return GetType().Name + " { value = " + _value._DDD() + ", frozen = " + _frozen + " }"; + } + + void _OnValueChange( double v ) + { + if ( _frozen ) + { + return; + } + + var setterValue = Mathf.Clamp( v, minValue, maxValue ); + + if ( _value == setterValue ) + { + return; + } + + var before = _value; + _value = setterValue; + + var belowLowTreshold = before < lowTreshold; + var aboveHighTreshold = before > highTreshold; + + onChange?.Trigger(); + + if ( ! belowLowTreshold && _value < lowTreshold ) + { + onBelowLowTreshold?.Trigger(); + } + + if ( ! aboveHighTreshold && _value > highTreshold ) + { + onAboveHighTreshold?.Trigger(); + } + + if ( _value == minValue ) + { + onMin?.Trigger(); + + if ( autoFreezeOnMin ) + { + _frozen = true; + } + } + + if ( _value == maxValue ) + { + onMax?.Trigger(); + } + + } + + public void FreezeValue() + { + this._frozen = true; + } + + public void UnfreezeValue() + { + this._frozen = false; + } + + public void ResetValue() + { + this._frozen = false; + this._value = resetValue; + } + + [Export] + public double resetValue = 100; + + [Export] + public double minValue = 0; + + [Export] + public double maxValue = 125; + + [Export] + public double lowTreshold = 25; + + [Export] + public double highTreshold = 75; + + [Export] + public bool autoFreezeOnMin = true; + + + [Export] + public Action onChange; + + [Export] + public Action onIncrement; + + [Export] + public Action onDecrement; + + [Export] + public Action onMin; + + [Export] + public Action onMax; + + [Export] + public Action onBelowLowTreshold; + + [Export] + public Action onAboveHighTreshold; + + + +} + diff --git a/Runtime/Actions/Values/NumberValue.cs.uid b/Runtime/Actions/Values/NumberValue.cs.uid new file mode 100644 index 0000000..28adfd0 --- /dev/null +++ b/Runtime/Actions/Values/NumberValue.cs.uid @@ -0,0 +1 @@ +uid://bg8bavpgdo0r6 diff --git a/Runtime/Cameras/CameraTypes/LockOnCamera/LockOnCamera.cs b/Runtime/Cameras/CameraTypes/LockOnCamera/LockOnCamera.cs new file mode 100644 index 0000000..f277c00 --- /dev/null +++ b/Runtime/Cameras/CameraTypes/LockOnCamera/LockOnCamera.cs @@ -0,0 +1,131 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori +{ + [Tool] + [GlobalClass] + public partial class LockOnCamera : VirtualCamera + { + [Export] + public Node3D playerTarget; + + [Export] + public LockOnPointer source; + + [Export] + public Node3D lockTarget; + + [Export] + public LockOnCameraSettings settings; + + [Export] + public CharacterController.CharacterUpdateMode updateMode = CharacterController.CharacterUpdateMode.Physics_Process; + + [Export] + public bool processInEditor = false; + + public bool canProcess => processInEditor || ! Engine.IsEditorHint(); + + public void SetLockTarget( Node3D target ) + { + lockTarget = target; + } + + Node3D CurrentLockTarget() + { + if ( source != null && source.pointable != null ) + { + return source.pointable; + } + + return lockTarget; + } + + public override void _Process( double delta ) + { + if ( canProcess && CharacterController.CharacterUpdateMode.Process == updateMode ) + { + _ProcessCamera( delta ); + } + } + + public override void _PhysicsProcess( double delta ) + { + if ( canProcess && CharacterController.CharacterUpdateMode.Physics_Process == updateMode ) + { + _ProcessCamera( delta ); + } + } + + void _ProcessCamera( double gameDelta ) + { + if ( playerTarget == null || settings == null ) + { + return; + } + + var delta = (float) gameDelta; + var target = CurrentLockTarget(); + + var playerPosition = Smoothing.Apply( settings.playerFollowSmoothing, playerTarget.GlobalPosition, delta ); + + Vector3 axisDirection; + Vector3 lookAtPoint; + + if ( target != null ) + { + var targetPosition = Smoothing.Apply( settings.targetFollowSmoothing, target.GlobalPosition, delta ); + var offset = targetPosition - playerPosition; + var flat = new Vector3( offset.X, 0, offset.Z ); + + axisDirection = flat.LengthSquared() > 0.0001f ? flat.Normalized() : -playerTarget.GlobalTransform.Basis.Z; + lookAtPoint = playerPosition.Lerp( targetPosition, settings.lookAtTargetWeight ); + } + else + { + axisDirection = -playerTarget.GlobalTransform.Basis.Z; + lookAtPoint = playerPosition; + } + + var side = axisDirection.Cross( Vector3.Up ); + + var desiredPosition = playerPosition - axisDirection * settings.distance + Vector3.Up * settings.height + side * settings.sideOffset; + + cameraPosition = Smoothing.Apply( settings.positionSmoothing, desiredPosition, delta ); + + lookAtPoint.Y += settings.lookAtHeight; + + cameraForward = ( lookAtPoint - cameraPosition ).Normalized(); + + var basis = Basis.LookingAt( cameraForward, Vector3.Up, true ); + cameraRotation = new Quaternion( basis ); + + FOV = settings.fov; + } + + public Vector3 cameraPosition; + + public override Vector3 GetCameraPosition() + { + return cameraPosition; + } + + public Vector3 cameraForward; + + public Quaternion cameraRotation; + + public override Quaternion GetCameraRotation() + { + return cameraRotation; + } + + [Export] + public float FOV = 60f; + + public override float GetCameraFOV() + { + return FOV; + } + } +} diff --git a/Runtime/Cameras/CameraTypes/LockOnCamera/LockOnCamera.cs.uid b/Runtime/Cameras/CameraTypes/LockOnCamera/LockOnCamera.cs.uid new file mode 100644 index 0000000..739981d --- /dev/null +++ b/Runtime/Cameras/CameraTypes/LockOnCamera/LockOnCamera.cs.uid @@ -0,0 +1 @@ +uid://b5o54ra4cgj2k diff --git a/Runtime/Cameras/CameraTypes/LockOnCamera/LockOnCameraSettings.cs b/Runtime/Cameras/CameraTypes/LockOnCamera/LockOnCameraSettings.cs new file mode 100644 index 0000000..76dfded --- /dev/null +++ b/Runtime/Cameras/CameraTypes/LockOnCamera/LockOnCameraSettings.cs @@ -0,0 +1,40 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori +{ + [Tool] + [GlobalClass] + public partial class LockOnCameraSettings : Resource + { + [Export] + public float distance = 6f; + + [Export] + public float height = 2f; + + [Export] + public float sideOffset = 0f; + + [ExportGroup( "Look At" )] + [Export(PropertyHint.Range, "0,1")] + public float lookAtTargetWeight = 0.5f; + + [Export] + public float lookAtHeight = 0f; + + [ExportGroup( "FOV" )] + [Export] + public float fov = 60f; + + [ExportGroup( "Smoothing" )] + [Export] + public Smoothing playerFollowSmoothing = new FrameSmoothing(); + + [Export] + public Smoothing targetFollowSmoothing = new FrameSmoothing(); + + [Export] + public Smoothing positionSmoothing = new FrameSmoothing(); + } +} diff --git a/Runtime/Cameras/CameraTypes/LockOnCamera/LockOnCameraSettings.cs.uid b/Runtime/Cameras/CameraTypes/LockOnCamera/LockOnCameraSettings.cs.uid new file mode 100644 index 0000000..8ebabb6 --- /dev/null +++ b/Runtime/Cameras/CameraTypes/LockOnCamera/LockOnCameraSettings.cs.uid @@ -0,0 +1 @@ +uid://cqa8ktsmoa041 diff --git a/Runtime/Cameras/CameraTypes/ThirdPersonCamera/SetThirdPersonCameraLockTarget.cs b/Runtime/Cameras/CameraTypes/ThirdPersonCamera/SetThirdPersonCameraLockTarget.cs new file mode 100644 index 0000000..5f4fb06 --- /dev/null +++ b/Runtime/Cameras/CameraTypes/ThirdPersonCamera/SetThirdPersonCameraLockTarget.cs @@ -0,0 +1,41 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori +{ + public enum ThirdPersonCameraLockMode + { + Lock, + Release + } + + [Tool] + [GlobalClass] + public partial class SetThirdPersonCameraLockTarget : Action + { + [Export] + public ThirdPersonCamera camera; + + [Export] + public Pointer source; + + [Export] + public ThirdPersonCameraLockMode mode = ThirdPersonCameraLockMode.Lock; + + protected override void _OnTrigger() + { + if ( camera == null ) + { + return; + } + + if ( mode == ThirdPersonCameraLockMode.Release ) + { + camera.lockTarget = null; + return; + } + + camera.lockTarget = source != null && source.pointable != null ? source.pointable.pointerPosition : null; + } + } +} diff --git a/Runtime/Cameras/CameraTypes/ThirdPersonCamera/SetThirdPersonCameraLockTarget.cs.uid b/Runtime/Cameras/CameraTypes/ThirdPersonCamera/SetThirdPersonCameraLockTarget.cs.uid new file mode 100644 index 0000000..12fb438 --- /dev/null +++ b/Runtime/Cameras/CameraTypes/ThirdPersonCamera/SetThirdPersonCameraLockTarget.cs.uid @@ -0,0 +1 @@ +uid://pesdcfjnpi30 diff --git a/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCamera.cs b/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCamera.cs index 4fd3376..646e26f 100644 --- a/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCamera.cs +++ b/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCamera.cs @@ -15,7 +15,10 @@ namespace Rokojori { [Export] public Node3D target; - + + [Export] + public Node3D lockTarget; + [Export] public float yaw; @@ -35,6 +38,8 @@ namespace Rokojori public bool canProcess => processInEditor || ! Engine.IsEditorHint(); + Vector3 _lastLockTargetPosition; + public override void _Process( double delta ) { if ( canProcess && CharacterController.CharacterUpdateMode.Process == updateMode ) @@ -71,6 +76,30 @@ namespace Rokojori var targetPosition = Smoothing.Apply( settings.targetFollowSmoothing, target.GlobalPosition, delta ); + var hasLockTarget = lockTarget != null; + var lockTargetPosition = _lastLockTargetPosition; + + if ( hasLockTarget ) + { + lockTargetPosition = Smoothing.Apply( settings.lockTargetFollowSmoothing, lockTarget.GlobalPosition, delta ); + _lastLockTargetPosition = lockTargetPosition; + + if ( settings.lockReleaseDistance > 0f && ( lockTargetPosition - targetPosition ).Length() > settings.lockReleaseDistance ) + { + lockTarget = null; + hasLockTarget = false; + } + } + + // lockBlend fades 0->1 on engage and 1->0 on release instead of snapping, so the focus + // point, yaw override, pitch clamp and bounding-distance below all ease in/out together - + // lockTargetPosition keeps the last known value through the fade-out tail even after + // lockTarget itself has gone null. + var lockBlend = Smoothing.Apply( settings.lockBlendSmoothing, hasLockTarget ? 1f : 0f, delta ); + var isLocked = lockBlend > 0.001f; + + var focusPosition = targetPosition.Lerp( ( targetPosition + lockTargetPosition ) / 2f, lockBlend ); + var yawAxis = 0f; var pitchAxis = 0f; @@ -80,20 +109,38 @@ namespace Rokojori pitchAxis = Sensors.PolarPowerAxis( data.pitchNegativeAxis, data.pitchPositiveAxis, 1f, data.pitchDeadZone, data.pitchPower ); } - // this.LogInfo( "YAW:", yawAxis, "PITCH:", pitchAxis ); - yaw += yawAxis * settings.yawSpeed * data.yawSpeed * delta; - + // this.LogInfo( "YAW:", yawAxis, "PITCH:", pitchAxis ); + if ( isLocked ) + { + var lockDirection = lockTargetPosition - targetPosition; + lockDirection.Y = 0; + + if ( lockDirection.LengthSquared() > 0.0001f ) + { + yaw = Math3D.GlobalYawDegrees( lockDirection ); + } + } + else + { + yaw += yawAxis * settings.yawSpeed * data.yawSpeed * delta; + } + if ( data.pitchIsRelative ) { - pitch += pitchAxis * settings.relativePitchSpeed * data.relativePitchSpeed * delta; + pitch += pitchAxis * settings.relativePitchSpeed * data.relativePitchSpeed * delta; pitch = Mathf.Clamp( pitch, settings.minPitch, settings.maxPitch ); } else - { + { pitch = Mathf.Remap( pitchAxis, -1, 1, settings.minPitch, settings.maxPitch ); } + if ( isLocked ) + { + pitch = Mathf.Clamp( pitch, settings.lockMinPitch, settings.lockMaxPitch ); + } + var appliedYaw = yaw; var smoothedYaw = Smoothing.ApplyDegrees( settings.yawSmoothing, appliedYaw, delta ); @@ -103,16 +150,35 @@ namespace Rokojori var pitchState = MathX.NormalizeClamped( pitch, settings.minPitch, settings.maxPitch ); - var distance = settings.distanceForPitch.Sample( pitchState ) * settings.distanceScale; - var smoothedDistance = Smoothing.Apply( settings.distanceSmoothing, distance, delta ); - FOV = settings.fovOffset + ( settings.fovForPitch?.Sample( pitchState ) ?? 0f ); + FOV = settings.fovOffset + ( settings.fovForPitch?.Sample( pitchState ) ?? 0f ); + + var desiredDistance = settings.distanceForPitch.Sample( pitchState ) * settings.distanceScale; + + if ( isLocked ) + { + // The camera always looks exactly at focusPosition (cameraForward is -fowardDirection by + // construction below), so fitting a bounding sphere around target+lockTarget in the view + // cone is pitch-independent - unlike pulling back along a single axis, it cannot + // degenerate when the view direction is near-vertical. radius/sin(halfFov) is the + // standard distance that makes a sphere of that radius exactly fill the frame. + var boundingRadius = ( lockTargetPosition - targetPosition ).Length() * 0.5f + settings.lockBoundsPadding; + var halfFovRadians = Mathf.DegToRad( FOV * 0.5f ); + + if ( boundingRadius > 0f && halfFovRadians > 0.001f ) + { + var minDistanceForBounds = boundingRadius / Mathf.Sin( halfFovRadians ); + desiredDistance = Mathf.Lerp( desiredDistance, Mathf.Max( desiredDistance, minDistanceForBounds ), lockBlend ); + } + } + + var smoothedDistance = Smoothing.Apply( settings.distanceSmoothing, desiredDistance, delta ); var fowardDirection = Math3D.YawPitchRotation( smoothedYaw, smoothedPitch ) * Vector3.Forward; - cameraPosition = targetPosition + fowardDirection * smoothedDistance; + cameraPosition = focusPosition + fowardDirection * smoothedDistance; - cameraForward = ( targetPosition - cameraPosition ).Normalized(); + cameraForward = ( focusPosition - cameraPosition ).Normalized(); // LookAt( targetPosition, Vector3.Up, true ); var pitchOffset = settings.offset; diff --git a/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCameraSettings.cs b/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCameraSettings.cs index f2a3c95..c8e93a0 100644 --- a/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCameraSettings.cs +++ b/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCameraSettings.cs @@ -68,11 +68,32 @@ namespace Rokojori [Export] public Curve fovForPitch = MathX.Curve( 0, 0 ).WithValueRange( -10, 10 ); - + [Export] public float fovOffset = 60; + [ExportGroup("Lock")] + + [Export] + public Smoothing lockTargetFollowSmoothing = new FrameSmoothing(); + + [Export] + public Smoothing lockBlendSmoothing = new FrameSmoothing(); + + [Export] + public float lockReleaseDistance = 0f; + + [Export] + public float lockMinPitch = -10; + + [Export] + public float lockMaxPitch = 45; + + [Export] + public float lockBoundsPadding = 0f; + + public float GetNormalizedPitch( float pitch ) { return MathX.Normalize( pitch, minPitch, maxPitch ); diff --git a/Runtime/Interactions/CharacterController/CharacterMovement.cs b/Runtime/Interactions/CharacterController/CharacterMovement.cs index 0157bf5..46d15e9 100644 --- a/Runtime/Interactions/CharacterController/CharacterMovement.cs +++ b/Runtime/Interactions/CharacterController/CharacterMovement.cs @@ -51,6 +51,13 @@ namespace Rokojori [Export] public CharacterMovementType[] deviceMovementTypes = []; + [ExportGroup( "Manual Movement Type Switch" )] + [Export] + public Sensor movementTypeSwitch; + + [Export] + public CharacterMovementType switchedMovementType; + [Export] public Smoothing onFloorMovementSmoothing = new FrameSmoothing(); @@ -119,6 +126,11 @@ namespace Rokojori currentMovementType = deviceMovementTypes.FindNonNull( d => d.deviceSelector.Selects( activeDevice ) ); + if ( movementTypeSwitch != null && movementTypeSwitch.isActive && switchedMovementType != null ) + { + currentMovementType = switchedMovementType; + } + if ( currentMovementType == null ) { // this.LogInfo( "No device found:", activeDevice == null ? "null" : activeDevice.GetType().Name ); diff --git a/Runtime/Interactions/LockOn/DirectionalCycleInput.cs b/Runtime/Interactions/LockOn/DirectionalCycleInput.cs new file mode 100644 index 0000000..4fe14b9 --- /dev/null +++ b/Runtime/Interactions/LockOn/DirectionalCycleInput.cs @@ -0,0 +1,16 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori +{ + [Tool] + [GlobalClass] + public partial class DirectionalCycleInput : Resource + { + [Export] + public Sensor sensor; + + [Export] + public Vector2 direction = Vector2.Right; + } +} diff --git a/Runtime/Interactions/LockOn/DirectionalCycleInput.cs.uid b/Runtime/Interactions/LockOn/DirectionalCycleInput.cs.uid new file mode 100644 index 0000000..708d2fb --- /dev/null +++ b/Runtime/Interactions/LockOn/DirectionalCycleInput.cs.uid @@ -0,0 +1 @@ +uid://dc6l8lylcqdtv diff --git a/Runtime/Interactions/LockOn/LockOnPointer.cs b/Runtime/Interactions/LockOn/LockOnPointer.cs new file mode 100644 index 0000000..c423147 --- /dev/null +++ b/Runtime/Interactions/LockOn/LockOnPointer.cs @@ -0,0 +1,289 @@ +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; + } + } +} diff --git a/Runtime/Interactions/LockOn/LockOnPointer.cs.uid b/Runtime/Interactions/LockOn/LockOnPointer.cs.uid new file mode 100644 index 0000000..297acf3 --- /dev/null +++ b/Runtime/Interactions/LockOn/LockOnPointer.cs.uid @@ -0,0 +1 @@ +uid://c0oeivnw2q0a5 diff --git a/Runtime/Interactions/Pointable.cs b/Runtime/Interactions/Pointable.cs index d4f6e1a..83967aa 100644 --- a/Runtime/Interactions/Pointable.cs +++ b/Runtime/Interactions/Pointable.cs @@ -33,6 +33,11 @@ namespace Rokojori [Export] public Action onPointerRemoved; + [Export] + public Node3D pointerPositionOverride; + + public Node3D pointerPosition => pointerPositionOverride ?? this; + [ExportGroup("Highlighting")] [Export] public HighlightEffect highlightEffect; diff --git a/Runtime/Interactions/Pointer.cs b/Runtime/Interactions/Pointer.cs index 1e8e827..9c06c4d 100644 --- a/Runtime/Interactions/Pointer.cs +++ b/Runtime/Interactions/Pointer.cs @@ -17,11 +17,25 @@ namespace Rokojori [Export] public HighlightEffect defaultHighlighter; + [Export] + public Node3D visualizer; + + [Export] + public Action onPointed; + + [Export] + public Action onUnpointed; + + [Export] + public Action onPointerChanged; + [ExportGroup("Read Only")] [Export] public Pointable pointable; + + public readonly EventSlot onPointerChange = new EventSlot(); @@ -29,7 +43,7 @@ namespace Rokojori public bool printPointables = false; protected override void _OnTrigger() - { + { if ( caster == null ) { return; @@ -37,32 +51,61 @@ namespace Rokojori var currentCollider = caster.NumColliders() == 0 ? null : caster.GetCollider( 0 ); + var currentPointable = currentCollider == null ? null : Nodes.Find( currentCollider ); - var currentPointable = currentCollider == null ? null : Nodes.Find( currentCollider ); + SetPointable( currentPointable ); - // RJLog.Log( currentCollider, currentPointable ); + + } - if ( currentPointable == pointable ) + + + protected void SetPointable( Pointable next ) + { + if ( next == pointable ) { + if ( visualizer != null && pointable != null ) + { + var pointable3D = pointable.pointerPosition; + visualizer.CopyGlobalPoseFrom( pointable3D ); + } + return; } - if ( pointable != null ) - { + { pointable.UpdatePointerState( this, false ); Highlight( HighlightActionType.End, pointable ); } - pointable = currentPointable; + pointable = next; if ( pointable != null ) - { - pointable.UpdatePointerState( this, true ); + { + pointable.UpdatePointerState( this, true ); Highlight( HighlightActionType.Start, pointable ); } - onPointerChange.DispatchEvent( currentPointable ); + + + onPointerChange.DispatchEvent( pointable ); + onPointerChanged?.Trigger(); + + if ( pointable == null ) + { + onUnpointed?.Trigger(); + } + else + { + onPointed?.Trigger(); + } + + if ( visualizer != null && pointable != null ) + { + var pointable3D = pointable.pointerPosition; + visualizer.CopyGlobalPoseFrom( pointable3D ); + } if ( printPointables ) { diff --git a/Runtime/Sensors/ActivationModeSensor.cs b/Runtime/Sensors/ActivationModeSensor.cs new file mode 100644 index 0000000..bbc7c05 --- /dev/null +++ b/Runtime/Sensors/ActivationModeSensor.cs @@ -0,0 +1,63 @@ + +using Godot; +using System.Collections.Generic; + +using Rokojori.Extensions; +namespace Rokojori +{ + public enum ActivationMode + { + Hold, + Toggle + } + + [Tool] + [GlobalClass] + public partial class ActivationModeSensor : Sensor + { + [Export] + public Sensor button; + + [Export] + public ActivationMode mode = ActivationMode.Hold; + + bool _toggledOn = false; + + public override bool IsSensor( InputEvent ie ) + { + return button != null && button.IsSensor( ie ); + } + + public void Release() + { + _toggledOn = false; + } + + protected override void UpdateValue() + { + if ( button == null ) + { + SetBoolValue( false ); + return; + } + + if ( mode == ActivationMode.Hold ) + { + SetFloatValue( button.value ); + return; + } + + if ( button.isDown ) + { + _toggledOn = ! _toggledOn; + } + + SetBoolValue( _toggledOn ); + } + + public override List GetInputIcons() + { + return button == null ? new List() : button.GetInputIcons(); + } + } +} diff --git a/Runtime/Sensors/ActivationModeSensor.cs.uid b/Runtime/Sensors/ActivationModeSensor.cs.uid new file mode 100644 index 0000000..c88cdc3 --- /dev/null +++ b/Runtime/Sensors/ActivationModeSensor.cs.uid @@ -0,0 +1 @@ +uid://b4eorkinrt8kv diff --git a/Runtime/Sensors/ReleaseActivationModeSensor.cs b/Runtime/Sensors/ReleaseActivationModeSensor.cs new file mode 100644 index 0000000..2c2866e --- /dev/null +++ b/Runtime/Sensors/ReleaseActivationModeSensor.cs @@ -0,0 +1,21 @@ + +using Godot; +using System.Collections.Generic; + +using Rokojori.Extensions; +namespace Rokojori +{ + + [Tool] + [GlobalClass] + public partial class ReleaseActivationModeSensor : Action + { + [Export] + public ActivationModeSensor activationModeSensor; + + protected override void _OnTrigger() + { + activationModeSensor?.Release(); + } + } +} \ No newline at end of file diff --git a/Runtime/Sensors/ReleaseActivationModeSensor.cs.uid b/Runtime/Sensors/ReleaseActivationModeSensor.cs.uid new file mode 100644 index 0000000..5f67472 --- /dev/null +++ b/Runtime/Sensors/ReleaseActivationModeSensor.cs.uid @@ -0,0 +1 @@ +uid://bmhvyxsurpcql diff --git a/Runtime/Time/TimelineManager.cs b/Runtime/Time/TimelineManager.cs index acfcd5b..bee5152 100644 --- a/Runtime/Time/TimelineManager.cs +++ b/Runtime/Time/TimelineManager.cs @@ -214,7 +214,7 @@ public partial class TimelineManager:NetworkNode { var inManager = timelines.Contains( timeline ); - RJLog.Log( "No runner found for:", timeline, "Is in manager:", inManager ); + // RJLog.Log( "No runner found for:", timeline, "Is in manager:", inManager ); } return runner; diff --git a/Runtime/VFX/FlashVFX/FlashVFXPresets/Fresnel/Orange Fresnel Flash.tres b/Runtime/VFX/FlashVFX/FlashVFXPresets/Fresnel/Orange Fresnel Flash.tres index 47addcf..ebd66bd 100644 --- a/Runtime/VFX/FlashVFX/FlashVFXPresets/Fresnel/Orange Fresnel Flash.tres +++ b/Runtime/VFX/FlashVFX/FlashVFXPresets/Fresnel/Orange Fresnel Flash.tres @@ -11,7 +11,7 @@ colors = PackedColorArray(0.96, 0.9211765, 0.24000007, 1, 1, 0.19607843, 0, 1, 0 [sub_resource type="Resource" id="Resource_w2g18"] script = ExtResource("1_b51fw") -baseColor = Color(1, 0.19607843, 0, 1) +baseColor = Color(1, 0.5342896, 0, 1) centerAttraction = 0.0 temperaturePolarBlendRadius = 30.0 startTemperatureShift = 45.0 diff --git a/Runtime/VFX/ParticleVFX/Embers/Ember Circle.gdshader b/Runtime/VFX/ParticleVFX/Embers/Ember Circle.gdshader index 4fcf307..a726966 100644 --- a/Runtime/VFX/ParticleVFX/Embers/Ember Circle.gdshader +++ b/Runtime/VFX/ParticleVFX/Embers/Ember Circle.gdshader @@ -29,11 +29,11 @@ void fragment() { float d = distance( UV, vec2(0.5) ); - float a = 1.0 - clamp( d / 0.5, 0.0, 1.0); + float a = 1.0 - clamp( d / ( 0.5 * COLOR.a ), 0.0, 1.0); ALBEDO = COLOR.rgb; - ALPHA *= a * COLOR.a; + ALPHA *= a; ALPHA_SCISSOR_THRESHOLD = 0.01; } diff --git a/Runtime/VFX/ParticleVFX/Embers/EmberVFXData.cs b/Runtime/VFX/ParticleVFX/Embers/EmberVFXData.cs index 4b8f7dc..86c912a 100644 --- a/Runtime/VFX/ParticleVFX/Embers/EmberVFXData.cs +++ b/Runtime/VFX/ParticleVFX/Embers/EmberVFXData.cs @@ -70,7 +70,9 @@ public partial class EmberVFXData:ParticleVFXData UpdateCoreSettings( preset, particles ); - processMaterial.Color = preset.color * preset.colorIntensity; + var color = preset.color.ScaleColor( preset.colorIntensity ); + + processMaterial.Color = color; var rampTexture = processMaterial.ColorRamp as GradientTexture1D; diff --git a/Runtime/VFX/ParticleVFX/ParticleVFX.cs b/Runtime/VFX/ParticleVFX/ParticleVFX.cs index 2869204..8c9ec27 100644 --- a/Runtime/VFX/ParticleVFX/ParticleVFX.cs +++ b/Runtime/VFX/ParticleVFX/ParticleVFX.cs @@ -67,6 +67,11 @@ public partial class ParticleVFX:Node3D _data = data; } + public void Play() + { + preset.Play( this ); + } + } \ No newline at end of file diff --git a/Runtime/VFX/ParticleVFX/ParticleVFXControl.cs b/Runtime/VFX/ParticleVFX/ParticleVFXControl.cs new file mode 100644 index 0000000..3db9912 --- /dev/null +++ b/Runtime/VFX/ParticleVFX/ParticleVFXControl.cs @@ -0,0 +1,32 @@ + +using Godot; +using System.Collections.Generic; + +using Rokojori.Extensions; +namespace Rokojori +{ + [Tool][GlobalClass, Icon("res://addons/rokojori_action_library/Icons/PlayParticles.svg")] + public partial class ParticleVFXControl:Action + { + public enum ControlType + { + Play + } + + [Export] + public ParticleVFX particleVFX; + + [Export] + public ControlType controlType = ControlType.Play; + + protected override void _OnTrigger() + { + if ( ControlType.Play == controlType ) + { + particleVFX?.Play(); + } + + } + + } +} \ No newline at end of file diff --git a/Runtime/VFX/ParticleVFX/ParticleVFXControl.cs.uid b/Runtime/VFX/ParticleVFX/ParticleVFXControl.cs.uid new file mode 100644 index 0000000..0558c5d --- /dev/null +++ b/Runtime/VFX/ParticleVFX/ParticleVFXControl.cs.uid @@ -0,0 +1 @@ +uid://cw6q1qxm35l7l