diff --git a/Icons/BehaviorBranch.svg b/Icons/BehaviorBranch.svg new file mode 100644 index 0000000..b48c2a4 --- /dev/null +++ b/Icons/BehaviorBranch.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + diff --git a/Icons/BehaviorBranch.svg.import b/Icons/BehaviorBranch.svg.import new file mode 100644 index 0000000..38bb271 --- /dev/null +++ b/Icons/BehaviorBranch.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://whc152vj4786" +path="res://.godot/imported/BehaviorBranch.svg-2479d9ece5118d365ff6adf5a4a78745.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/rokojori_action_library/Icons/BehaviorBranch.svg" +dest_files=["res://.godot/imported/BehaviorBranch.svg-2479d9ece5118d365ff6adf5a4a78745.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/BehaviorSequence.svg b/Icons/BehaviorSequence.svg new file mode 100644 index 0000000..1dc484e --- /dev/null +++ b/Icons/BehaviorSequence.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + diff --git a/Icons/BehaviorSequence.svg.import b/Icons/BehaviorSequence.svg.import new file mode 100644 index 0000000..71d4c80 --- /dev/null +++ b/Icons/BehaviorSequence.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c0bg8q1qhta8d" +path="res://.godot/imported/BehaviorSequence.svg-e83c81b196d7953a5989711a8e3762a9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/rokojori_action_library/Icons/BehaviorSequence.svg" +dest_files=["res://.godot/imported/BehaviorSequence.svg-e83c81b196d7953a5989711a8e3762a9.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/Brain.svg b/Icons/Brain.svg new file mode 100644 index 0000000..cc17ee6 --- /dev/null +++ b/Icons/Brain.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + diff --git a/Icons/Brain.svg.import b/Icons/Brain.svg.import new file mode 100644 index 0000000..c405a3a --- /dev/null +++ b/Icons/Brain.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ddywe4nqkppim" +path="res://.godot/imported/Brain.svg-22061c8f9edbd3ff520fcf62d8a1f665.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/rokojori_action_library/Icons/Brain.svg" +dest_files=["res://.godot/imported/Brain.svg-22061c8f9edbd3ff520fcf62d8a1f665.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/BrainControl.svg b/Icons/BrainControl.svg new file mode 100644 index 0000000..8d7ac86 --- /dev/null +++ b/Icons/BrainControl.svg @@ -0,0 +1,51 @@ + + + + + + + + + diff --git a/Icons/BrainControl.svg.import b/Icons/BrainControl.svg.import new file mode 100644 index 0000000..4701ae7 --- /dev/null +++ b/Icons/BrainControl.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c37yfx3ryvoxu" +path="res://.godot/imported/BrainControl.svg-0da1cfbf5e1e82c6dfa220b6df44195a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/rokojori_action_library/Icons/BrainControl.svg" +dest_files=["res://.godot/imported/BrainControl.svg-0da1cfbf5e1e82c6dfa220b6df44195a.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/Receptor.svg b/Icons/Receptor.svg new file mode 100644 index 0000000..cf43587 --- /dev/null +++ b/Icons/Receptor.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + diff --git a/Icons/Receptor.svg.import b/Icons/Receptor.svg.import new file mode 100644 index 0000000..1bf59c9 --- /dev/null +++ b/Icons/Receptor.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://jusswlaofkgc" +path="res://.godot/imported/Receptor.svg-3c1cccf5229247e268d12db0af1e1abb.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/rokojori_action_library/Icons/Receptor.svg" +dest_files=["res://.godot/imported/Receptor.svg-3c1cccf5229247e268d12db0af1e1abb.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/ReceptorBranch.svg b/Icons/ReceptorBranch.svg new file mode 100644 index 0000000..b8562de --- /dev/null +++ b/Icons/ReceptorBranch.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + diff --git a/Icons/ReceptorBranch.svg.import b/Icons/ReceptorBranch.svg.import new file mode 100644 index 0000000..f9feb77 --- /dev/null +++ b/Icons/ReceptorBranch.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c6wwqj7hdrmpa" +path="res://.godot/imported/ReceptorBranch.svg-bb59728d991a1a6154c3682d9e33cce7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/rokojori_action_library/Icons/ReceptorBranch.svg" +dest_files=["res://.godot/imported/ReceptorBranch.svg-bb59728d991a1a6154c3682d9e33cce7.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/Actions/Node3D/LookAt.cs b/Runtime/Actions/Node3D/LookAt.cs index 5824a14..7958f77 100644 --- a/Runtime/Actions/Node3D/LookAt.cs +++ b/Runtime/Actions/Node3D/LookAt.cs @@ -18,7 +18,9 @@ namespace Rokojori protected override void _OnTrigger() { - if ( lookFrom == null || lookTarget == null ) + if ( lookFrom == null || lookTarget == null || + ! IsInstanceValid( lookFrom ) || ! IsInstanceValid( lookTarget ) + ) { return; } diff --git a/Runtime/Actions/Node3D/SelectNode3D.cs b/Runtime/Actions/Node3D/SelectNode3D.cs new file mode 100644 index 0000000..a09072a --- /dev/null +++ b/Runtime/Actions/Node3D/SelectNode3D.cs @@ -0,0 +1,141 @@ + +using Godot; +using System.Collections.Generic; +using System.Reflection; + +using Rokojori.Extensions; +namespace Rokojori +{ + [GlobalClass, Tool ] + public partial class SelectNode3D : Action + { + [Export] + public Selector selector; + + bool _enteredTree = false; + + public override void _EnterTree() + { + base._EnterTree(); + _enteredTree = true; + } + + Node _target; + + [Export] + public Node target + { + get => _target; + set + { + if ( _target == value ) + { + return; + } + + _target = value; + + // Deserialization sets exported properties before _EnterTree fires, so this + // only triggers for genuine interactive edits in the editor, never for a scene + // being loaded (regardless of property order) or for runtime reassignment. + if ( _enteredTree && Engine.IsEditorHint() ) + { + targetMember = ""; + +#if TOOLS + NotifyPropertyListChanged(); +#endif + } + } + } + + [Export] + public string targetMember; + + + + protected override void _OnTrigger() + { + if ( target == null ) + { + return; + } + + Node3D node3D = null; + + if ( selector == null ) + { + ReflectionHelper.SetValue( target, targetMember, node3D ); + this.LogInfo( "Set Null:", HierarchyName.Of( target ), targetMember ); + return; + } + + var selectedNode = Unique.Get().Select( selector ); + + + if ( selectedNode is Selectable ) + { + var go = selectedNode.GetParent(); + + if ( go != null ) + { + node3D = go.body; + } + } + + ReflectionHelper.SetValue( target, targetMember, node3D ); + + } + +#if TOOLS + + public override void _ValidateProperty( Godot.Collections.Dictionary property ) + { + if ( property[ "name" ].AsString() != nameof( targetMember ) ) + { + return; + } + + var members = GetTargetNode3DMemberNames(); + + property[ "hint" ] = (long) PropertyHint.Enum; + property[ "hint_string" ] = members.Count == 0 + ? "(Invalid Target)" + : "None," + string.Join( ",", members ); + } + + List GetTargetNode3DMemberNames() + { + var names = new List(); + + if ( target == null ) + { + return names; + } + + var infos = ReflectionHelper.GetDataMemberInfos( target, BindingFlags.Public | BindingFlags.Instance ); + + foreach ( var info in infos ) + { + if ( ! info.IsDefined( typeof( ExportAttribute ) ) ) + { + continue; + } + + var memberType = info.GetMemberType(); + + if ( memberType == null || ! memberType.IsAssignableFrom( typeof( Node3D ) ) ) + { + continue; + } + + names.Add( info.Name ); + } + + return names; + } + +#endif + + } +} \ No newline at end of file diff --git a/Runtime/Actions/Node3D/SelectNode3D.cs.uid b/Runtime/Actions/Node3D/SelectNode3D.cs.uid new file mode 100644 index 0000000..cd38eeb --- /dev/null +++ b/Runtime/Actions/Node3D/SelectNode3D.cs.uid @@ -0,0 +1 @@ +uid://76pepn06ofq5 diff --git a/Runtime/Actions/Node3D/SetNode3D.cs b/Runtime/Actions/Node3D/SetNode3D.cs index d551d85..6196649 100644 --- a/Runtime/Actions/Node3D/SetNode3D.cs +++ b/Runtime/Actions/Node3D/SetNode3D.cs @@ -30,10 +30,13 @@ namespace Rokojori return; } - var value = ReflectionHelper.GetValue( source, sourceMember ); + var value = sourceMember == null || sourceMember == "" ? (Node3D) source : + ReflectionHelper.GetValue( source, sourceMember ); ReflectionHelper.SetValue( target, targetMember, value ); } + + } } \ No newline at end of file diff --git a/Runtime/Actions/Values/NumberValue.cs b/Runtime/Actions/Values/NumberValue.cs index 1a9835c..962c81a 100644 --- a/Runtime/Actions/Values/NumberValue.cs +++ b/Runtime/Actions/Values/NumberValue.cs @@ -23,7 +23,7 @@ public partial class NumberValue : Node return GetType().Name + " { value = " + _value._DDD() + ", frozen = " + _frozen + " }"; } - void _OnValueChange( double v ) + protected virtual void _OnValueChange( double v ) { if ( _frozen ) { diff --git a/Runtime/Actions/Visual/TweenFloatShaderProperty.cs b/Runtime/Actions/Visual/TweenFloatShaderProperty.cs index 7e3e05a..9ce737a 100644 --- a/Runtime/Actions/Visual/TweenFloatShaderProperty.cs +++ b/Runtime/Actions/Visual/TweenFloatShaderProperty.cs @@ -13,6 +13,9 @@ namespace Rokojori [Export] public Material material; + [Export] + public GeometryInstance3D instanceTarget; + [Export] public FloatPropertyName propertyName; @@ -58,12 +61,47 @@ namespace Rokojori public bool interruptCurrent = true; protected override void _OnTrigger() + { + if ( material != null && ! IsInstanceValid( material ) ) + { + material = null; + } + + if ( instanceTarget != null && ! IsInstanceValid( instanceTarget ) ) + { + instanceTarget = null; + } + + if ( propertyName == null ) + { + return; + } + + // this.LogInfo( "TRIGGERED" ); + + if ( instanceTarget != null ) + { + // this.LogInfo( "INSTANCE" ); + TweenInstanceUniform(); + return; + } + + if ( material != null ) + { + // this.LogInfo( "MATERIAL" ); + TweenMaterial(); + return; + } + + + } + + void TweenMaterial() { if ( ! interruptCurrent && _actionID != -1 ) { return; } - if ( _actionID != -1 ) { @@ -72,30 +110,30 @@ namespace Rokojori if ( stopAnimation ) { - + AnimationManager.StartAnimation( this, material, propertyName ); if ( setStopValue ) { propertyName.Set( material, endValue ); } - + AnimationManager.EndAnimation( this, material, propertyName ); - + return; } _actionID = DispatchStart(); - + var startValue = propertyName.Get( material ); - + if ( forceStartValue && Engine.IsEditorHint() ) - { + { startValue = forcedStartValue; } @@ -104,15 +142,15 @@ namespace Rokojori startValue = animationStartValue; } - - + + AnimationManager.StartAnimation( this, material, propertyName ); // this.LogInfo( "Get Float Tween", startValue ); _timeID = TimelineManager.ScheduleSpanWith( duration, ( span, type )=> - { + { // this.LogInfo( "Update Float Tween", startValue ); if ( span.id != _timeID ) @@ -138,12 +176,12 @@ namespace Rokojori propertyName.Set( material, value ); // ReflectionHelper.SetValue( material, propertyName , value ); - - if ( type == TimelineSpanUpdateType.End ) - { - // this.LogInfo( "End Float Tween", endValue ); - // target._Set( targetMember, endValue ); + if ( type == TimelineSpanUpdateType.End ) + { + // this.LogInfo( "End Float Tween", endValue ); + + // target._Set( targetMember, endValue ); propertyName.Set( material, endValue ); AnimationManager.EndAnimation( this, material, propertyName ); @@ -156,5 +194,105 @@ namespace Rokojori ).id; } + void TweenInstanceUniform() + { + if ( ! interruptCurrent && _actionID != -1 ) + { + return; + } + + if ( _actionID != -1 ) + { + CancelAction( _actionID ); + } + + if ( stopAnimation ) + { + + AnimationManager.StartAnimation( this, instanceTarget, propertyName.propertyName ); + + if ( setStopValue ) + { + propertyName.SetInstance( instanceTarget, endValue ); + } + + AnimationManager.EndAnimation( this, instanceTarget, propertyName.propertyName ); + + + return; + } + + + _actionID = DispatchStart(); + + var startValue = propertyName.GetInstance( instanceTarget ); + + + + + if ( forceStartValue && Engine.IsEditorHint() ) + { + startValue = forcedStartValue; + } + + if ( animateStartValue ) + { + startValue = animationStartValue; + } + + + + AnimationManager.StartAnimation( this, instanceTarget, propertyName.propertyName ); + + // this.LogInfo( "Get Float Tween", startValue ); + + _timeID = TimelineManager.ScheduleSpanWith( duration, + ( span, type )=> + { + + // this.LogInfo( "Update Float Tween", startValue ); + if ( span.id != _timeID ) + { + return; + } + + if ( ! AnimationManager.IsAnimating( this, instanceTarget, propertyName.propertyName ) ) + { + return; + } + + var phase = span.phase; + + if ( curve != null ) + { + phase = curve.Sample( phase ); + } + + var value = Mathf.Lerp( startValue, endValue, phase ); + + + + // this.LogInfo( "Updating Float Tween", "phase:", phase, "value:", value, instanceTarget ); + + propertyName.SetInstance( instanceTarget, value ); + // ReflectionHelper.SetValue( material, propertyName , value ); + + if ( type == TimelineSpanUpdateType.End ) + { + // this.LogInfo( "End Float Tween", endValue ); + + // target._Set( targetMember, endValue ); + + propertyName.SetInstance( instanceTarget, endValue ); + AnimationManager.EndAnimation( this, instanceTarget, propertyName.propertyName ); + DispatchEnd( _actionID ); + _actionID = -1; + _timeID = -1; + } + }, + this + ).id; + } + } } \ No newline at end of file diff --git a/Runtime/Actions/Visual/TweenMaterial.cs b/Runtime/Actions/Visual/TweenMaterial.cs index addb0d9..a73dc5f 100644 --- a/Runtime/Actions/Visual/TweenMaterial.cs +++ b/Runtime/Actions/Visual/TweenMaterial.cs @@ -89,6 +89,11 @@ namespace Rokojori var fromMaterial = MaterialSurfaceContainer.From( target, this.index ).GetMaterialInSlot( slot ); + // Registers ownership of the *slot assignment itself* (as opposed to the color/float + // properties on the material), so a second TweenMaterial targeting the same slot takes + // over cleanly instead of both tweens fighting to set the end material. + AnimationManager.StartAnimation( this, target, GetSlotKey() ); + SetAssignedMaterial( fromMaterial ); var tl = TimelineManager.Ensure( timeLine ); @@ -180,25 +185,32 @@ namespace Rokojori AnimationManager.EndAnimation( this, _assignedMaterial, f.propertyName ); } - if ( MaterialAssignmentMode.Temporary_Duplicate_For_Each_Tween == assignmentMode ) + // Only write the end material (and release ownership) if no later TweenMaterial + // has since taken over this slot - otherwise this would stomp on it after the fact. + if ( AnimationManager.IsAnimating( this, target, GetSlotKey() ) ) { - _assignedMaterial = null; - var msc = MaterialSurfaceContainer.From( target, index ); - msc.SetMaterialInSlot( slot, toMaterial ); - - var prop = new ColorPropertyName(); - prop.propertyName = "albedo_color"; - - var info = new Color(); - - if ( toMaterial is BaseMaterial3D bm ) + if ( MaterialAssignmentMode.Temporary_Duplicate_For_Each_Tween == assignmentMode ) { - info = bm.AlbedoColor; + _assignedMaterial = null; + var msc = MaterialSurfaceContainer.From( target, index ); + msc.SetMaterialInSlot( slot, toMaterial ); + + var prop = new ColorPropertyName(); + prop.propertyName = "albedo_color"; + + var info = new Color(); + + if ( toMaterial is BaseMaterial3D bm ) + { + info = bm.AlbedoColor; + } + + // this.LogInfo( "Setting:", slot, toMaterial, info, prop.Get( toMaterial ) ); } - // this.LogInfo( "Setting:", slot, toMaterial, info, prop.Get( toMaterial ) ); + AnimationManager.EndAnimation( this, target, GetSlotKey() ); } - + DispatchEnd( sequenceID ); } }, @@ -208,6 +220,11 @@ namespace Rokojori } + string GetSlotKey() + { + return $"MaterialSlot:{index}:{slot}"; + } + void SetAssignedMaterial( Material material ) { if ( MaterialAssignmentMode.Temporary_Duplicate_For_Each_Tween == assignmentMode ) diff --git a/Runtime/Animation/Shake/Shake.cs b/Runtime/Animation/Shake/Shake.cs index adbcd89..28d3a1d 100644 --- a/Runtime/Animation/Shake/Shake.cs +++ b/Runtime/Animation/Shake/Shake.cs @@ -118,6 +118,11 @@ namespace Rokojori for ( int i = 0; i < targets.Length; i++ ) { + if ( ! IsInstanceValid( targets[ i ] ) ) + { + continue; + } + var o = _targetValues[ i ]; o.Set( targets[ i ], shakeEffect.globalPosition, shakeEffect.globalRotation ); @@ -146,6 +151,11 @@ namespace Rokojori for ( int i = 0; i < targets.Length; i++ ) { + if ( ! IsInstanceValid( targets[ i ] ) ) + { + continue; + } + var combined = offsetData.Clone(); var o = _targetValues[ i ]; diff --git a/Runtime/Behavior/Brain.cs b/Runtime/Behavior/Brain.cs deleted file mode 100644 index 36e89c2..0000000 --- a/Runtime/Behavior/Brain.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Godot; - -using Rokojori.Extensions; -namespace Rokojori; - -[Tool] -[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Action.svg")] -public partial class Brain:Action -{ - [Export] - public Receptor[] receptors; - - [Export] - public Node behaviorRoot; - - [Export] - public BehaviorSequence activeSequence; - - bool _paused = false; - - public bool paused => _paused; - - public void SetPaused( bool value ) - { - _paused = value; - } - - protected override void _OnTrigger() - { - if ( _paused ) - { - return; - } - - if ( activeSequence != null && activeSequence.isExclusive && activeSequence.isRunning ) - { - return; - } - - var resolved = Resolve(); - - if ( resolved == null ) - { - return; - } - - if ( resolved == activeSequence ) - { - if ( activeSequence.reinitialize ) - { - activeSequence.CancelAllRunning(); - activeSequence.Trigger(); - } - - return; - } - - var previous = activeSequence; - activeSequence = resolved; - - if ( previous != null ) - { - previous.CancelAllRunning(); - } - - activeSequence.Trigger(); - } - - BehaviorSequence Resolve() - { - if ( behaviorRoot == null ) - { - return null; - } - - return ResolveIn( behaviorRoot ); - } - - BehaviorSequence ResolveIn( Node parent ) - { - var walker = NodesWalker.Get(); - var numChildren = walker.NumChildren( parent ); - - for ( int i = 0; i < numChildren; i++ ) - { - var child = walker.ChildAt( parent, i ); - - if ( child is BehaviorSequence sequence ) - { - return sequence; - } - - if ( child is BehaviorBranch branch ) - { - if ( branch.IsBranchActive( this ) ) - { - var found = ResolveIn( branch ); - - if ( found != null ) - { - return found; - } - } - } - } - - return null; - } -} diff --git a/Runtime/Behavior/BehaviorBranch.cs b/Runtime/Behavior/Core/BehaviorBranch.cs similarity index 83% rename from Runtime/Behavior/BehaviorBranch.cs rename to Runtime/Behavior/Core/BehaviorBranch.cs index 6b3a335..5fffc64 100644 --- a/Runtime/Behavior/BehaviorBranch.cs +++ b/Runtime/Behavior/Core/BehaviorBranch.cs @@ -4,7 +4,7 @@ using Rokojori.Extensions; namespace Rokojori; [Tool] -[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Action.svg")] +[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/BehaviorBranch.svg")] public abstract partial class BehaviorBranch:Node { [Export] diff --git a/Runtime/Behavior/BehaviorBranch.cs.uid b/Runtime/Behavior/Core/BehaviorBranch.cs.uid similarity index 100% rename from Runtime/Behavior/BehaviorBranch.cs.uid rename to Runtime/Behavior/Core/BehaviorBranch.cs.uid diff --git a/Runtime/Behavior/BehaviorSequence.cs b/Runtime/Behavior/Core/BehaviorSequence.cs similarity index 77% rename from Runtime/Behavior/BehaviorSequence.cs rename to Runtime/Behavior/Core/BehaviorSequence.cs index 2b69fd9..7647568 100644 --- a/Runtime/Behavior/BehaviorSequence.cs +++ b/Runtime/Behavior/Core/BehaviorSequence.cs @@ -4,7 +4,7 @@ using Rokojori.Extensions; namespace Rokojori; [Tool] -[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Action.svg")] +[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/BehaviorSequence.svg")] public partial class BehaviorSequence:Sequence { [Export] @@ -29,7 +29,9 @@ public partial class BehaviorSequence:Sequence void OnDone( SequenceActionFinishedEvent ev ) { - if ( _brain == null || _brain.activeSequence != this ) + this.LogInfo( "On Done BS" ); + + if ( _brain == null || _brain.activeNode != this ) { return; } diff --git a/Runtime/Behavior/BehaviorSequence.cs.uid b/Runtime/Behavior/Core/BehaviorSequence.cs.uid similarity index 100% rename from Runtime/Behavior/BehaviorSequence.cs.uid rename to Runtime/Behavior/Core/BehaviorSequence.cs.uid diff --git a/Runtime/Behavior/Core/Brain.cs b/Runtime/Behavior/Core/Brain.cs new file mode 100644 index 0000000..d021012 --- /dev/null +++ b/Runtime/Behavior/Core/Brain.cs @@ -0,0 +1,321 @@ +using Godot; + +using System.Collections.Generic; + +using Rokojori.Extensions; +using System.Linq; +namespace Rokojori; + +[Tool] +[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Brain.svg")] +public partial class Brain:Action +{ + [Export] + public Receptor[] receptors; + + [Export] + public Node[] receporContainers; + + [Export] + public Node behaviorRoot; + + + [ExportGroup( "Debugging")] + [Export] + public Node activeNode; + + + [Export] + public bool printActiveNodeSelection = false; + + [Export] + public bool printBranchResult = false; + + // Non-BehaviorSequence nodes (plain Actions) don't carry this state themselves, + // so these stand in for it. Kept as protected statics in case that needs tuning later. + protected static bool defaultIsExclusive = false; + protected static bool defaultIsRunning = false; + protected static bool defaultReinitialize = true; + + bool _paused = false; + + public bool paused => _paused; + + public void SetPaused( bool value ) + { + _paused = value; + } + + static bool IsExclusive( Node node ) + { + return node is BehaviorSequence sequence ? sequence.isExclusive : defaultIsExclusive; + } + + static bool IsRunning( Node node ) + { + return node is BehaviorSequence sequence ? sequence.isRunning : defaultIsRunning; + } + + static bool ShouldReinitialize( Node node ) + { + return node is BehaviorSequence sequence ? sequence.reinitialize : defaultReinitialize; + } + + static void CancelIfRunning( Node node ) + { + if ( node is BehaviorSequence sequence ) + { + sequence.CancelAllRunning(); + } + } + + public void StartBrain() + { + SetPaused( false ); + + receptors?.ForEachNonNull( r => r.Start() ); + + receporContainers?.ForEachNonNull( c => c.ForEach( r => r.Start() ) ); + + Trigger(); + } + + // The node most recently armed with a one-shot onSequenceDone listener, so a late + // event from a node the Brain has since moved away from doesn't retrigger it. + SequenceAction _listenedSequenceAction; + + // The node a next-frame restart is currently scheduled for, so a plain Action + // doesn't accumulate more than one pending restart chain at a time. + Node _pendingRestartNode; + + void ActivateNode( Node node ) + { + if ( node is Action action ) + { + action.Trigger(); + } + + if ( node is BehaviorSequence ) + { + // Manages its own re-trigger/loop via onSequenceDone already (see BehaviorSequence._Ready). + return; + } + + if ( node is SequenceAction sequenceAction ) + { + _listenedSequenceAction = sequenceAction; + + if ( ! sequenceAction.onSequenceDone.HasAction( OnListenedSequenceActionDone ) ) + { + sequenceAction.onSequenceDone.Once( OnListenedSequenceActionDone ); + } + } + else if ( node is Action ) + { + ScheduleRestartNextFrame( node ); + } + } + + void OnListenedSequenceActionDone( SequenceActionFinishedEvent ev ) + { + if ( activeNode != _listenedSequenceAction ) + { + return; + } + + Trigger(); + } + + async void ScheduleRestartNextFrame( Node node ) + { + if ( _pendingRestartNode == node ) + { + return; + } + + _pendingRestartNode = node; + + await this.RequestNextFrame(); + + if ( _pendingRestartNode == node ) + { + _pendingRestartNode = null; + } + + if ( activeNode != node ) + { + return; + } + + Trigger(); + } + + // Dedup flag for the no-match-resolved restart below, mirroring _pendingRestartNode. + bool _pendingNullResolveRestart = false; + + async void ScheduleRestartNextFrameOnNullResolve() + { + if ( _pendingNullResolveRestart ) + { + return; + } + + _pendingNullResolveRestart = true; + + await this.RequestNextFrame(); + + _pendingNullResolveRestart = false; + + Trigger(); + } + + List _branchInfo = new List(); + + protected override void _OnTrigger() + { + if ( _paused ) + { + if ( printActiveNodeSelection ) + { + this.LogInfo( "Skipped. Paused" ); + } + return; + } + + if ( activeNode != null && IsExclusive( activeNode ) && IsRunning( activeNode ) ) + { + if ( printActiveNodeSelection ) + { + this.LogInfo( "Skipped. Running already exclusively" ); + } + return; + } + + if ( printBranchResult && _branchInfo.Count > 0 ) + { + _branchInfo.Clear(); + } + + var resolved = Resolve(); + + if ( printBranchResult && _branchInfo.Count > 0 ) + { + this.LogInfo( _branchInfo.Join( "\n" ) ); + } + + if ( printActiveNodeSelection ) + { + this.LogInfo( "Active Node:", resolved == null ? "Null" : ( HierarchyName.Of( resolved ) ) ); + } + + if ( resolved == null ) + { + ScheduleRestartNextFrameOnNullResolve(); + return; + } + + if ( resolved == activeNode ) + { + if ( ShouldReinitialize( activeNode ) ) + { + CancelIfRunning( activeNode ); + ActivateNode( activeNode ); + } + + return; + } + + var previous = activeNode; + activeNode = resolved; + + if ( previous != null ) + { + CancelIfRunning( previous ); + } + + ActivateNode( activeNode ); + } + + Node Resolve() + { + if ( behaviorRoot == null ) + { + return null; + } + + return ResolveIn( behaviorRoot ); + } + + Node ResolveIn( Node parent ) + { + var walker = NodesWalker.Get(); + var numChildren = walker.NumChildren( parent ); + + for ( int i = 0; i < numChildren; i++ ) + { + var child = walker.ChildAt( parent, i ); + + if ( child is BehaviorBranch branch && branch.IsBranchActive( this ) ) + { + if ( printBranchResult ) + { + _branchInfo.Add( HierarchyName.Of( child ) ); + } + + var found = ResolveIn( branch ); + + if ( found != null ) + { + return found; + } + + } + + if ( child is SceneCondition condition && condition.Evaluate() ) + { + if ( printBranchResult ) + { + _branchInfo.Add( HierarchyName.Of( child ) ); + } + + var found = ResolveIn( condition ); + + if ( found != null ) + { + return found; + } + + } + + if ( child is Receptor receptor && receptor.active ) + { + if ( printBranchResult ) + { + _branchInfo.Add( HierarchyName.Of( child ) ); + } + + var found = ResolveIn( receptor ); + + if ( found != null ) + { + return found; + } + + } + + if ( child is BehaviorSequence sequence ) + { + return sequence; + } + + if ( child is Action action ) + { + return action; + } + + + } + + return null; + } +} diff --git a/Runtime/Behavior/Brain.cs.uid b/Runtime/Behavior/Core/Brain.cs.uid similarity index 100% rename from Runtime/Behavior/Brain.cs.uid rename to Runtime/Behavior/Core/Brain.cs.uid diff --git a/Runtime/Behavior/BrainControl.cs b/Runtime/Behavior/Core/BrainControl.cs similarity index 63% rename from Runtime/Behavior/BrainControl.cs rename to Runtime/Behavior/Core/BrainControl.cs index 2e01ce6..1d70e61 100644 --- a/Runtime/Behavior/BrainControl.cs +++ b/Runtime/Behavior/Core/BrainControl.cs @@ -4,7 +4,7 @@ using Rokojori.Extensions; namespace Rokojori; [Tool] -[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Action.svg")] +[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/BrainControl.svg")] public partial class BrainControl:Action { public enum Mode @@ -31,7 +31,7 @@ public partial class BrainControl:Action switch ( mode ) { case Mode.Start: - StartBrain(); + brain.StartBrain(); break; case Mode.Pause: @@ -48,21 +48,5 @@ public partial class BrainControl:Action } } - void StartBrain() - { - brain.SetPaused( false ); - - if ( brain.receptors != null ) - { - foreach ( var receptor in brain.receptors ) - { - if ( receptor != null ) - { - receptor.Start(); - } - } - } - - brain.Trigger(); - } + } diff --git a/Runtime/Behavior/BrainControl.cs.uid b/Runtime/Behavior/Core/BrainControl.cs.uid similarity index 100% rename from Runtime/Behavior/BrainControl.cs.uid rename to Runtime/Behavior/Core/BrainControl.cs.uid diff --git a/Runtime/Behavior/ConditionBranch.cs b/Runtime/Behavior/Core/ConditionBranch.cs similarity index 79% rename from Runtime/Behavior/ConditionBranch.cs rename to Runtime/Behavior/Core/ConditionBranch.cs index de402ff..59f20fa 100644 --- a/Runtime/Behavior/ConditionBranch.cs +++ b/Runtime/Behavior/Core/ConditionBranch.cs @@ -4,7 +4,7 @@ using Rokojori.Extensions; namespace Rokojori; [Tool] -[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Action.svg")] +[GlobalClass] public partial class ConditionBranch:BehaviorBranch { [Export] diff --git a/Runtime/Behavior/ConditionBranch.cs.uid b/Runtime/Behavior/Core/ConditionBranch.cs.uid similarity index 100% rename from Runtime/Behavior/ConditionBranch.cs.uid rename to Runtime/Behavior/Core/ConditionBranch.cs.uid diff --git a/Runtime/Behavior/ReceptorBranch.cs b/Runtime/Behavior/Core/ReceptorBranch.cs similarity index 76% rename from Runtime/Behavior/ReceptorBranch.cs rename to Runtime/Behavior/Core/ReceptorBranch.cs index 7f7d457..43d233a 100644 --- a/Runtime/Behavior/ReceptorBranch.cs +++ b/Runtime/Behavior/Core/ReceptorBranch.cs @@ -4,7 +4,7 @@ using Rokojori.Extensions; namespace Rokojori; [Tool] -[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Action.svg")] +[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/ReceptorBranch.svg")] public partial class ReceptorBranch:BehaviorBranch { [Export] diff --git a/Runtime/Behavior/ReceptorBranch.cs.uid b/Runtime/Behavior/Core/ReceptorBranch.cs.uid similarity index 100% rename from Runtime/Behavior/ReceptorBranch.cs.uid rename to Runtime/Behavior/Core/ReceptorBranch.cs.uid diff --git a/Runtime/Behavior/FollowBehavior.cs b/Runtime/Behavior/FollowBehavior.cs deleted file mode 100644 index 9d2d102..0000000 --- a/Runtime/Behavior/FollowBehavior.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Godot; - -using Rokojori.Extensions; -namespace Rokojori; - -[Tool] -[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Action.svg")] -public partial class FollowBehavior:BehaviorSequence -{ - [Export] - public BehaviorMovement movement; - - [Export] - public Node3D target; - - protected override void _OnTrigger() - { - if ( movement != null ) - { - movement.Follow( target ); - } - - base._OnTrigger(); - } -} diff --git a/Runtime/Behavior/BehaviorMovement.cs b/Runtime/Behavior/Movements/BehaviorMovement.cs similarity index 83% rename from Runtime/Behavior/BehaviorMovement.cs rename to Runtime/Behavior/Movements/BehaviorMovement.cs index 4e1de7e..d1ad9ac 100644 --- a/Runtime/Behavior/BehaviorMovement.cs +++ b/Runtime/Behavior/Movements/BehaviorMovement.cs @@ -4,7 +4,7 @@ using Rokojori.Extensions; namespace Rokojori; [Tool] -[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Action.svg")] +[GlobalClass] public abstract partial class BehaviorMovement:Node { public abstract void GoToPosition( Vector3 worldPosition ); diff --git a/Runtime/Behavior/BehaviorMovement.cs.uid b/Runtime/Behavior/Movements/BehaviorMovement.cs.uid similarity index 100% rename from Runtime/Behavior/BehaviorMovement.cs.uid rename to Runtime/Behavior/Movements/BehaviorMovement.cs.uid diff --git a/Runtime/Behavior/CharacterBodyBehaviorMovement.cs b/Runtime/Behavior/Movements/CharacterBodyBehaviorMovement.cs similarity index 96% rename from Runtime/Behavior/CharacterBodyBehaviorMovement.cs rename to Runtime/Behavior/Movements/CharacterBodyBehaviorMovement.cs index e3653f0..ddbe14b 100644 --- a/Runtime/Behavior/CharacterBodyBehaviorMovement.cs +++ b/Runtime/Behavior/Movements/CharacterBodyBehaviorMovement.cs @@ -4,7 +4,7 @@ using Rokojori.Extensions; namespace Rokojori; [Tool] -[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Action.svg")] +[GlobalClass] public partial class CharacterBodyBehaviorMovement:BehaviorMovement { [Export] diff --git a/Runtime/Behavior/CharacterBodyBehaviorMovement.cs.uid b/Runtime/Behavior/Movements/CharacterBodyBehaviorMovement.cs.uid similarity index 100% rename from Runtime/Behavior/CharacterBodyBehaviorMovement.cs.uid rename to Runtime/Behavior/Movements/CharacterBodyBehaviorMovement.cs.uid diff --git a/Runtime/Behavior/Movements/FollowBehavior.cs b/Runtime/Behavior/Movements/FollowBehavior.cs new file mode 100644 index 0000000..f6ae51a --- /dev/null +++ b/Runtime/Behavior/Movements/FollowBehavior.cs @@ -0,0 +1,33 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori; + +[Tool] +[GlobalClass] +public partial class FollowBehavior:Action +{ + [Export] + public BehaviorMovement movement; + + [Export] + public Node3D target; + + protected override void _OnTrigger() + { + if ( movement == null || target == null ) + { + return; + } + + // var id = DispatchStart(); + + movement.Follow( target ); + + // await this.RequestNextFrame(); + + // this.LogInfo( "Done" ); + + // DispatchEnd( id ); + } +} diff --git a/Runtime/Behavior/FollowBehavior.cs.uid b/Runtime/Behavior/Movements/FollowBehavior.cs.uid similarity index 100% rename from Runtime/Behavior/FollowBehavior.cs.uid rename to Runtime/Behavior/Movements/FollowBehavior.cs.uid diff --git a/Runtime/Behavior/Receptor.cs b/Runtime/Behavior/Receptors/Receptor.cs similarity index 73% rename from Runtime/Behavior/Receptor.cs rename to Runtime/Behavior/Receptors/Receptor.cs index 13ed2b8..3e09562 100644 --- a/Runtime/Behavior/Receptor.cs +++ b/Runtime/Behavior/Receptors/Receptor.cs @@ -4,25 +4,29 @@ using Rokojori.Extensions; namespace Rokojori; [Tool] -[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Action.svg")] +[GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Receptor.svg")] public partial class Receptor:Action { [Export] public SceneCondition sceneCondition; [Export( PropertyHint.Range, "0.1,100")] - public float receiptionTriggerCoverage = 75f; + public float receptionTriggerCoverage = 75f; [Export( PropertyHint.Range, "10,2000")] - public float receiptionWindowDurationMS = 500f; + 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( receiptionWindowDurationMS / tickRateMS ); + int resultSize => Mathf.CeilToInt( receptionWindowDurationMS / tickRateMS ); RingBuffer _resultsBuffer; @@ -70,8 +74,11 @@ public partial class Receptor:Action void Sample() { + // this.LogInfo( "Receptor Update" ); + if ( sceneCondition == null ) { + // this.LogInfo( "No condition" ); return; } @@ -84,12 +91,15 @@ public partial class Receptor:Action if ( _resultsBuffer.count != resultSize ) { + // this.LogInfo( "No activation" ); _activated = false; return; } var numActive = _resultsBuffer.CountItems( b => b ); - _activated = numActive / (float) resultSize >= receiptionTriggerCoverage / 100f; + fillState = numActive / (float) resultSize; + + _activated = fillState >= receptionTriggerCoverage / 100f; } } diff --git a/Runtime/Behavior/Receptor.cs.uid b/Runtime/Behavior/Receptors/Receptor.cs.uid similarity index 100% rename from Runtime/Behavior/Receptor.cs.uid rename to Runtime/Behavior/Receptors/Receptor.cs.uid diff --git a/Runtime/Cameras/CameraTypes/ThirdPersonCamera/SetThirdPersonCameraLockTarget.cs b/Runtime/Cameras/CameraTypes/ThirdPersonCamera/SetThirdPersonCameraLockTarget.cs deleted file mode 100644 index 5f4fb06..0000000 --- a/Runtime/Cameras/CameraTypes/ThirdPersonCamera/SetThirdPersonCameraLockTarget.cs +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index 12fb438..0000000 --- a/Runtime/Cameras/CameraTypes/ThirdPersonCamera/SetThirdPersonCameraLockTarget.cs.uid +++ /dev/null @@ -1 +0,0 @@ -uid://pesdcfjnpi30 diff --git a/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCamera.cs b/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCamera.cs index 6a01446..49e8fef 100644 --- a/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCamera.cs +++ b/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCamera.cs @@ -17,7 +17,7 @@ namespace Rokojori public Node3D target; [Export] - public Node3D lockTarget; + public CameraLocker cameraLocker; [Export] public float yaw; @@ -68,12 +68,6 @@ namespace Rokojori return; } - if ( lockTarget != null && ! IsInstanceValid( lockTarget ) ) - { - lockTarget = null; - } - - // var tl = TimeLineManager.Ensure( timeLine ); // var delta = tl.delta; var delta = (float) gameDelta; @@ -81,19 +75,14 @@ namespace Rokojori var targetPosition = Smoothing.Apply( settings.targetFollowSmoothing, target.GlobalPosition, delta ); - var hasLockTarget = lockTarget != null; + var lockTargetNode = cameraLocker != null && cameraLocker.isLocked && IsInstanceValid( cameraLocker.lockTarget ) ? cameraLocker.lockTarget.pointerPosition : null; + var hasLockTarget = lockTargetNode != null; var lockTargetPosition = _lastLockTargetPosition; if ( hasLockTarget ) { - lockTargetPosition = Smoothing.Apply( settings.lockTargetFollowSmoothing, lockTarget.GlobalPosition, delta ); + lockTargetPosition = Smoothing.Apply( settings.lockTargetFollowSmoothing, lockTargetNode.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 diff --git a/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCameraSettings.cs b/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCameraSettings.cs index c8e93a0..44fc73d 100644 --- a/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCameraSettings.cs +++ b/Runtime/Cameras/CameraTypes/ThirdPersonCamera/ThirdPersonCameraSettings.cs @@ -81,9 +81,6 @@ namespace Rokojori [Export] public Smoothing lockBlendSmoothing = new FrameSmoothing(); - [Export] - public float lockReleaseDistance = 0f; - [Export] public float lockMinPitch = -10; diff --git a/Runtime/Conditions/HasCondition.cs b/Runtime/Conditions/HasCondition.cs new file mode 100644 index 0000000..45792f2 --- /dev/null +++ b/Runtime/Conditions/HasCondition.cs @@ -0,0 +1,205 @@ + +using System.Collections.Generic; +using System.Reflection; +using Godot; + + +using Rokojori.Extensions; +namespace Rokojori +{ + [Tool] + [GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Condition.svg")] + public partial class HasCondition: SceneCondition + { + bool _enteredTree = false; + + public override void _EnterTree() + { + base._EnterTree(); + _enteredTree = true; + } + + void OnTargetAssignmentChanged() + { + // Deserialization sets exported properties before _EnterTree fires, so this + // only triggers for genuine interactive edits in the editor, never for a scene + // being loaded (regardless of property order) or for runtime reassignment. + if ( ! _enteredTree || ! Engine.IsEditorHint() ) + { + return; + } + + targetName = ""; + +#if TOOLS + NotifyPropertyListChanged(); +#endif + } + + GodotObject _target; + + [Export] + public GodotObject target + { + get => _target; + set + { + if ( _target == value ) + { + return; + } + + _target = value; + OnTargetAssignmentChanged(); + } + } + + Node _targetNode; + + [Export] + public Node targetNode + { + get => _targetNode; + set + { + if ( _targetNode == value ) + { + return; + } + + _targetNode = value; + OnTargetAssignmentChanged(); + } + } + + [Export] + public string targetName; + + GodotObject GetEffectiveTarget() + { + return target != null ? target : (GodotObject) targetNode; + } + + public override bool Evaluate() + { + var effectiveTarget = GetEffectiveTarget(); + + if ( effectiveTarget == null || string.IsNullOrEmpty( targetName ) ) + { + return false; + } + + var value = ReflectionHelper.GetDataMemberValue( effectiveTarget, targetName ); + + return EvaluateValue( value ); + } + + static bool EvaluateValue( object value ) + { + switch ( value ) + { + case null: + return false; + + case GodotObject go: + return GodotObject.IsInstanceValid( go ); + + case bool b: + return b; + + case string s: + return ! string.IsNullOrEmpty( s ); + + case int i: + return i > 0; + + case long l: + return l > 0; + + case float f: + return f > 0; + + case double d: + return d > 0; + + default: + return false; + } + } + +#if TOOLS + + static bool IsSupportedMemberType( System.Type type ) + { + if ( type == null ) + { + return false; + } + + if ( type == typeof( bool ) || type == typeof( string ) ) + { + return true; + } + + if ( type == typeof( int ) || type == typeof( long ) || type == typeof( float ) || type == typeof( double ) ) + { + return true; + } + + if ( typeof( Node ).IsAssignableFrom( type ) || typeof( Resource ).IsAssignableFrom( type ) ) + { + return true; + } + + return false; + } + + List GetTargetMemberNames() + { + var names = new List(); + var effectiveTarget = GetEffectiveTarget(); + + if ( effectiveTarget == null ) + { + return names; + } + + var infos = ReflectionHelper.GetDataMemberInfos( effectiveTarget, BindingFlags.Public | BindingFlags.Instance ); + + foreach ( var info in infos ) + { + if ( ! info.IsDefined( typeof( ExportAttribute ) ) ) + { + continue; + } + + if ( ! IsSupportedMemberType( info.GetMemberType() ) ) + { + continue; + } + + names.Add( info.Name ); + } + + return names; + } + + public override void _ValidateProperty( Godot.Collections.Dictionary property ) + { + if ( property[ "name" ].AsString() != nameof( targetName ) ) + { + return; + } + + var members = GetTargetMemberNames(); + + property[ "hint" ] = (long) PropertyHint.Enum; + property[ "hint_string" ] = members.Count == 0 + ? "(Invalid Target)" + : "None," + string.Join( ",", members ); + } + +#endif + + } +} diff --git a/Runtime/Conditions/HasCondition.cs.uid b/Runtime/Conditions/HasCondition.cs.uid new file mode 100644 index 0000000..d4aac8f --- /dev/null +++ b/Runtime/Conditions/HasCondition.cs.uid @@ -0,0 +1 @@ +uid://de0vq8n1ylgjn diff --git a/Runtime/GameObjects/GameObject3D.cs b/Runtime/GameObjects/GameObject3D.cs index 83ce080..b81abfb 100644 --- a/Runtime/GameObjects/GameObject3D.cs +++ b/Runtime/GameObjects/GameObject3D.cs @@ -14,6 +14,7 @@ namespace Rokojori [GlobalClass, Icon("res://addons/rokojori_action_library/Icons/GameObject3D.svg") ] public partial class GameObject3D:Node3D, INetworkNode { + [ExportToolButton("Center On Children")] public Callable centerOnChildrenButton => Callable.From( ()=> { this.CenterOnChildren(); } ); diff --git a/Runtime/GameObjects/GameObject3DFlag.cs b/Runtime/GameObjects/GameObject3DFlag.cs new file mode 100644 index 0000000..4764af1 --- /dev/null +++ b/Runtime/GameObjects/GameObject3DFlag.cs @@ -0,0 +1,15 @@ + +using Godot; +using Rokojori; +using System.Collections.Generic; + +using Rokojori.Extensions; +namespace Rokojori; + + +[Tool] +[GlobalClass] +public partial class GameObject3DFlag:SelectorFlag +{ + +} diff --git a/Runtime/GameObjects/GameObject3DFlag.cs.uid b/Runtime/GameObjects/GameObject3DFlag.cs.uid new file mode 100644 index 0000000..61a69c6 --- /dev/null +++ b/Runtime/GameObjects/GameObject3DFlag.cs.uid @@ -0,0 +1 @@ +uid://exkchmx40u0h diff --git a/Runtime/Godot/Extensions/ArrayExtensions.cs b/Runtime/Godot/Extensions/ArrayExtensions.cs index 9a9bf8d..72d9ea0 100644 --- a/Runtime/Godot/Extensions/ArrayExtensions.cs +++ b/Runtime/Godot/Extensions/ArrayExtensions.cs @@ -16,6 +16,40 @@ namespace Rokojori } } + public static void ForEach<[MustBeVariant] T>( this T[] array, System.Action action ) + { + for (int i = 0; i < array.Length; i++) + { + action( array[i] ); + } + } + + public static void ForEachNonNull<[MustBeVariant] T>( this Godot.Collections.Array array, System.Action action ) where T:class + { + for (int i = 0; i < array.Count; i++) + { + if ( array[ i ] == null ) + { + continue; + } + + action( array[i] ); + } + } + + public static void ForEachNonNull( this T[] array, System.Action action ) where T:class + { + for (int i = 0; i < array.Length; i++) + { + if ( array[ i ] == null ) + { + continue; + } + + action( array[i] ); + } + } + public static T Find<[MustBeVariant] T>( this Godot.Collections.Array array, System.Func evaluater, int offset = 0 ) { for ( int i = offset; i < array.Count; i++ ) diff --git a/Runtime/Godot/Nodes.cs b/Runtime/Godot/Nodes.cs index bb64c2a..2df7aeb 100644 --- a/Runtime/Godot/Nodes.cs +++ b/Runtime/Godot/Nodes.cs @@ -435,6 +435,50 @@ namespace Rokojori return list; } + public static Node Select( this Node root, Selector selector ) + { + var walker = nodesWalker; + + var end = walker.IterationEndOf( root ); + + var it = root; + + while ( it != end ) + { + if ( selector.Selects( it ) ) + { + return it; + } + + it = walker.NextNode( it ); + } + + return null; + } + + public static List SelectAll( this Node root, Selector selector ) + { + var walker = nodesWalker; + + var end = walker.IterationEndOf( root ); + + var it = root; + + var list = new List(); + + while ( it != end ) + { + if ( selector.Selects( it ) ) + { + list.Add( it ); + } + + it = walker.NextNode( it ); + } + + return null; + } + public static void ForEach( this Node root, Action callback ) where T:class { var walker = nodesWalker; diff --git a/Runtime/Interactions/LockOn/CameraLocker.cs b/Runtime/Interactions/LockOn/CameraLocker.cs new file mode 100644 index 0000000..26036a3 --- /dev/null +++ b/Runtime/Interactions/LockOn/CameraLocker.cs @@ -0,0 +1,293 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori +{ + // Single owner of lock state. LockOnPointer only tracks candidates and the current selection; + // ThirdPersonCamera (or anything else) only reads isLocked/lockTarget from here. + [Tool] + [GlobalClass] + public partial class CameraLocker : Action + { + [Export] + public LockOnPointer source; + + [Export] + public Sensor engageSensor; + + [ExportGroup( "Release" )] + [Export] + public float releaseDistance = 0f; + + [Export] + public Node3D releaseDistanceOrigin; + + [ExportGroup( "Cycling" )] + [Export] + public LockOnCyclingSetup cyclingSetup; + + [Export] + public LockOnCyclingSetup lockedCyclingSetup; + + [ExportGroup( "Visualizer" )] + [Export] + public Node3D visualizerMarker; + + [ExportGroup( "Candidate Events" )] + [Export] + public Action onCandidateSpotted; + + [Export] + public Action onCandidateUnlocked; + + [Export] + public Action onCandidateVanished; + + [Export] + public Action onCandidateChanged; + + [Export] + public bool triggerCanidatesWhenLocked = false; + + + [ExportGroup( "Lock Events" )] + [Export] + public Action onLock; + + [Export] + public Action onReleasedLock; + + [Export] + public Action onChangeLocked; + + [ExportGroup( "Read Only" )] + [Export] + public Pointable lockTarget; + + public bool isLocked => lockTarget != null; + + Pointable _previousCandidate; + bool _wasEngaged = false; + + protected override void _OnTrigger() + { + if ( source == null ) + { + return; + } + + source.Trigger(); + + UpdateCandidateEvents(); + + var previousTarget = lockTarget; + var wasLocked = previousTarget != null; + + // Order matters: cycling may move an existing lock, engagement may start/end one, + // auto-release may end one - each only touches lockTarget for its own reason, nothing + // here resyncs it every frame just because source.pointable drifted on its own. + UpdateCycling(); + UpdateEngagement(); + UpdateAutoRelease(); + + FireLockEvents( wasLocked, previousTarget ); + + UpdateVisualizer(); + } + + public void Release() + { + if ( lockTarget == null ) + { + return; + } + + var previousTarget = lockTarget; + lockTarget = null; + + FireLockEvents( true, previousTarget ); + } + + void UpdateCandidateEvents() + { + if ( lockTarget != null && ! triggerCanidatesWhenLocked ) + { + return; + } + + var candidate = source.pointable; + + if ( candidate == _previousCandidate ) + { + return; + } + + _previousCandidate = candidate; + + onCandidateChanged?.Trigger(); + + Action.TriggerSafe( candidate == null ? onCandidateVanished : onCandidateSpotted ); + } + + void UpdateCycling() + { + var setup = isLocked ? ( lockedCyclingSetup ?? cyclingSetup ) : ( cyclingSetup ?? lockedCyclingSetup ); + + if ( setup == null ) + { + return; + } + + var cycled = false; + + if ( setup.cycleMode == LockOnCycleMode.ClosestCycle ) + { + if ( setup.cycleSensor != null && setup.cycleSensor.isDown ) + { + source.CycleNext(); + cycled = true; + } + } + else if ( setup.cycleMode == LockOnCycleMode.Directional && setup.directionalCycleInputs != null ) + { + for ( int i = 0; i < setup.directionalCycleInputs.Length; i++ ) + { + var input = setup.directionalCycleInputs[ i ]; + + if ( input == null || input.sensor == null || ! input.sensor.isDown ) + { + continue; + } + + source.CycleInScreenSpaceDirection( input.direction ); + cycled = true; + break; + } + } + + // Cycling is the only thing that is allowed to move an already-established lock onto a + // different candidate - a deliberate, one-time assignment here, not a per-frame resync. + if ( cycled && isLocked ) + { + lockTarget = source.pointable; + } + } + + void UpdateEngagement() + { + var engaged = engageSensor != null && engageSensor.isActive; + var risingEdge = engaged && ! _wasEngaged; + _wasEngaged = engaged; + + if ( ! engaged ) + { + lockTarget = null; + return; + } + + // Only a fresh press/toggle-on acquires a target. Once locked, the lock is held + // regardless of what source.pointable does on its own (e.g. losing/regaining the raycast + // hit) - it only changes via cycling, auto-release, or an explicit Release() call. + if ( lockTarget == null && risingEdge ) + { + lockTarget = source.pointable; + } + } + + void UpdateAutoRelease() + { + if ( lockTarget == null ) + { + return; + } + + if ( IsGone( lockTarget ) ) + { + lockTarget = null; + return; + } + + if ( IsTooFar( lockTarget ) ) + { + lockTarget = null; + } + } + + // Covers both an actually freed node and one that was removed from the tree without being + // freed (e.g. pooled/recycled enemies) - either way it is no longer something the lock + // should keep pointing at. + static bool IsGone( Pointable target ) + { + return target == null || ! IsInstanceValid( target ) || ! target.IsInsideTree(); + } + + bool IsTooFar( Pointable target ) + { + if ( releaseDistance <= 0f ) + { + return false; + } + + var origin = releaseDistanceOrigin ?? source.caster; + + if ( origin == null ) + { + return false; + } + + return origin.GlobalPosition.DistanceTo( target.GlobalPosition ) > releaseDistance; + } + + void ReleaseEngageSensorIfToggled() + { + if ( engageSensor is ActivationModeSensor ams && ams.isActive ) + { + ams.Release(); + } + } + + void FireLockEvents( bool wasLocked, Pointable previousTarget ) + { + var isLockedNow = lockTarget != null; + + if ( isLockedNow && ! wasLocked ) + { + Action.TriggerSafe( onLock ); + return; + } + + if ( ! isLockedNow && wasLocked ) + { + ReleaseEngageSensorIfToggled(); + Action.TriggerSafe( onReleasedLock ); + + if ( source.IsCandidate( previousTarget ) ) + { + Action.TriggerSafe( onCandidateUnlocked ); + } + + return; + } + + if ( isLockedNow && wasLocked && lockTarget != previousTarget ) + { + Action.TriggerSafe( onChangeLocked ); + } + } + + void UpdateVisualizer() + { + if ( visualizerMarker == null ) + { + return; + } + + var current = lockTarget ?? source.pointable; + + if ( ! IsGone( current ) ) + { + visualizerMarker.CopyGlobalPoseFrom( current.pointerPosition ); + } + } + } +} diff --git a/Runtime/Interactions/LockOn/CameraLocker.cs.uid b/Runtime/Interactions/LockOn/CameraLocker.cs.uid new file mode 100644 index 0000000..e2e1aa0 --- /dev/null +++ b/Runtime/Interactions/LockOn/CameraLocker.cs.uid @@ -0,0 +1 @@ +uid://dskrrdkp3hcu6 diff --git a/Runtime/Interactions/LockOn/LockOnCyclingSetup.cs b/Runtime/Interactions/LockOn/LockOnCyclingSetup.cs new file mode 100644 index 0000000..d18e09b --- /dev/null +++ b/Runtime/Interactions/LockOn/LockOnCyclingSetup.cs @@ -0,0 +1,19 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori +{ + [Tool] + [GlobalClass] + public partial class LockOnCyclingSetup : Resource + { + [Export] + public LockOnCycleMode cycleMode = LockOnCycleMode.None; + + [Export] + public Sensor cycleSensor; + + [Export] + public DirectionalCycleInput[] directionalCycleInputs = []; + } +} diff --git a/Runtime/Interactions/LockOn/LockOnCyclingSetup.cs.uid b/Runtime/Interactions/LockOn/LockOnCyclingSetup.cs.uid new file mode 100644 index 0000000..b822f4f --- /dev/null +++ b/Runtime/Interactions/LockOn/LockOnCyclingSetup.cs.uid @@ -0,0 +1 @@ +uid://bt07jjknw0aty diff --git a/Runtime/Interactions/LockOn/LockOnPointer.cs b/Runtime/Interactions/LockOn/LockOnPointer.cs index de4005b..5ce6f46 100644 --- a/Runtime/Interactions/LockOn/LockOnPointer.cs +++ b/Runtime/Interactions/LockOn/LockOnPointer.cs @@ -11,52 +11,28 @@ namespace Rokojori Directional } + // Only tracks the current candidate list and the single selected pointable (sticky - stays on + // the selection until it is cycled away from or stops being a valid candidate). Owns no concept + // of "locked" - that is CameraLocker's job, reading/driving this pointer's pointable. [Tool] [GlobalClass] public partial class LockOnPointer : Pointer { - [Export] - public Sensor engageSensor; + public int test = 0; + + List _candidates = new List(); - [Export] - public bool releaseWhenTargetInvalid = true; + public int numCandidates => _candidates.Count; - [Export] - public Action onLockEngaged; + public Pointable GetCandidate( int index ) + { + return _candidates[ index ]; + } - [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; + public bool IsCandidate( Pointable p ) + { + return p != null && _candidates.Contains( p ); + } protected override void _OnTrigger() { @@ -68,185 +44,68 @@ namespace Rokojori if ( pointable != null && ! IsInstanceValid( pointable ) ) { pointable = null; - - if ( engageSensor.isActive && engageSensor is ActivationModeSensor ams ) - { - ams.Release(); - } - - onLockReleased?.Trigger(); } - UpdateClosestCandidate(); + RefreshCandidates(); - var wasLocked = pointable != null; - - if ( visualizer != null && pointable != null ) + if ( pointable == null || ! _candidates.Contains( pointable ) ) { - 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 ); + SetPointable( ClosestCandidate() ); } } - void UpdateClosestCandidate() + void RefreshCandidates() { - var next = FindClosestCandidate(); + _candidates.Clear(); - 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; + _candidates.Add( p ); } } - - return null; } - Pointable FindNextClosestCandidate() + Pointable ClosestCandidate() { - 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 ]; + return _candidates.Count == 0 ? null : _candidates[ 0 ]; } - Pointable FindDirectionalCandidate( Vector2 direction ) + public void CycleNext() + { + if ( _candidates.Count == 0 ) + { + return; + } + + var currentIndex = _candidates.IndexOf( pointable ); + var nextIndex = currentIndex == -1 ? 0 : ( currentIndex + 1 ) % _candidates.Count; + + SetPointable( _candidates[ nextIndex ] ); + } + + public void CyclePrevious() + { + if ( _candidates.Count == 0 ) + { + return; + } + + var currentIndex = _candidates.IndexOf( pointable ); + var previousIndex = currentIndex == -1 ? 0 : ( currentIndex - 1 + _candidates.Count ) % _candidates.Count; + + SetPointable( _candidates[ previousIndex ] ); + } + + public void CycleInScreenSpaceDirection( Vector2 screenSpaceDirection ) { if ( pointable == null ) { - return FindClosestCandidate(); + SetPointable( ClosestCandidate() ); + return; } var right = caster.GlobalTransform.Basis.X; @@ -256,11 +115,11 @@ namespace Rokojori Pointable best = null; var bestScore = float.NegativeInfinity; - for ( int i = 0; i < caster.NumColliders(); i++ ) + for ( int i = 0; i < _candidates.Count; i++ ) { - var candidate = Nodes.Find( caster.GetCollider( i ) ); + var candidate = _candidates[ i ]; - if ( candidate == null || candidate == pointable || ! candidate.enabled ) + if ( candidate == pointable ) { continue; } @@ -273,7 +132,7 @@ namespace Rokojori continue; } - var score = projected.Normalized().Dot( direction ); + var score = projected.Normalized().Dot( screenSpaceDirection ); if ( score > bestScore ) { @@ -282,20 +141,10 @@ namespace Rokojori } } - return best; - } - - bool IsStillValidTarget( Pointable p ) - { - for ( int i = 0; i < caster.NumColliders(); i++ ) + if ( best != null ) { - if ( Nodes.Find( caster.GetCollider( i ) ) == p ) - { - return true; - } + SetPointable( best ); } - - return false; } } } diff --git a/Runtime/Interactions/LockOn/ReleaseCameraLock.cs b/Runtime/Interactions/LockOn/ReleaseCameraLock.cs new file mode 100644 index 0000000..80786f6 --- /dev/null +++ b/Runtime/Interactions/LockOn/ReleaseCameraLock.cs @@ -0,0 +1,18 @@ +using Godot; + +using Rokojori.Extensions; +namespace Rokojori +{ + [Tool] + [GlobalClass] + public partial class ReleaseCameraLock : Action + { + [Export] + public CameraLocker cameraLocker; + + protected override void _OnTrigger() + { + cameraLocker?.Release(); + } + } +} diff --git a/Runtime/Interactions/LockOn/ReleaseCameraLock.cs.uid b/Runtime/Interactions/LockOn/ReleaseCameraLock.cs.uid new file mode 100644 index 0000000..1479c03 --- /dev/null +++ b/Runtime/Interactions/LockOn/ReleaseCameraLock.cs.uid @@ -0,0 +1 @@ +uid://b6ipiialv2te8 diff --git a/Runtime/Selectors/FlagSelector.cs b/Runtime/Selectors/FlagSelector.cs index 4d1b2c4..e192404 100644 --- a/Runtime/Selectors/FlagSelector.cs +++ b/Runtime/Selectors/FlagSelector.cs @@ -24,7 +24,18 @@ namespace Rokojori } [Export] - public FlagMode mode = FlagMode.Needs_One_Flag; + public FlagMode flagMode = FlagMode.Needs_One_Flag; + + public enum IterationMode + { + Only_Node, + Only_Iterator, + Node_And_Iterator + } + + [Export] + public IterationMode iterationMode= IterationMode.Node_And_Iterator; + [Export] public TreeIteratorType iteratorType = TreeIteratorType.DirectChildren; @@ -40,8 +51,13 @@ namespace Rokojori numPresent ++; } } + + if ( numPresent == flag.Length ) + { + return true; + } - if ( numPresent == flag.Length || mode == FlagMode.Needs_One_Flag && numPresent > 0 ) + if ( flagMode == FlagMode.Needs_One_Flag && numPresent > 0 ) { return true; } @@ -51,6 +67,21 @@ namespace Rokojori public override bool Selects( Node node ) { + + if ( IterationMode.Only_Node == iterationMode ) + { + return node is Selectable s && SelectsSelectable( s ); + } + + if ( IterationMode.Only_Iterator != iterationMode ) + { + if ( node is Selectable s && SelectsSelectable( s ) ) + { + return true; + } + } + + var iterator = TreeIterator.GetIterator( iteratorType, node, NodesWalker.Get() ); while ( iterator.HasNext() ) diff --git a/Runtime/Selectors/SelectManager.cs b/Runtime/Selectors/SelectManager.cs new file mode 100644 index 0000000..a3fc0fd --- /dev/null +++ b/Runtime/Selectors/SelectManager.cs @@ -0,0 +1,57 @@ + +using System.Diagnostics; +using System.Collections; +using System.Collections.Generic; +using System; +using Godot; + + +using Rokojori.Extensions; +namespace Rokojori +{ + [Tool] + [GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Selector.svg")] + public partial class SelectManager:Node + { + [Export] + public Node root; + + Dictionary _singleSelections = new Dictionary(); + Dictionary> _listSelections = new Dictionary>(); + + public Node GetSelectionRoot() + { + return root == null ? GetTree().Root : root; + } + + public Node Select( Selector selector ) + { + if ( selector == null ) + { + return null; + } + + if ( ! _singleSelections.ContainsKey( selector ) ) + { + _singleSelections[ selector ] = GetSelectionRoot().Select( selector ); + } + + return _singleSelections[ selector ]; + } + + public List SelectAll( Selector selector ) + { + if ( selector == null ) + { + return new List(); + } + + if ( ! _singleSelections.ContainsKey( selector ) ) + { + _listSelections[ selector ] = GetSelectionRoot().SelectAll( selector ); + } + + return _listSelections[ selector ]; + } + } +} \ No newline at end of file diff --git a/Runtime/Selectors/SelectManager.cs.uid b/Runtime/Selectors/SelectManager.cs.uid new file mode 100644 index 0000000..6802aa7 --- /dev/null +++ b/Runtime/Selectors/SelectManager.cs.uid @@ -0,0 +1 @@ +uid://7ogqhvo3x5og diff --git a/Runtime/Shading/Properties/ShaderPropertyName.cs b/Runtime/Shading/Properties/ShaderPropertyName.cs index ab79a10..26b487b 100644 --- a/Runtime/Shading/Properties/ShaderPropertyName.cs +++ b/Runtime/Shading/Properties/ShaderPropertyName.cs @@ -36,6 +36,8 @@ namespace Rokojori { var variant = Variant.From( value ); mi.SetInstanceShaderParameter( propertyName, variant ); + + // RJLog.Log( "Set", propertyName, variant ); return; } diff --git a/Runtime/Tools/ListExtensions.cs b/Runtime/Tools/ListExtensions.cs index ceb0a9e..3b97de8 100644 --- a/Runtime/Tools/ListExtensions.cs +++ b/Runtime/Tools/ListExtensions.cs @@ -155,6 +155,20 @@ public static class ListExtensions ); } + public static void ForEachNonNull( this List elements, Action callback ) where T:class + { + foreach ( var e in elements ) + { + if ( e == null ) + { + continue; + } + + callback( e ); + } + } + + public static List Sub( this List elements, int start, int length = 0 ) { var end = length == 0 ? elements.Count : ( start + length ); diff --git a/Runtime/UI/Nodes/Image/UIImageTypes/CustomUIImageType.cs b/Runtime/UI/Nodes/Image/UIImageTypes/CustomUIImageType.cs new file mode 100644 index 0000000..ebf7eff --- /dev/null +++ b/Runtime/UI/Nodes/Image/UIImageTypes/CustomUIImageType.cs @@ -0,0 +1,24 @@ + +using Godot; +using Rokojori; +using System.Collections.Generic; + +using Rokojori.Extensions; +namespace Rokojori +{ + [Tool] + [GlobalClass,Icon("res://addons/rokojori_action_library/Icons/UIImage.svg")] + public partial class CustomUIImageType:UIImageType + { + [Export] + public Material material; + + + protected override void _Assign( UIImage image ) + { + image.Material = material; + + } + + } +} \ No newline at end of file diff --git a/Runtime/UI/Nodes/Image/UIImageTypes/CustomUIImageType.cs.uid b/Runtime/UI/Nodes/Image/UIImageTypes/CustomUIImageType.cs.uid new file mode 100644 index 0000000..65bf169 --- /dev/null +++ b/Runtime/UI/Nodes/Image/UIImageTypes/CustomUIImageType.cs.uid @@ -0,0 +1 @@ +uid://dd3mbku3iiltt diff --git a/Runtime/UI/UI.cs b/Runtime/UI/UI.cs index 0aa04a3..fa25409 100644 --- a/Runtime/UI/UI.cs +++ b/Runtime/UI/UI.cs @@ -613,7 +613,10 @@ namespace Rokojori } else { - return control.GetWindow().Size.X; + // return control.GetWindow().Size.X; + return control.GetViewport().GetVisibleRect().Size.X; + + } } @@ -625,7 +628,8 @@ namespace Rokojori } else { - return control.GetWindow().Size.Y; + // return control.GetWindow().Size.Y; + return control.GetViewport().GetVisibleRect().Size.Y; } } diff --git a/Runtime/VFX/ParticleVFX/ParticleVFXData.cs b/Runtime/VFX/ParticleVFX/ParticleVFXData.cs index 647b9ea..4336227 100644 --- a/Runtime/VFX/ParticleVFX/ParticleVFXData.cs +++ b/Runtime/VFX/ParticleVFX/ParticleVFXData.cs @@ -15,6 +15,11 @@ public abstract partial class ParticleVFXData:Resource return; } + if ( ! IsInstanceValid( particles ) ) + { + return; + } + if ( particles.Amount != preset.maxParticles ) { particles.Amount = preset.maxParticles;