From 211613e06b3806c1b60440f351da03cb75287cd9 Mon Sep 17 00:00:00 2001 From: Rokojori Date: Sun, 11 Oct 2026 09:51:12 +0200 Subject: [PATCH] NPC Behaviors: movement/fight classes, Smoother Quaternion fix, ThirdPersonCamera lock blend split - New Behavior/Movements: MoveOnPath, GoToPointBehavior, FleeBehavior, HidingSpot/HideAtSpotBehavior, NavigationBehaviorMovement; BehaviorMovement gained Flee/GetGlobalPosition, CharacterBodyBehaviorMovement moved from raw duration floats to Smoothing-resource exports. - New Behavior/Fight: FightManager/FightSlot/JoinFight/LeaveFight/IsEngagedInFight/HasAttackPermission/IsAllyAttacking/FightState, mirroring the CameraManager/CameraSlot singleton+slot pattern. - New Conditions/IsValueBelowThreshold (generic against NumberValue). - Fixed Smoother.SmoothWithCoefficient(Quaternion...) not renormalizing after Slerp (crashed live after sustained use) and a MathX.SmoothingCoefficient argument-order bug in all three SmoothForDuration overloads. - ThirdPersonCamera: lock engage/release now blends yaw/pitch (fixing a hard isLocked snap), fixed a yaw-staleness regression from that change, then split the single lock blend into independent focus (lockEngageSmoothing/lockReleaseSmoothing) and orientation (lockOrientationEngageSmoothing/lockOrientationReleaseSmoothing) blends. - Includes carried-over uncommitted state from the earlier NPC/AI/Behaviours task (Behavior/StateMachine, Receptor/ReceptorBase/ReceptorControl, related Icons) that hadn't been committed yet. Co-Authored-By: Claude Sonnet 5 --- Icons/OnStateActive.svg | 59 +++++ Icons/OnStateActive.svg.import | 43 ++++ Icons/OnStateEntered.svg | 94 +++++++ Icons/OnStateEntered.svg.import | 43 ++++ Icons/OnStateExited.svg | 61 +++++ Icons/OnStateExited.svg.import | 43 ++++ Icons/State.svg | 71 ++++++ Icons/State.svg.import | 43 ++++ Icons/StateMachine.svg | 150 +++++++++++ Icons/StateMachine.svg.import | 43 ++++ Icons/StateTransition.svg | 72 ++++++ Icons/StateTransition.svg.import | 43 ++++ Runtime/Behavior/Fight/FightManager.cs | 103 ++++++++ Runtime/Behavior/Fight/FightManager.cs.uid | 1 + Runtime/Behavior/Fight/FightSlot.cs | 48 ++++ Runtime/Behavior/Fight/FightSlot.cs.uid | 1 + Runtime/Behavior/Fight/FightState.cs | 13 + Runtime/Behavior/Fight/FightState.cs.uid | 1 + Runtime/Behavior/Fight/HasAttackPermission.cs | 17 ++ .../Behavior/Fight/HasAttackPermission.cs.uid | 1 + Runtime/Behavior/Fight/IsAllyAttacking.cs | 17 ++ Runtime/Behavior/Fight/IsAllyAttacking.cs.uid | 1 + Runtime/Behavior/Fight/IsEngagedInFight.cs | 17 ++ .../Behavior/Fight/IsEngagedInFight.cs.uid | 1 + Runtime/Behavior/Fight/JoinFight.cs | 17 ++ Runtime/Behavior/Fight/JoinFight.cs.uid | 1 + Runtime/Behavior/Fight/LeaveFight.cs | 17 ++ Runtime/Behavior/Fight/LeaveFight.cs.uid | 1 + .../Behavior/Movements/BehaviorMovement.cs | 6 +- .../CharacterBodyBehaviorMovement.cs | 93 +++++-- Runtime/Behavior/Movements/FleeBehavior.cs | 25 ++ .../Behavior/Movements/FleeBehavior.cs.uid | 1 + .../Behavior/Movements/GoToPointBehavior.cs | 65 +++++ .../Movements/GoToPointBehavior.cs.uid | 1 + .../Behavior/Movements/HideAtSpotBehavior.cs | 93 +++++++ .../Movements/HideAtSpotBehavior.cs.uid | 1 + Runtime/Behavior/Movements/HidingSpot.cs | 12 + Runtime/Behavior/Movements/HidingSpot.cs.uid | 1 + Runtime/Behavior/Movements/MoveOnPath.cs | 62 +++++ Runtime/Behavior/Movements/MoveOnPath.cs.uid | 1 + .../Movements/NavigationBehaviorMovement.cs | 189 ++++++++++++++ .../NavigationBehaviorMovement.cs.uid | 1 + Runtime/Behavior/Receptors/Receptor.cs | 96 +------- Runtime/Behavior/Receptors/ReceptorBase.cs | 109 ++++++++ .../Behavior/Receptors/ReceptorBase.cs.uid | 1 + Runtime/Behavior/Receptors/ReceptorControl.cs | 58 +++++ .../Behavior/Receptors/ReceptorControl.cs.uid | 1 + .../Behavior/StateMachine/OnStateActive.cs | 57 +++++ .../StateMachine/OnStateActive.cs.uid | 1 + .../Behavior/StateMachine/OnStateEntered.cs | 10 + .../StateMachine/OnStateEntered.cs.uid | 1 + .../Behavior/StateMachine/OnStateExited.cs | 10 + .../StateMachine/OnStateExited.cs.uid | 1 + Runtime/Behavior/StateMachine/State.cs | 10 + Runtime/Behavior/StateMachine/State.cs.uid | 1 + .../StateMachine/StateEnterExitAction.cs | 38 +++ .../StateMachine/StateEnterExitAction.cs.uid | 1 + Runtime/Behavior/StateMachine/StateMachine.cs | 232 ++++++++++++++++++ .../Behavior/StateMachine/StateMachine.cs.uid | 1 + .../Behavior/StateMachine/StateTransition.cs | 62 +++++ .../StateMachine/StateTransition.cs.uid | 1 + .../ThirdPersonCamera/ThirdPersonCamera.cs | 58 +++-- .../ThirdPersonCameraSettings.cs | 15 +- Runtime/Conditions/IsValueBelowThreshold.cs | 27 ++ .../Conditions/IsValueBelowThreshold.cs.uid | 1 + Runtime/Godot/Extensions/Node3DExtensions.cs | 13 + Runtime/Math/Smoother.cs | 14 +- Runtime/Physics/PhysicsRayCast.cs | 8 +- 68 files changed, 2266 insertions(+), 134 deletions(-) create mode 100644 Icons/OnStateActive.svg create mode 100644 Icons/OnStateActive.svg.import create mode 100644 Icons/OnStateEntered.svg create mode 100644 Icons/OnStateEntered.svg.import create mode 100644 Icons/OnStateExited.svg create mode 100644 Icons/OnStateExited.svg.import create mode 100644 Icons/State.svg create mode 100644 Icons/State.svg.import create mode 100644 Icons/StateMachine.svg create mode 100644 Icons/StateMachine.svg.import create mode 100644 Icons/StateTransition.svg create mode 100644 Icons/StateTransition.svg.import create mode 100644 Runtime/Behavior/Fight/FightManager.cs create mode 100644 Runtime/Behavior/Fight/FightManager.cs.uid create mode 100644 Runtime/Behavior/Fight/FightSlot.cs create mode 100644 Runtime/Behavior/Fight/FightSlot.cs.uid create mode 100644 Runtime/Behavior/Fight/FightState.cs create mode 100644 Runtime/Behavior/Fight/FightState.cs.uid create mode 100644 Runtime/Behavior/Fight/HasAttackPermission.cs create mode 100644 Runtime/Behavior/Fight/HasAttackPermission.cs.uid create mode 100644 Runtime/Behavior/Fight/IsAllyAttacking.cs create mode 100644 Runtime/Behavior/Fight/IsAllyAttacking.cs.uid create mode 100644 Runtime/Behavior/Fight/IsEngagedInFight.cs create mode 100644 Runtime/Behavior/Fight/IsEngagedInFight.cs.uid create mode 100644 Runtime/Behavior/Fight/JoinFight.cs create mode 100644 Runtime/Behavior/Fight/JoinFight.cs.uid create mode 100644 Runtime/Behavior/Fight/LeaveFight.cs create mode 100644 Runtime/Behavior/Fight/LeaveFight.cs.uid create mode 100644 Runtime/Behavior/Movements/FleeBehavior.cs create mode 100644 Runtime/Behavior/Movements/FleeBehavior.cs.uid create mode 100644 Runtime/Behavior/Movements/GoToPointBehavior.cs create mode 100644 Runtime/Behavior/Movements/GoToPointBehavior.cs.uid create mode 100644 Runtime/Behavior/Movements/HideAtSpotBehavior.cs create mode 100644 Runtime/Behavior/Movements/HideAtSpotBehavior.cs.uid create mode 100644 Runtime/Behavior/Movements/HidingSpot.cs create mode 100644 Runtime/Behavior/Movements/HidingSpot.cs.uid create mode 100644 Runtime/Behavior/Movements/MoveOnPath.cs create mode 100644 Runtime/Behavior/Movements/MoveOnPath.cs.uid create mode 100644 Runtime/Behavior/Movements/NavigationBehaviorMovement.cs create mode 100644 Runtime/Behavior/Movements/NavigationBehaviorMovement.cs.uid create mode 100644 Runtime/Behavior/Receptors/ReceptorBase.cs create mode 100644 Runtime/Behavior/Receptors/ReceptorBase.cs.uid create mode 100644 Runtime/Behavior/Receptors/ReceptorControl.cs create mode 100644 Runtime/Behavior/Receptors/ReceptorControl.cs.uid create mode 100644 Runtime/Behavior/StateMachine/OnStateActive.cs create mode 100644 Runtime/Behavior/StateMachine/OnStateActive.cs.uid create mode 100644 Runtime/Behavior/StateMachine/OnStateEntered.cs create mode 100644 Runtime/Behavior/StateMachine/OnStateEntered.cs.uid create mode 100644 Runtime/Behavior/StateMachine/OnStateExited.cs create mode 100644 Runtime/Behavior/StateMachine/OnStateExited.cs.uid create mode 100644 Runtime/Behavior/StateMachine/State.cs create mode 100644 Runtime/Behavior/StateMachine/State.cs.uid create mode 100644 Runtime/Behavior/StateMachine/StateEnterExitAction.cs create mode 100644 Runtime/Behavior/StateMachine/StateEnterExitAction.cs.uid create mode 100644 Runtime/Behavior/StateMachine/StateMachine.cs create mode 100644 Runtime/Behavior/StateMachine/StateMachine.cs.uid create mode 100644 Runtime/Behavior/StateMachine/StateTransition.cs create mode 100644 Runtime/Behavior/StateMachine/StateTransition.cs.uid create mode 100644 Runtime/Conditions/IsValueBelowThreshold.cs create mode 100644 Runtime/Conditions/IsValueBelowThreshold.cs.uid diff --git a/Icons/OnStateActive.svg b/Icons/OnStateActive.svg new file mode 100644 index 0000000..a78c625 --- /dev/null +++ b/Icons/OnStateActive.svg @@ -0,0 +1,59 @@ + + + + + + + diff --git a/Icons/OnStateActive.svg.import b/Icons/OnStateActive.svg.import new file mode 100644 index 0000000..7abf1f5 --- /dev/null +++ b/Icons/OnStateActive.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://pu48jird8x7e" +path="res://.godot/imported/OnStateActive.svg-44af1d9740b53d2319633872a1d84635.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/rokojori_action_library/Icons/OnStateActive.svg" +dest_files=["res://.godot/imported/OnStateActive.svg-44af1d9740b53d2319633872a1d84635.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/Icons/OnStateEntered.svg b/Icons/OnStateEntered.svg new file mode 100644 index 0000000..a719853 --- /dev/null +++ b/Icons/OnStateEntered.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + diff --git a/Icons/OnStateEntered.svg.import b/Icons/OnStateEntered.svg.import new file mode 100644 index 0000000..69d8112 --- /dev/null +++ b/Icons/OnStateEntered.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://di0gwxfn5v3jm" +path="res://.godot/imported/OnStateEntered.svg-cc2c47cc15c447c0d4066848fb108e6f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/rokojori_action_library/Icons/OnStateEntered.svg" +dest_files=["res://.godot/imported/OnStateEntered.svg-cc2c47cc15c447c0d4066848fb108e6f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/Icons/OnStateExited.svg b/Icons/OnStateExited.svg new file mode 100644 index 0000000..16dbcaa --- /dev/null +++ b/Icons/OnStateExited.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + diff --git a/Icons/OnStateExited.svg.import b/Icons/OnStateExited.svg.import new file mode 100644 index 0000000..f37ee4f --- /dev/null +++ b/Icons/OnStateExited.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dcs52ui6ku110" +path="res://.godot/imported/OnStateExited.svg-5e9a8abbedeb843cc99c78ab473f3106.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/rokojori_action_library/Icons/OnStateExited.svg" +dest_files=["res://.godot/imported/OnStateExited.svg-5e9a8abbedeb843cc99c78ab473f3106.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/Icons/State.svg b/Icons/State.svg new file mode 100644 index 0000000..5f86cd8 --- /dev/null +++ b/Icons/State.svg @@ -0,0 +1,71 @@ + + + + + + + + + diff --git a/Icons/State.svg.import b/Icons/State.svg.import new file mode 100644 index 0000000..2d073d4 --- /dev/null +++ b/Icons/State.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cyii8eavle6jq" +path="res://.godot/imported/State.svg-152f831ddb60ea6a98f9b0cc3a8104b5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/rokojori_action_library/Icons/State.svg" +dest_files=["res://.godot/imported/State.svg-152f831ddb60ea6a98f9b0cc3a8104b5.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/Icons/StateMachine.svg b/Icons/StateMachine.svg new file mode 100644 index 0000000..839b571 --- /dev/null +++ b/Icons/StateMachine.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + diff --git a/Icons/StateMachine.svg.import b/Icons/StateMachine.svg.import new file mode 100644 index 0000000..d8c4f75 --- /dev/null +++ b/Icons/StateMachine.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bc0rlxsml3vwi" +path="res://.godot/imported/StateMachine.svg-310f195fb3534c1788d58d28c8ba9b86.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/rokojori_action_library/Icons/StateMachine.svg" +dest_files=["res://.godot/imported/StateMachine.svg-310f195fb3534c1788d58d28c8ba9b86.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/Icons/StateTransition.svg b/Icons/StateTransition.svg new file mode 100644 index 0000000..fa0ea64 --- /dev/null +++ b/Icons/StateTransition.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + diff --git a/Icons/StateTransition.svg.import b/Icons/StateTransition.svg.import new file mode 100644 index 0000000..84e85a4 --- /dev/null +++ b/Icons/StateTransition.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bkubxrl2eugj6" +path="res://.godot/imported/StateTransition.svg-9e667c2d4f17c05ec9915b7e754fb653.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/rokojori_action_library/Icons/StateTransition.svg" +dest_files=["res://.godot/imported/StateTransition.svg-9e667c2d4f17c05ec9915b7e754fb653.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/Runtime/Behavior/Fight/FightManager.cs b/Runtime/Behavior/Fight/FightManager.cs new file mode 100644 index 0000000..86ae958 --- /dev/null +++ b/Runtime/Behavior/Fight/FightManager.cs @@ -0,0 +1,103 @@ +using Godot; +using System.Collections.Generic; +using System.Linq; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass] +public partial class FightManager:Node +{ + [Export] + public int maxEngaged = 3; + + [Export] + public int maxAttacking = 1; + + List _slots = new List(); + Dictionary _lastAttackTime = new Dictionary(); + + ulong _cachedFrame = ulong.MaxValue; + List _cachedEngaged = new List(); + List _cachedAttackers = new List(); + + public static FightManager Get() + { + return Unique.Get(); + } + + public int registeredCount => _slots.Count; + public int engagedCount => EngagedSlots().Count; + public int attackingCount => AttackingSlots().Count; + + public void Register( FightSlot slot ) + { + if ( slot == null || _slots.Contains( slot ) ) + { + return; + } + + _slots.Add( slot ); + } + + public void Unregister( FightSlot slot ) + { + _slots.Remove( slot ); + _lastAttackTime.Remove( slot ); + } + + List EngagedSlots() + { + RefreshFrameCache(); + return _cachedEngaged; + } + + List AttackingSlots() + { + RefreshFrameCache(); + return _cachedAttackers; + } + + void RefreshFrameCache() + { + var frame = Engine.GetPhysicsFrames(); + + if ( frame == _cachedFrame ) + { + return; + } + + _cachedFrame = frame; + + _cachedEngaged = _slots + .OrderByDescending( s => s.priority ) + .Take( Mathf.Max( 0, maxEngaged ) ) + .ToList(); + + var now = Time.GetTicksMsec() / 1000.0; + + _cachedAttackers = _cachedEngaged + .OrderByDescending( s => s.priority ) + .ThenBy( s => _lastAttackTime.TryGetValue( s, out var t ) ? t : -1 ) + .Take( Mathf.Max( 0, maxAttacking ) ) + .ToList(); + + _cachedAttackers.ForEach( s => _lastAttackTime[ s ] = now ); + } + + public bool IsEngaged( FightSlot slot ) + { + return slot != null && EngagedSlots().Contains( slot ); + } + + public bool HasAttackPermission( FightSlot slot ) + { + return slot != null && AttackingSlots().Contains( slot ); + } + + public bool IsAnyOtherAttacking( FightSlot self ) + { + return AttackingSlots().Any( s => s != self ); + } +} diff --git a/Runtime/Behavior/Fight/FightManager.cs.uid b/Runtime/Behavior/Fight/FightManager.cs.uid new file mode 100644 index 0000000..ee8a613 --- /dev/null +++ b/Runtime/Behavior/Fight/FightManager.cs.uid @@ -0,0 +1 @@ +uid://cpu8c7kvpg8cf diff --git a/Runtime/Behavior/Fight/FightSlot.cs b/Runtime/Behavior/Fight/FightSlot.cs new file mode 100644 index 0000000..5eaa930 --- /dev/null +++ b/Runtime/Behavior/Fight/FightSlot.cs @@ -0,0 +1,48 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass] +public partial class FightSlot:Node +{ + [Export] + public float priority = 0; + + bool _joined = false; + + public void Join() + { + if ( _joined ) + { + return; + } + + var manager = FightManager.Get(); + + if ( manager == null ) + { + return; + } + + manager.Register( this ); + _joined = true; + } + + public void Leave() + { + if ( ! _joined ) + { + return; + } + + FightManager.Get()?.Unregister( this ); + _joined = false; + } + + public override void _ExitTree() + { + Leave(); + } +} diff --git a/Runtime/Behavior/Fight/FightSlot.cs.uid b/Runtime/Behavior/Fight/FightSlot.cs.uid new file mode 100644 index 0000000..3ac0d20 --- /dev/null +++ b/Runtime/Behavior/Fight/FightSlot.cs.uid @@ -0,0 +1 @@ +uid://bvfaxecr50ode diff --git a/Runtime/Behavior/Fight/FightState.cs b/Runtime/Behavior/Fight/FightState.cs new file mode 100644 index 0000000..1c1008e --- /dev/null +++ b/Runtime/Behavior/Fight/FightState.cs @@ -0,0 +1,13 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass] +public partial class FightState:Node +{ + public bool isInCombat => ( FightManager.Get()?.registeredCount ?? 0 ) > 0; + + public int attackerCount => FightManager.Get()?.attackingCount ?? 0; +} diff --git a/Runtime/Behavior/Fight/FightState.cs.uid b/Runtime/Behavior/Fight/FightState.cs.uid new file mode 100644 index 0000000..4708336 --- /dev/null +++ b/Runtime/Behavior/Fight/FightState.cs.uid @@ -0,0 +1 @@ +uid://b7ygkdwfomf1c diff --git a/Runtime/Behavior/Fight/HasAttackPermission.cs b/Runtime/Behavior/Fight/HasAttackPermission.cs new file mode 100644 index 0000000..2a4a24f --- /dev/null +++ b/Runtime/Behavior/Fight/HasAttackPermission.cs @@ -0,0 +1,17 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass] +public partial class HasAttackPermission:SceneCondition +{ + [Export] + public FightSlot slot; + + public override bool Evaluate() + { + return FightManager.Get()?.HasAttackPermission( slot ) ?? false; + } +} diff --git a/Runtime/Behavior/Fight/HasAttackPermission.cs.uid b/Runtime/Behavior/Fight/HasAttackPermission.cs.uid new file mode 100644 index 0000000..2c3deb9 --- /dev/null +++ b/Runtime/Behavior/Fight/HasAttackPermission.cs.uid @@ -0,0 +1 @@ +uid://f5sb5yvwtbem diff --git a/Runtime/Behavior/Fight/IsAllyAttacking.cs b/Runtime/Behavior/Fight/IsAllyAttacking.cs new file mode 100644 index 0000000..6e33d82 --- /dev/null +++ b/Runtime/Behavior/Fight/IsAllyAttacking.cs @@ -0,0 +1,17 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass] +public partial class IsAllyAttacking:SceneCondition +{ + [Export] + public FightSlot slot; + + public override bool Evaluate() + { + return FightManager.Get()?.IsAnyOtherAttacking( slot ) ?? false; + } +} diff --git a/Runtime/Behavior/Fight/IsAllyAttacking.cs.uid b/Runtime/Behavior/Fight/IsAllyAttacking.cs.uid new file mode 100644 index 0000000..70b06e5 --- /dev/null +++ b/Runtime/Behavior/Fight/IsAllyAttacking.cs.uid @@ -0,0 +1 @@ +uid://b5gql6d11xntt diff --git a/Runtime/Behavior/Fight/IsEngagedInFight.cs b/Runtime/Behavior/Fight/IsEngagedInFight.cs new file mode 100644 index 0000000..5a21028 --- /dev/null +++ b/Runtime/Behavior/Fight/IsEngagedInFight.cs @@ -0,0 +1,17 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass] +public partial class IsEngagedInFight:SceneCondition +{ + [Export] + public FightSlot slot; + + public override bool Evaluate() + { + return FightManager.Get()?.IsEngaged( slot ) ?? false; + } +} diff --git a/Runtime/Behavior/Fight/IsEngagedInFight.cs.uid b/Runtime/Behavior/Fight/IsEngagedInFight.cs.uid new file mode 100644 index 0000000..16056ba --- /dev/null +++ b/Runtime/Behavior/Fight/IsEngagedInFight.cs.uid @@ -0,0 +1 @@ +uid://k8fi0cfr0wl4 diff --git a/Runtime/Behavior/Fight/JoinFight.cs b/Runtime/Behavior/Fight/JoinFight.cs new file mode 100644 index 0000000..c77760b --- /dev/null +++ b/Runtime/Behavior/Fight/JoinFight.cs @@ -0,0 +1,17 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass] +public partial class JoinFight:Action +{ + [Export] + public FightSlot slot; + + protected override void _OnTrigger() + { + slot?.Join(); + } +} diff --git a/Runtime/Behavior/Fight/JoinFight.cs.uid b/Runtime/Behavior/Fight/JoinFight.cs.uid new file mode 100644 index 0000000..be96a44 --- /dev/null +++ b/Runtime/Behavior/Fight/JoinFight.cs.uid @@ -0,0 +1 @@ +uid://4jvpwjxix4ce diff --git a/Runtime/Behavior/Fight/LeaveFight.cs b/Runtime/Behavior/Fight/LeaveFight.cs new file mode 100644 index 0000000..2a9217e --- /dev/null +++ b/Runtime/Behavior/Fight/LeaveFight.cs @@ -0,0 +1,17 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass] +public partial class LeaveFight:Action +{ + [Export] + public FightSlot slot; + + protected override void _OnTrigger() + { + slot?.Leave(); + } +} diff --git a/Runtime/Behavior/Fight/LeaveFight.cs.uid b/Runtime/Behavior/Fight/LeaveFight.cs.uid new file mode 100644 index 0000000..868749d --- /dev/null +++ b/Runtime/Behavior/Fight/LeaveFight.cs.uid @@ -0,0 +1 @@ +uid://d08f82rnn7kkr diff --git a/Runtime/Behavior/Movements/BehaviorMovement.cs b/Runtime/Behavior/Movements/BehaviorMovement.cs index d1ad9ac..d77a7f3 100644 --- a/Runtime/Behavior/Movements/BehaviorMovement.cs +++ b/Runtime/Behavior/Movements/BehaviorMovement.cs @@ -9,8 +9,12 @@ public abstract partial class BehaviorMovement:Node { public abstract void GoToPosition( Vector3 worldPosition ); public abstract void LookAtPosition( Vector3 worldPosition ); - + public abstract void FleeFromPosition( Vector3 worldPosition ); + public abstract void GoTo( Node3D node ); public abstract void Follow( Node3D node ); public abstract void LookAt( Node3D node ); + public abstract void Flee( Node3D node ); + + public abstract Vector3 GetGlobalPosition(); } diff --git a/Runtime/Behavior/Movements/CharacterBodyBehaviorMovement.cs b/Runtime/Behavior/Movements/CharacterBodyBehaviorMovement.cs index ddbe14b..bd68409 100644 --- a/Runtime/Behavior/Movements/CharacterBodyBehaviorMovement.cs +++ b/Runtime/Behavior/Movements/CharacterBodyBehaviorMovement.cs @@ -16,12 +16,33 @@ public partial class CharacterBodyBehaviorMovement:BehaviorMovement [Export] public float arrivalDistance = 0.1f; + [Export] + public float fleeDistance = 10f; + + [ExportGroup("Smoothing")] + [Export] + public Smoothing rotationSmoothing; + + [Export] + public Smoothing velocitySmoothing; + + [Export] + public Smoothing accelerationSmoothing; + Node3D _followTarget; Vector3? _targetPosition; + Vector3? _fleeFromPosition; + bool _fleeing; + + public override Vector3 GetGlobalPosition() + { + return body?.GlobalPosition ?? Vector3.Zero; + } public override void GoToPosition( Vector3 worldPosition ) { _followTarget = null; + _fleeing = false; _targetPosition = worldPosition; } @@ -40,21 +61,45 @@ public partial class CharacterBodyBehaviorMovement:BehaviorMovement return; } - body.GlobalTransform = new Transform3D( Basis.LookingAt( direction, Vector3.Up ), body.GlobalPosition ); + var targetRotation = Basis.LookingAt( direction, Vector3.Up ).GetRotationQuaternion(); + var delta = (float) GetPhysicsProcessDeltaTime(); + var smoothedRotation = Smoothing.Apply( rotationSmoothing, targetRotation, delta ); + + body.GlobalTransform = new Transform3D( new Basis( smoothedRotation ), body.GlobalPosition ); + } + + public override void FleeFromPosition( Vector3 worldPosition ) + { + _followTarget = null; + _targetPosition = null; + _fleeing = true; + _fleeFromPosition = worldPosition; } public override void GoTo( Node3D node ) { _followTarget = null; + _fleeing = false; _targetPosition = node == null ? null : node.GlobalPosition; } public override void Follow( Node3D node ) { _followTarget = node; + _fleeing = false; _targetPosition = null; } + public override void Flee( Node3D node ) + { + if ( node == null ) + { + return; + } + + FleeFromPosition( node.GlobalPosition ); + } + public override void LookAt( Node3D node ) { if ( node == null ) @@ -72,28 +117,44 @@ public partial class CharacterBodyBehaviorMovement:BehaviorMovement return; } - Vector3? target = _followTarget != null ? _followTarget.GlobalPosition : _targetPosition; + Vector3 direction; + bool arrived; - if ( target == null ) + if ( _fleeing && _fleeFromPosition != null ) { - return; + var away = body.GlobalPosition - _fleeFromPosition.Value; + away.Y = 0; + + arrived = away.Length() >= fleeDistance; + direction = away.LengthSquared() < 0.0001f ? Vector3.Forward : away.Normalized(); + } + else + { + Vector3? target = _followTarget != null ? _followTarget.GlobalPosition : _targetPosition; + + if ( target == null ) + { + return; + } + + var toTarget = target.Value - body.GlobalPosition; + toTarget.Y = 0; + + arrived = toTarget.Length() <= arrivalDistance; + direction = arrived ? Vector3.Zero : toTarget.Normalized(); } - var toTarget = target.Value - body.GlobalPosition; - toTarget.Y = 0; + var desiredVelocity = arrived ? Vector3.Zero : direction * moveSpeed; - if ( toTarget.Length() <= arrivalDistance ) - { - body.Velocity = Vector3.Zero; - body.MoveAndSlide(); - return; - } + var velocityStage = Smoothing.Apply( velocitySmoothing, desiredVelocity, (float) delta ); + var accelerationStage = Smoothing.Apply( accelerationSmoothing, velocityStage, (float) delta ); - var direction = toTarget.Normalized(); - - body.Velocity = direction * moveSpeed; + body.Velocity = accelerationStage; body.MoveAndSlide(); - LookAtPosition( body.GlobalPosition + direction ); + if ( ! arrived && accelerationStage.LengthSquared() > 0.0001f ) + { + LookAtPosition( body.GlobalPosition + accelerationStage.Normalized() ); + } } } diff --git a/Runtime/Behavior/Movements/FleeBehavior.cs b/Runtime/Behavior/Movements/FleeBehavior.cs new file mode 100644 index 0000000..1e93b40 --- /dev/null +++ b/Runtime/Behavior/Movements/FleeBehavior.cs @@ -0,0 +1,25 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass] +public partial class FleeBehavior:Action +{ + [Export] + public BehaviorMovement movement; + + [Export] + public Node3D target; + + protected override void _OnTrigger() + { + if ( movement == null || target == null ) + { + return; + } + + movement.Flee( target ); + } +} diff --git a/Runtime/Behavior/Movements/FleeBehavior.cs.uid b/Runtime/Behavior/Movements/FleeBehavior.cs.uid new file mode 100644 index 0000000..1fa8a8a --- /dev/null +++ b/Runtime/Behavior/Movements/FleeBehavior.cs.uid @@ -0,0 +1 @@ +uid://vvewrqnq4msf diff --git a/Runtime/Behavior/Movements/GoToPointBehavior.cs b/Runtime/Behavior/Movements/GoToPointBehavior.cs new file mode 100644 index 0000000..c898887 --- /dev/null +++ b/Runtime/Behavior/Movements/GoToPointBehavior.cs @@ -0,0 +1,65 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass] +public partial class GoToPointBehavior:SequenceAction +{ + [Export] + public BehaviorMovement movement; + + [Export] + public Node3D target; + + [Export] + public float arrivalDistance = 0.2f; + + int _activeId = -1; + + protected override void _OnTrigger() + { + if ( movement == null || target == null ) + { + return; + } + + movement.GoTo( target ); + _activeId = DispatchStart(); + } + + public override void _PhysicsProcess( double delta ) + { + if ( _activeId == -1 ) + { + return; + } + + if ( target == null ) + { + var cancelledId = _activeId; + _activeId = -1; + DispatchCancelled( cancelledId ); + return; + } + + var distance = ( movement.GetGlobalPosition() - target.GlobalPosition ).Length(); + + if ( distance <= arrivalDistance ) + { + var id = _activeId; + _activeId = -1; + DispatchEnd( id ); + } + } + + public override void CancelAction( int id ) + { + if ( id == _activeId ) + { + _activeId = -1; + DispatchCancelled( id ); + } + } +} diff --git a/Runtime/Behavior/Movements/GoToPointBehavior.cs.uid b/Runtime/Behavior/Movements/GoToPointBehavior.cs.uid new file mode 100644 index 0000000..2f1034d --- /dev/null +++ b/Runtime/Behavior/Movements/GoToPointBehavior.cs.uid @@ -0,0 +1 @@ +uid://c8t8djdwj7sah diff --git a/Runtime/Behavior/Movements/HideAtSpotBehavior.cs b/Runtime/Behavior/Movements/HideAtSpotBehavior.cs new file mode 100644 index 0000000..8be129c --- /dev/null +++ b/Runtime/Behavior/Movements/HideAtSpotBehavior.cs @@ -0,0 +1,93 @@ +using System.Linq; +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass] +public partial class HideAtSpotBehavior:SequenceAction +{ + [Export] + public BehaviorMovement movement; + + [Export] + public Node searchRoot; + + [Export] + public SelectorFlag[] requiredFlags = []; + + [Export] + public float arrivalDistance = 0.2f; + + [ExportGroup("Read Only")] + [Export] + public HidingSpot chosenSpot; + + int _activeId = -1; + + protected override void _OnTrigger() + { + if ( movement == null ) + { + return; + } + + var root = searchRoot ?? GetTree()?.CurrentScene; + + if ( root == null ) + { + return; + } + + var spots = root.GetAll( Matches ); + + if ( spots.Count == 0 ) + { + return; + } + + var position = movement.GetGlobalPosition(); + + chosenSpot = spots.OrderBy( s => s.GlobalPosition.DistanceSquaredTo( position ) ).First(); + + movement.GoTo( chosenSpot ); + _activeId = DispatchStart(); + } + + bool Matches( HidingSpot spot ) + { + if ( requiredFlags == null || requiredFlags.Length == 0 ) + { + return true; + } + + return requiredFlags.All( f => spot.flags.Contains( f ) ); + } + + public override void _PhysicsProcess( double delta ) + { + if ( _activeId == -1 || chosenSpot == null || movement == null ) + { + return; + } + + var distance = ( movement.GetGlobalPosition() - chosenSpot.GlobalPosition ).Length(); + + if ( distance <= arrivalDistance ) + { + var id = _activeId; + _activeId = -1; + DispatchEnd( id ); + } + } + + public override void CancelAction( int id ) + { + if ( id == _activeId ) + { + _activeId = -1; + DispatchCancelled( id ); + } + } +} diff --git a/Runtime/Behavior/Movements/HideAtSpotBehavior.cs.uid b/Runtime/Behavior/Movements/HideAtSpotBehavior.cs.uid new file mode 100644 index 0000000..ee720eb --- /dev/null +++ b/Runtime/Behavior/Movements/HideAtSpotBehavior.cs.uid @@ -0,0 +1 @@ +uid://cys665jxtmxi3 diff --git a/Runtime/Behavior/Movements/HidingSpot.cs b/Runtime/Behavior/Movements/HidingSpot.cs new file mode 100644 index 0000000..e926400 --- /dev/null +++ b/Runtime/Behavior/Movements/HidingSpot.cs @@ -0,0 +1,12 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass] +public partial class HidingSpot:Node3D +{ + [Export] + public SelectorFlag[] flags = []; +} diff --git a/Runtime/Behavior/Movements/HidingSpot.cs.uid b/Runtime/Behavior/Movements/HidingSpot.cs.uid new file mode 100644 index 0000000..a4d6754 --- /dev/null +++ b/Runtime/Behavior/Movements/HidingSpot.cs.uid @@ -0,0 +1 @@ +uid://dx60nkvsjsikr diff --git a/Runtime/Behavior/Movements/MoveOnPath.cs b/Runtime/Behavior/Movements/MoveOnPath.cs new file mode 100644 index 0000000..2625438 --- /dev/null +++ b/Runtime/Behavior/Movements/MoveOnPath.cs @@ -0,0 +1,62 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass] +public partial class MoveOnPath:SequenceAction +{ + [Export] + public Node3D target; + + [Export] + public Spline spline; + + [Export] + public float duration = 1f; + + double _elapsed; + int _activeId = -1; + + protected override void _OnTrigger() + { + if ( target == null || spline == null ) + { + return; + } + + _elapsed = 0; + _activeId = DispatchStart(); + } + + public override void _PhysicsProcess( double delta ) + { + if ( _activeId == -1 ) + { + return; + } + + _elapsed += delta; + + var t = duration <= 0 ? 1f : Mathf.Clamp( (float)( _elapsed / duration ), 0f, 1f ); + + target.GlobalPosition = spline.GetCurve().PositionAt( t ); + + if ( t >= 1f ) + { + var id = _activeId; + _activeId = -1; + DispatchEnd( id ); + } + } + + public override void CancelAction( int id ) + { + if ( id == _activeId ) + { + _activeId = -1; + DispatchCancelled( id ); + } + } +} diff --git a/Runtime/Behavior/Movements/MoveOnPath.cs.uid b/Runtime/Behavior/Movements/MoveOnPath.cs.uid new file mode 100644 index 0000000..eacda70 --- /dev/null +++ b/Runtime/Behavior/Movements/MoveOnPath.cs.uid @@ -0,0 +1 @@ +uid://cvi582q7lkeod diff --git a/Runtime/Behavior/Movements/NavigationBehaviorMovement.cs b/Runtime/Behavior/Movements/NavigationBehaviorMovement.cs new file mode 100644 index 0000000..abbbd19 --- /dev/null +++ b/Runtime/Behavior/Movements/NavigationBehaviorMovement.cs @@ -0,0 +1,189 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass] +public partial class NavigationBehaviorMovement:BehaviorMovement +{ + [Export] + public CharacterBody3D body; + + [Export] + public NavigationAgent3D agent; + + [Export] + public float moveSpeed = 5f; + + [Export] + public float fleeDistance = 10f; + + [ExportGroup("Smoothing")] + [Export] + public Smoothing rotationSmoothing; + + [Export] + public Smoothing velocitySmoothing; + + Node3D _followTarget; + Vector3? _fleeFromPosition; + bool _fleeing; + + public override Vector3 GetGlobalPosition() + { + return body?.GlobalPosition ?? Vector3.Zero; + } + + public override void GoToPosition( Vector3 worldPosition ) + { + _followTarget = null; + _fleeing = false; + agent?.SetTargetPosition( worldPosition ); + } + + public override void GoTo( Node3D node ) + { + _followTarget = null; + _fleeing = false; + + if ( node == null ) + { + return; + } + + agent?.SetTargetPosition( node.GlobalPosition ); + } + + public override void Follow( Node3D node ) + { + _followTarget = node; + _fleeing = false; + } + + public override void LookAtPosition( Vector3 worldPosition ) + { + if ( body == null ) + { + return; + } + + var direction = worldPosition - body.GlobalPosition; + direction.Y = 0; + + if ( direction.LengthSquared() < 0.0001f ) + { + return; + } + + var targetRotation = Basis.LookingAt( direction, Vector3.Up ).GetRotationQuaternion(); + var delta = (float) GetPhysicsProcessDeltaTime(); + var smoothedRotation = Smoothing.Apply( rotationSmoothing, targetRotation, delta ); + + body.GlobalTransform = new Transform3D( new Basis( smoothedRotation ), body.GlobalPosition ); + } + + public override void LookAt( Node3D node ) + { + if ( node == null ) + { + return; + } + + LookAtPosition( node.GlobalPosition ); + } + + public override void FleeFromPosition( Vector3 worldPosition ) + { + _followTarget = null; + _fleeing = true; + _fleeFromPosition = worldPosition; + } + + public override void Flee( Node3D node ) + { + if ( node == null ) + { + return; + } + + FleeFromPosition( node.GlobalPosition ); + } + + public override void _Ready() + { + if ( agent != null && agent.AvoidanceEnabled ) + { + agent.VelocityComputed += OnVelocityComputed; + } + } + + void OnVelocityComputed( Vector3 safeVelocity ) + { + if ( body == null ) + { + return; + } + + body.Velocity = safeVelocity; + body.MoveAndSlide(); + } + + public override void _PhysicsProcess( double delta ) + { + if ( body == null || agent == null ) + { + return; + } + + if ( _fleeing && _fleeFromPosition != null ) + { + var away = body.GlobalPosition - _fleeFromPosition.Value; + away.Y = 0; + + if ( away.Length() >= fleeDistance ) + { + body.Velocity = Vector3.Zero; + body.MoveAndSlide(); + return; + } + + var fleeDirection = away.LengthSquared() < 0.0001f ? Vector3.Forward : away.Normalized(); + agent.SetTargetPosition( body.GlobalPosition + fleeDirection * fleeDistance ); + } + else if ( _followTarget != null ) + { + agent.SetTargetPosition( _followTarget.GlobalPosition ); + } + + if ( agent.IsNavigationFinished() ) + { + body.Velocity = Vector3.Zero; + body.MoveAndSlide(); + return; + } + + var nextPosition = agent.GetNextPathPosition(); + var toNext = nextPosition - body.GlobalPosition; + toNext.Y = 0; + + var desiredVelocity = toNext.LengthSquared() < 0.0001f ? Vector3.Zero : toNext.Normalized() * moveSpeed; + + var finalVelocity = Smoothing.Apply( velocitySmoothing, desiredVelocity, (float) delta ); + + if ( agent.AvoidanceEnabled ) + { + agent.Velocity = finalVelocity; + } + else + { + body.Velocity = finalVelocity; + body.MoveAndSlide(); + } + + if ( finalVelocity.LengthSquared() > 0.0001f ) + { + LookAtPosition( body.GlobalPosition + finalVelocity.Normalized() ); + } + } +} diff --git a/Runtime/Behavior/Movements/NavigationBehaviorMovement.cs.uid b/Runtime/Behavior/Movements/NavigationBehaviorMovement.cs.uid new file mode 100644 index 0000000..1ab39eb --- /dev/null +++ b/Runtime/Behavior/Movements/NavigationBehaviorMovement.cs.uid @@ -0,0 +1 @@ +uid://b15wtairu8jfd diff --git a/Runtime/Behavior/Receptors/Receptor.cs b/Runtime/Behavior/Receptors/Receptor.cs index 3e09562..081cbdb 100644 --- a/Runtime/Behavior/Receptors/Receptor.cs +++ b/Runtime/Behavior/Receptors/Receptor.cs @@ -5,101 +5,15 @@ namespace Rokojori; [Tool] [GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Receptor.svg")] -public partial class Receptor:Action +public partial class Receptor:ReceptorBase { + [Export] public SceneCondition sceneCondition; - - [Export( PropertyHint.Range, "0.1,100")] - public float receptionTriggerCoverage = 75f; - - [Export( PropertyHint.Range, "10,2000")] - public float receptionWindowDurationMS = 500f; - - [Export( PropertyHint.Range, "10,1000")] - public float tickRateMS = 100f; - [Export] - public Timeline timeline; - - [ExportGroup("Debugging")] - [Export] - public float fillState = 0; - - int resultSize => Mathf.CeilToInt( receptionWindowDurationMS / tickRateMS ); - - RingBuffer _resultsBuffer; - - bool _activated = false; - - public bool activated => _activated; - - bool _active = false; - - public bool active => _active; - - int _eventID = -1; - - public void Start() + protected override bool _Sample() { - if ( _active ) - { - return; - } - - _active = true; - - var duration = tickRateMS / 1000f; - var tle = TimelineManager.ScheduleLoopEvent( timeline, duration, 0, tle => Sample() ); - _eventID = tle.id; - } - - public void Stop() - { - if ( ! _active ) - { - return; - } - - _active = false; - - TimelineManager.RemoveEvent( timeline, _eventID ); - _eventID = -1; - } - - protected override void _OnTrigger() - { - Sample(); - } - - void Sample() - { - // this.LogInfo( "Receptor Update" ); - - if ( sceneCondition == null ) - { - // this.LogInfo( "No condition" ); - return; - } - - if ( _resultsBuffer == null || _resultsBuffer.capacity != resultSize ) - { - _resultsBuffer = new RingBuffer( resultSize ); - } - - _resultsBuffer.Add( sceneCondition.Evaluate() ); - - if ( _resultsBuffer.count != resultSize ) - { - // this.LogInfo( "No activation" ); - _activated = false; - return; - } - - var numActive = _resultsBuffer.CountItems( b => b ); - fillState = numActive / (float) resultSize; - - _activated = fillState >= receptionTriggerCoverage / 100f; - + return sceneCondition?.Evaluate() ?? false; } + } diff --git a/Runtime/Behavior/Receptors/ReceptorBase.cs b/Runtime/Behavior/Receptors/ReceptorBase.cs new file mode 100644 index 0000000..65645ec --- /dev/null +++ b/Runtime/Behavior/Receptors/ReceptorBase.cs @@ -0,0 +1,109 @@ +using Godot; +using System.Collections.Generic; +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Receptor.svg")] +public abstract partial class ReceptorBase:Action +{ + + [Export( PropertyHint.Range, "0.1,100")] + public float receptionTriggerCoverage = 75f; + + [Export( PropertyHint.Range, "10,2000")] + public float receptionWindowDurationMS = 500f; + + [Export( PropertyHint.Range, "10,1000")] + public float tickRateMS = 100f; + + [Export] + public Timeline timeline; + + [ExportGroup("Debugging")] + [Export] + public float fillState = 0; + + [Export] + public bool printUpdates = false; + + int resultSize => Mathf.CeilToInt( receptionWindowDurationMS / tickRateMS ); + + RingBuffer _resultsBuffer; + + bool _activated = false; + + public bool activated => _activated; + + bool _active = false; + + public bool active => _active; + + int _eventID = -1; + + public void Start() + { + if ( _active ) + { + return; + } + + _active = true; + + var duration = tickRateMS / 1000f; + var tle = TimelineManager.ScheduleLoopEvent( timeline, duration, 0, tle => ProcessSample() ); + _eventID = tle.id; + } + + public void Stop() + { + if ( ! _active ) + { + return; + } + + _active = false; + + TimelineManager.RemoveEvent( timeline, _eventID ); + _eventID = -1; + } + + protected override void _OnTrigger() + { + ProcessSample(); + } + + protected abstract bool _Sample(); + + void ProcessSample() + { + var sample = _Sample(); + + + + if ( _resultsBuffer == null || _resultsBuffer.capacity != resultSize ) + { + _resultsBuffer = new RingBuffer( resultSize ); + } + + _resultsBuffer.Add( sample ); + + if ( _resultsBuffer.count != resultSize ) + { + // this.LogInfo( "No activation" ); + _activated = false; + return; + } + + var numActive = _resultsBuffer.CountItems( b => b ); + fillState = numActive / (float) resultSize; + + + _activated = fillState >= receptionTriggerCoverage / 100f; + + if ( printUpdates ) + { + this.LogInfo( fillState, _activated ); + } + } +} diff --git a/Runtime/Behavior/Receptors/ReceptorBase.cs.uid b/Runtime/Behavior/Receptors/ReceptorBase.cs.uid new file mode 100644 index 0000000..7a778e0 --- /dev/null +++ b/Runtime/Behavior/Receptors/ReceptorBase.cs.uid @@ -0,0 +1 @@ +uid://dj5ffwbp0v4ee diff --git a/Runtime/Behavior/Receptors/ReceptorControl.cs b/Runtime/Behavior/Receptors/ReceptorControl.cs new file mode 100644 index 0000000..313a8f3 --- /dev/null +++ b/Runtime/Behavior/Receptors/ReceptorControl.cs @@ -0,0 +1,58 @@ +using Godot; + +using System; +using System.Collections; +using System.Collections.Generic; + + + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/ReceptorControl.svg")] +public partial class ReceptorControl:Action +{ + public enum Mode + { + Start, + Stop + } + + [Export] + public Receptor[] receptors; + + [Export] + public Node[] containers; + + + + [Export] + public Mode mode; + + protected override void _OnTrigger() + { + switch ( mode ) + { + case Mode.Start: + { + ForEachReceptor( r => { r.LogInfo( "Starting" ); r.Start(); } ); + } + break; + + case Mode.Stop: + { + ForEachReceptor( r => r.Stop() ); + } + break; + } + } + + void ForEachReceptor( System.Action callback ) + { + receptors?.ForEachNonNull( r => callback( r ) ); + containers?.ForEachNonNull( c => c.ForEach( r => callback( r ) ) ); + } + + +} diff --git a/Runtime/Behavior/Receptors/ReceptorControl.cs.uid b/Runtime/Behavior/Receptors/ReceptorControl.cs.uid new file mode 100644 index 0000000..98aac46 --- /dev/null +++ b/Runtime/Behavior/Receptors/ReceptorControl.cs.uid @@ -0,0 +1 @@ +uid://l66q2ciopfmj diff --git a/Runtime/Behavior/StateMachine/OnStateActive.cs b/Runtime/Behavior/StateMachine/OnStateActive.cs new file mode 100644 index 0000000..d90ba96 --- /dev/null +++ b/Runtime/Behavior/StateMachine/OnStateActive.cs @@ -0,0 +1,57 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/OnStateActive.svg")] +public partial class OnStateActive:Sequence +{ + + [Export] + public bool isExclusive = true; + + [Export] + public bool reinitialize = false; + + public enum LoopMode + { + No_Loop, + Next_Frame, + Direct_Continuation + } + + [Export] + public LoopMode loopMode = LoopMode.No_Loop; + + public override void _Ready() + { + base._Ready(); + + onSequenceDone.AddAction( OnDone ); + } + + protected override void _OnTrigger() + { + // this.LogInfo( "Triggering" ); + base._OnTrigger(); + } + + async void OnDone( SequenceActionFinishedEvent ev ) + { + if ( LoopMode.No_Loop == loopMode ) + { + return; + } + + if ( LoopMode.Next_Frame == loopMode ) + { + await this.RequestNextFrame(); + } + + + // this.LogInfo( "Loop Re-Triggering" ); + Trigger(); + + } +} diff --git a/Runtime/Behavior/StateMachine/OnStateActive.cs.uid b/Runtime/Behavior/StateMachine/OnStateActive.cs.uid new file mode 100644 index 0000000..811c458 --- /dev/null +++ b/Runtime/Behavior/StateMachine/OnStateActive.cs.uid @@ -0,0 +1 @@ +uid://dr8k0q7q3760 diff --git a/Runtime/Behavior/StateMachine/OnStateEntered.cs b/Runtime/Behavior/StateMachine/OnStateEntered.cs new file mode 100644 index 0000000..39edd04 --- /dev/null +++ b/Runtime/Behavior/StateMachine/OnStateEntered.cs @@ -0,0 +1,10 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/OnStateEntered.svg")] +public partial class OnStateEntered:StateEnterExitAction +{ +} diff --git a/Runtime/Behavior/StateMachine/OnStateEntered.cs.uid b/Runtime/Behavior/StateMachine/OnStateEntered.cs.uid new file mode 100644 index 0000000..762fce9 --- /dev/null +++ b/Runtime/Behavior/StateMachine/OnStateEntered.cs.uid @@ -0,0 +1 @@ +uid://duny4jcvytcjs diff --git a/Runtime/Behavior/StateMachine/OnStateExited.cs b/Runtime/Behavior/StateMachine/OnStateExited.cs new file mode 100644 index 0000000..cb8a497 --- /dev/null +++ b/Runtime/Behavior/StateMachine/OnStateExited.cs @@ -0,0 +1,10 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/OnStateExited.svg")] +public partial class OnStateExited:StateEnterExitAction +{ +} diff --git a/Runtime/Behavior/StateMachine/OnStateExited.cs.uid b/Runtime/Behavior/StateMachine/OnStateExited.cs.uid new file mode 100644 index 0000000..5ddb30f --- /dev/null +++ b/Runtime/Behavior/StateMachine/OnStateExited.cs.uid @@ -0,0 +1 @@ +uid://cxv11h345udfg diff --git a/Runtime/Behavior/StateMachine/State.cs b/Runtime/Behavior/StateMachine/State.cs new file mode 100644 index 0000000..304a510 --- /dev/null +++ b/Runtime/Behavior/StateMachine/State.cs @@ -0,0 +1,10 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/State.svg")] +public partial class State:Node +{ +} diff --git a/Runtime/Behavior/StateMachine/State.cs.uid b/Runtime/Behavior/StateMachine/State.cs.uid new file mode 100644 index 0000000..b4e6b73 --- /dev/null +++ b/Runtime/Behavior/StateMachine/State.cs.uid @@ -0,0 +1 @@ +uid://bf32kfq4h4ws3 diff --git a/Runtime/Behavior/StateMachine/StateEnterExitAction.cs b/Runtime/Behavior/StateMachine/StateEnterExitAction.cs new file mode 100644 index 0000000..24bc307 --- /dev/null +++ b/Runtime/Behavior/StateMachine/StateEnterExitAction.cs @@ -0,0 +1,38 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/OnEvent.svg")] +public abstract partial class StateEnterExitAction:Sequence +{ + [Export] + public SceneCondition sceneCondition; + + [Export] + public Condition condition; + + [Export] + public State state; + + public bool Matches( State other ) + { + if ( state != null && state != other ) + { + return false; + } + + if ( sceneCondition != null && ! sceneCondition.Evaluate() ) + { + return false; + } + + if ( condition != null && ! condition.Evaluate() ) + { + return false; + } + + return true; + } +} diff --git a/Runtime/Behavior/StateMachine/StateEnterExitAction.cs.uid b/Runtime/Behavior/StateMachine/StateEnterExitAction.cs.uid new file mode 100644 index 0000000..c527f35 --- /dev/null +++ b/Runtime/Behavior/StateMachine/StateEnterExitAction.cs.uid @@ -0,0 +1 @@ +uid://cs64njpww5p5v diff --git a/Runtime/Behavior/StateMachine/StateMachine.cs b/Runtime/Behavior/StateMachine/StateMachine.cs new file mode 100644 index 0000000..86d45b8 --- /dev/null +++ b/Runtime/Behavior/StateMachine/StateMachine.cs @@ -0,0 +1,232 @@ +using Godot; + +using System.Collections.Generic; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/StateMachine.svg")] +public partial class StateMachine:SequenceAction +{ + [ExportGroup( "Debugging")] + [Export] + public State currentState; + + [Export] + public bool printStateChanges = false; + + [Export] + public bool printTransitionChanges = false; + + int _runID = -1; + + List _receptors = new List(); + + + protected override void _OnTrigger() + { + if ( currentState != null ) + { + // Already running. + return; + } + + var states = this.GetDirectChildren(); + + if ( states.Count == 0 ) + { + return; + } + + _runID = DispatchStart(); + + StartReceptors( states ); + + EnterState( states[ 0 ], null ); + } + + public override void _Process( double delta ) + { + if ( Engine.IsEditorHint() || currentState == null ) + { + return; + } + + CheckTransitions(); + } + + Dictionary _transitions = new Dictionary(); + + void CheckTransitions() + { + + var transitions = currentState.GetDirectChildren(); + + for ( int i = 0; i < transitions.Count; i++ ) + { + var transition = transitions[ i ]; + var isTriggered = transition.IsTriggered(); + + var before = ! _transitions.ContainsKey( transition ) ? false : _transitions[ transition ]; + + if ( before != isTriggered ) + { + _transitions[ transition ] = isTriggered; + RJLog.Log( HierarchyName.Of( transition ), isTriggered ); + } + + + if ( isTriggered ) + { + DoTransition( transition.targetState ); + transition.onTransition?.Trigger(); + return; + } + } + } + + void DoTransition( State target ) + { + var previous = currentState; + + if ( target == previous ) + { + ReinitializeState( previous ); + return; + } + + ExitState( previous, target ); + + if ( target == null ) + { + EndStateMachine(); + return; + } + + EnterState( target, previous ); + } + + void EnterState( State state, State previous ) + { + currentState = state; + + if ( printStateChanges ) + { + this.LogInfo( "Entering State:", HierarchyName.Of( state ) ); + } + + state.ForEachDirectChild( oe => + { + if ( oe.Matches( previous ) ) + { + oe.Trigger(); + } + } ); + + state.ForEachDirectChild( oa => oa.Trigger() ); + } + + void ExitState( State state, State next ) + { + if ( printStateChanges ) + { + this.LogInfo( "Exiting State:", HierarchyName.Of( state ) ); + } + + state.ForEachDirectChild( oe => + { + if ( oe.Matches( next ) ) + { + oe.Trigger(); + } + } ); + + state.ForEachDirectChild( oa => oa.CancelAllRunning() ); + } + + void ReinitializeState( State state ) + { + state.ForEachDirectChild( oe => + { + if ( oe.Matches( state ) ) + { + oe.Trigger(); + } + } ); + + state.ForEachDirectChild( oe => + { + if ( oe.Matches( state ) ) + { + oe.Trigger(); + } + } ); + + state.ForEachDirectChild( oa => + { + if ( ! oa.reinitialize ) + { + return; + } + + if ( oa.isExclusive && oa.isRunning ) + { + return; + } + + oa.CancelAllRunning(); + oa.Trigger(); + } ); + } + + void StartReceptors( List states ) + { + _receptors.Clear(); + + states.ForEach( s => + s.ForEachDirectChild( t => + { + if ( t.receptor != null ) + { + _receptors.Add( t.receptor ); + } + } ) + ); + + _receptors.ForEach( r => r.Start() ); + } + + void StopReceptors() + { + _receptors.ForEach( r => r.Stop() ); + _receptors.Clear(); + } + + void EndStateMachine() + { + currentState = null; + + SetProcess( false ); + + StopReceptors(); + + DispatchEnd( _runID ); + } + + public override void CancelAction( int id ) + { + if ( id != _runID || currentState == null ) + { + return; + } + + currentState = null; + + SetProcess( false ); + + StopReceptors(); + + DispatchCancelled( id ); + } +} diff --git a/Runtime/Behavior/StateMachine/StateMachine.cs.uid b/Runtime/Behavior/StateMachine/StateMachine.cs.uid new file mode 100644 index 0000000..3655db2 --- /dev/null +++ b/Runtime/Behavior/StateMachine/StateMachine.cs.uid @@ -0,0 +1 @@ +uid://cnv4f0rjl6l0f diff --git a/Runtime/Behavior/StateMachine/StateTransition.cs b/Runtime/Behavior/StateMachine/StateTransition.cs new file mode 100644 index 0000000..c2d5018 --- /dev/null +++ b/Runtime/Behavior/StateMachine/StateTransition.cs @@ -0,0 +1,62 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/StateTransition.svg")] +public partial class StateTransition:Node +{ + [Export] + public SceneCondition sceneCondition; + + [Export] + public Condition condition; + + [Export] + public Receptor receptor; + + // Transitioning to a null targetState ends the owning StateMachine. + [Export] + public State targetState; + + + [Export] + public Action onTransition; + + + [ExportToolButton( "Set Reference Name" )] + public Callable setReferencedNameButton => Callable.From( + ()=> + { + if ( targetState == null ) + { + this.Name = "▸ (nothing)"; + } + else + { + this.Name = "▸ " + targetState.Name; + } + } + ); + + public bool IsTriggered() + { + if ( sceneCondition?.Evaluate() ?? false ) + { + return true; + } + + if ( condition?.Evaluate() ?? false ) + { + return true; + } + + if ( receptor?.activated ?? false ) + { + return true; + } + + return false; + } +} diff --git a/Runtime/Behavior/StateMachine/StateTransition.cs.uid b/Runtime/Behavior/StateMachine/StateTransition.cs.uid new file mode 100644 index 0000000..84cece8 --- /dev/null +++ b/Runtime/Behavior/StateMachine/StateTransition.cs.uid @@ -0,0 +1 @@ +uid://dok5gagvhoo6r diff --git a/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCamera.cs b/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCamera.cs index 49e8fef..b6ffddd 100644 --- a/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCamera.cs +++ b/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCamera.cs @@ -39,6 +39,8 @@ namespace Rokojori public bool canProcess => processInEditor || ! Engine.IsEditorHint(); Vector3 _lastLockTargetPosition; + float _lockBlend; + float _orientationBlend; public override void _Process( double delta ) { @@ -88,10 +90,23 @@ namespace Rokojori // 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 ); + // lockTarget itself has gone null. Engage and release each get their own Smoothing (and + // therefore their own duration), driven off the same persistent _lockBlend value via + // ApplyWith so switching which one is active each frame can't desync from the other's + // internal state. + var lockBlendSmoothing = hasLockTarget ? settings.lockEngageSmoothing : settings.lockReleaseSmoothing; + _lockBlend = Smoothing.ApplyWith( lockBlendSmoothing, _lockBlend, hasLockTarget ? 1f : 0f, delta ); + var lockBlend = _lockBlend; var isLocked = lockBlend > 0.001f; + // Separate blend for yaw/pitch: the lock target's position needs easing since it keeps + // moving, but once released there's nothing left to track - yaw/pitch should return to + // "the normal camera" on their own (fast) terms instead of riding lockBlend's slower, + // framing-tuned duration back down. + var orientationBlendSmoothing = hasLockTarget ? settings.lockOrientationEngageSmoothing : settings.lockOrientationReleaseSmoothing; + _orientationBlend = Smoothing.ApplyWith( orientationBlendSmoothing, _orientationBlend, hasLockTarget ? 1f : 0f, delta ); + var orientationBlend = _orientationBlend; + var focusPosition = targetPosition.Lerp( ( targetPosition + lockTargetPosition ) / 2f, lockBlend ); var yawAxis = 0f; @@ -104,21 +119,37 @@ namespace Rokojori } // this.LogInfo( "YAW:", yawAxis, "PITCH:", pitchAxis ); - if ( isLocked ) + + // Free-look yaw keeps integrating, just scaled down as orientationBlend rises, so it + // doesn't freeze the instant a lock starts and resumes smoothly from wherever it actually + // is once released - rather than hard-pausing on the isLocked boolean. + yaw += yawAxis * settings.yawSpeed * data.yawSpeed * delta * ( 1f - orientationBlend ); + + var lockYawTarget = yaw; + + if ( hasLockTarget ) { var lockDirection = lockTargetPosition - targetPosition; lockDirection.Y = 0; if ( lockDirection.LengthSquared() > 0.0001f ) { - yaw = Math3D.GlobalYawDegrees( lockDirection ); + lockYawTarget = Math3D.GlobalYawDegrees( lockDirection ); } } - else - { - yaw += yawAxis * settings.yawSpeed * data.yawSpeed * delta; - } + // Blended by orientationBlend instead of hard-switching on isLocked, so engage eases over + // lockOrientationEngageSmoothing's duration while release can return to free-look fast via + // lockOrientationReleaseSmoothing, independent of how slow lockBlend (focus/distance) is + // still easing out. The normal (faster) yawSmoothing below only adds its usual per-frame + // responsiveness on top of that already-eased target. + var appliedYaw = MathX.LerpAngleDegrees( yaw, lockYawTarget, orientationBlend ); + + // Keep the free-look accumulator synced to whatever is actually being displayed, so it + // never goes stale while locked (it barely integrates then) - otherwise releasing the lock + // makes lockYawTarget's "no target" fallback snap back to a long-stale pre-lock value + // instead of the direction the camera was just actually facing. + yaw = appliedYaw; if ( data.pitchIsRelative ) { @@ -130,12 +161,11 @@ namespace Rokojori pitch = Mathf.Remap( pitchAxis, -1, 1, settings.minPitch, settings.maxPitch ); } - if ( isLocked ) - { - pitch = Mathf.Clamp( pitch, settings.lockMinPitch, settings.lockMaxPitch ); - } - - var appliedYaw = yaw; + // Same orientationBlend for the pitch clamp range - avoids a hard clamp snap on + // engage/release, and returns to the normal range just as quickly as yaw does on release. + var blendedMinPitch = Mathf.Lerp( settings.minPitch, settings.lockMinPitch, orientationBlend ); + var blendedMaxPitch = Mathf.Lerp( settings.maxPitch, settings.lockMaxPitch, orientationBlend ); + pitch = Mathf.Clamp( pitch, blendedMinPitch, blendedMaxPitch ); var smoothedYaw = Smoothing.ApplyDegrees( settings.yawSmoothing, appliedYaw, delta ); diff --git a/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCameraSettings.cs b/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCameraSettings.cs index 44fc73d..7e3c391 100644 --- a/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCameraSettings.cs +++ b/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCameraSettings.cs @@ -79,7 +79,20 @@ namespace Rokojori public Smoothing lockTargetFollowSmoothing = new FrameSmoothing(); [Export] - public Smoothing lockBlendSmoothing = new FrameSmoothing(); + public Smoothing lockEngageSmoothing = new FrameSmoothing(); + + [Export] + public Smoothing lockReleaseSmoothing = new FrameSmoothing(); + + // Separate from the pair above: those blend the focus position and bounding distance (can + // stay slow/cinematic on release). These blend yaw and the pitch clamp range - on release, + // orientation should snap back to "the normal camera" fast, independent of how slow the + // framing is still easing out. + [Export] + public Smoothing lockOrientationEngageSmoothing = new FrameSmoothing(); + + [Export] + public Smoothing lockOrientationReleaseSmoothing = new FrameSmoothing(); [Export] public float lockMinPitch = -10; diff --git a/Runtime/Conditions/IsValueBelowThreshold.cs b/Runtime/Conditions/IsValueBelowThreshold.cs new file mode 100644 index 0000000..e67e03e --- /dev/null +++ b/Runtime/Conditions/IsValueBelowThreshold.cs @@ -0,0 +1,27 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass] +public partial class IsValueBelowThreshold:SceneCondition +{ + [Export] + public NumberValue numberValue; + + [Export] + public double threshold = -1; + + public override bool Evaluate() + { + if ( numberValue == null ) + { + return false; + } + + var t = threshold < 0 ? numberValue.lowTreshold : threshold; + + return numberValue.value < t; + } +} diff --git a/Runtime/Conditions/IsValueBelowThreshold.cs.uid b/Runtime/Conditions/IsValueBelowThreshold.cs.uid new file mode 100644 index 0000000..6be3a5e --- /dev/null +++ b/Runtime/Conditions/IsValueBelowThreshold.cs.uid @@ -0,0 +1 @@ +uid://dj8npbgn3g3wm diff --git a/Runtime/Godot/Extensions/Node3DExtensions.cs b/Runtime/Godot/Extensions/Node3DExtensions.cs index 70d4b02..d1bee73 100644 --- a/Runtime/Godot/Extensions/Node3DExtensions.cs +++ b/Runtime/Godot/Extensions/Node3DExtensions.cs @@ -42,6 +42,19 @@ namespace Rokojori return box.center; } + public static bool CanSee( this Node3D self, Node3D other, float angle = 45f ) + { + var forward = self.GlobalForward(); + var dir = self.DirectionTowards( other ).Normalized(); + + var dot = forward.Dot( dir ); + + var threshold = Mathf.Cos( Mathf.DegToRad( angle / 2f ) ); + + return dot > threshold; + } + + public static Vector3 GetGlobalCenterOfChildren( this Node3D self, bool bounds = false, bool onlyVisible = true ) { var center = Vector3.Zero; diff --git a/Runtime/Math/Smoother.cs b/Runtime/Math/Smoother.cs index 5d15277..c69c960 100644 --- a/Runtime/Math/Smoother.cs +++ b/Runtime/Math/Smoother.cs @@ -29,9 +29,9 @@ namespace Rokojori public float SmoothForDuration( float value, float nextValue, float duration, float delta, float processDelta = MathX.fps120Delta ) { - var coefficient = MathX.SmoothingCoefficient( duration * 1000f, processDelta ); + var coefficient = MathX.SmoothingCoefficient( duration * 1000f, 0.1f, processDelta ); return SmoothWithCoefficient( value, nextValue, coefficient, delta, processDelta ); - } + } public float SmoothWithCoefficient( float value, float nextValue, float coefficient, float delta, float processDelta = MathX.fps120Delta ) @@ -50,9 +50,9 @@ namespace Rokojori public Quaternion SmoothForDuration( Quaternion value, Quaternion nextValue, float duration, float delta, float processDelta = MathX.fps120Delta ) { - var coefficient = MathX.SmoothingCoefficient( duration * 1000f, processDelta ); + var coefficient = MathX.SmoothingCoefficient( duration * 1000f, 0.1f, processDelta ); return SmoothWithCoefficient( value, nextValue, coefficient, delta, processDelta ); - } + } public Quaternion SmoothWithCoefficient( Quaternion value, Quaternion nextValue, float coefficient, float delta, float processDelta = MathX.fps120Delta ) { @@ -60,7 +60,7 @@ namespace Rokojori while ( overflowDelta > processDelta ) { - value = value.Slerp( nextValue, coefficient ); + value = value.Slerp( nextValue, coefficient ).Normalized(); overflowDelta -= processDelta; } @@ -69,9 +69,9 @@ namespace Rokojori public Vector3 SmoothForDuration( Vector3 value, Vector3 nextValue, float duration, float delta, float processDelta = MathX.fps120Delta ) { - var coefficient = MathX.SmoothingCoefficient( duration * 1000f, processDelta ); + var coefficient = MathX.SmoothingCoefficient( duration * 1000f, 0.1f, processDelta ); return SmoothWithCoefficient( value, nextValue, coefficient, delta, processDelta ); - } + } public Vector3 SmoothWithCoefficient( Vector3 value, Vector3 nextValue, float coefficient, float delta, float processDelta = MathX.fps120Delta ) { diff --git a/Runtime/Physics/PhysicsRayCast.cs b/Runtime/Physics/PhysicsRayCast.cs index 3d32da0..78abc4d 100644 --- a/Runtime/Physics/PhysicsRayCast.cs +++ b/Runtime/Physics/PhysicsRayCast.cs @@ -7,7 +7,7 @@ namespace Rokojori { public class PhysicsRayCast { - public static CollisionData GetFirst( CharacterBody3D body, Vector3 from, Vector3 to ) + public static CollisionData GetFirst( PhysicsBody3D body, Vector3 from, Vector3 to ) { var world = body.GetWorld3D(); var rayParameters = new PhysicsRayQueryParameters3D @@ -25,7 +25,7 @@ namespace Rokojori return hits.Count == 0 ? null : hits[ 0 ]; } - public static CollisionData GetFirst( CharacterBody3D body, Vector3 direction ) + public static CollisionData GetFirst( PhysicsBody3D body, Vector3 direction ) { var from = body.GlobalPosition; var to = from + direction; @@ -33,7 +33,7 @@ namespace Rokojori return GetFirst( body, from, to ); } - public static int GetAll( List outputCollisionData, CharacterBody3D body, Vector3 from, Vector3 to ) + public static int GetAll( List outputCollisionData, PhysicsBody3D body, Vector3 from, Vector3 to ) { var world = body.GetWorld3D(); var rayParameters = new PhysicsRayQueryParameters3D @@ -49,7 +49,7 @@ namespace Rokojori return CollisionData.MultiRayCast( world, rayParameters, outputCollisionData ); } - public static int GetAll( List outputCollisionData, CharacterBody3D body, Vector3 direction ) + public static int GetAll( List outputCollisionData, PhysicsBody3D body, Vector3 direction ) { var from = body.GlobalPosition; var to = from + direction;