└── addons └── imjp94.yafsm ├── assets ├── icons │ ├── stack_player_icon.png │ ├── state_machine_icon.png │ ├── state_machine_player_icon.png │ ├── remove-white-18dp.svg │ ├── arrow_right-white-18dp.svg │ ├── add-white-18dp.svg │ ├── subdirectory_arrow_right-white-18dp.svg │ ├── close-white-18dp.svg │ ├── compare_arrows-white-18dp.svg │ ├── stack_player_icon.png.import │ ├── state_machine_icon.png.import │ ├── state_machine_player_icon.png.import │ ├── add-white-18dp.svg.import │ ├── close-white-18dp.svg.import │ ├── remove-white-18dp.svg.import │ ├── arrow_right-white-18dp.svg.import │ ├── compare_arrows-white-18dp.svg.import │ └── subdirectory_arrow_right-white-18dp.svg.import └── fonts │ └── sans_serif.tres ├── plugin.cfg ├── src ├── debugger │ ├── StackItem.tscn │ ├── StackPlayerDebugger.tscn │ └── StackPlayerDebugger.gd ├── conditions │ ├── BooleanCondition.gd │ ├── IntegerCondition.gd │ ├── Condition.gd │ ├── StringCondition.gd │ ├── FloatCondition.gd │ └── ValueCondition.gd ├── states │ ├── State.gd │ └── StateMachine.gd ├── StackPlayer.gd ├── StateDirectory.gd ├── transitions │ └── Transition.gd └── StateMachinePlayer.gd ├── scenes ├── ContextMenu.tscn ├── state_nodes │ ├── StateInspector.gd │ ├── StateNode.gd │ └── StateNode.tscn ├── StateNodeContextMenu.tscn ├── condition_editors │ ├── BoolConditionEditor.gd │ ├── ConditionEditor.tscn │ ├── FloatConditionEditor.tscn │ ├── IntegerConditionEditor.tscn │ ├── StringConditionEditor.tscn │ ├── BoolConditionEditor.tscn │ ├── ValueConditionEditor.tscn │ ├── IntegerConditionEditor.gd │ ├── StringConditionEditor.gd │ ├── FloatConditionEditor.gd │ ├── ValueConditionEditor.gd │ └── ConditionEditor.gd ├── flowchart │ ├── FlowChartNode.gd │ ├── FlowChartNode.tscn │ ├── FlowChartGrid.gd │ ├── FlowChartLine.gd │ ├── FlowChartLayer.gd │ ├── FlowChartLine.tscn │ └── FlowChart.gd ├── transition_editors │ ├── TransitionInspector.gd │ ├── TransitionLine.tscn │ ├── TransitionLine.gd │ ├── TransitionEditor.gd │ └── TransitionEditor.tscn ├── PathViewer.gd ├── ParametersPanel.gd ├── StateMachineEditor.tscn ├── StateMachineEditorLayer.gd └── StateMachineEditor.gd ├── YAFSM.gd ├── README.md ├── scripts └── Utils.gd └── plugin.gd /addons/imjp94.yafsm/assets/icons/stack_player_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imjp94/gd-YAFSM/HEAD/addons/imjp94.yafsm/assets/icons/stack_player_icon.png -------------------------------------------------------------------------------- /addons/imjp94.yafsm/assets/icons/state_machine_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imjp94/gd-YAFSM/HEAD/addons/imjp94.yafsm/assets/icons/state_machine_icon.png -------------------------------------------------------------------------------- /addons/imjp94.yafsm/assets/icons/state_machine_player_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imjp94/gd-YAFSM/HEAD/addons/imjp94.yafsm/assets/icons/state_machine_player_icon.png -------------------------------------------------------------------------------- /addons/imjp94.yafsm/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="gd-YAFSM" 4 | description="Yet Another Finite State Machine" 5 | author="imjp94" 6 | version="0.6.3" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/assets/icons/remove-white-18dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/assets/fonts/sans_serif.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="SystemFont" format=3 uid="uid://dmcxm8gxsonbq"] 2 | 3 | [resource] 4 | font_names = PackedStringArray("Sans-Serif") 5 | multichannel_signed_distance_field = true 6 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/assets/icons/arrow_right-white-18dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/assets/icons/add-white-18dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/assets/icons/subdirectory_arrow_right-white-18dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/assets/icons/close-white-18dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/src/debugger/StackItem.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene format=3 uid="uid://b3b5ivtjmka6b"] 2 | 3 | [node name="StackItem" type="PanelContainer"] 4 | __meta__ = { 5 | "_edit_use_anchors_": false 6 | } 7 | 8 | [node name="Label" type="Label" parent="."] 9 | offset_right = 36.0 10 | offset_bottom = 26.0 11 | text = "Item" 12 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/assets/icons/compare_arrows-white-18dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/ContextMenu.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene format=3 uid="uid://cflltb00e10be"] 2 | 3 | [node name="ContextMenu" type="PopupMenu"] 4 | size = Vector2i(104, 100) 5 | visible = true 6 | item_count = 3 7 | item_0/text = "Add State" 8 | item_0/id = 0 9 | item_1/text = "Add Entry" 10 | item_1/id = 1 11 | item_2/text = "Add Exit" 12 | item_2/id = 2 13 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/state_nodes/StateInspector.gd: -------------------------------------------------------------------------------- 1 | extends EditorInspectorPlugin 2 | 3 | const State = preload("res://addons/imjp94.yafsm/src/states/State.gd") 4 | 5 | func _can_handle(object): 6 | return object is State 7 | 8 | func _parse_property(object, type, path, hint, hint_text, usage, wide) -> bool: 9 | return false 10 | # Hide all property 11 | return true 12 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/StateNodeContextMenu.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene format=3 uid="uid://ccv81pntbud75"] 2 | 3 | [node name="StateNodeContextMenu" type="PopupMenu"] 4 | size = Vector2i(154, 120) 5 | visible = true 6 | item_count = 5 7 | item_0/text = "Copy" 8 | item_0/id = 0 9 | item_1/text = "Duplicate" 10 | item_1/id = 1 11 | item_2/text = "Delete" 12 | item_2/id = 4 13 | item_3/text = "" 14 | item_3/id = 2 15 | item_3/separator = true 16 | item_4/text = "Convert to State" 17 | item_4/id = 3 18 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/src/conditions/BooleanCondition.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValueCondition 3 | class_name BooleanCondition 4 | 5 | @export var value: bool: 6 | set = set_value, 7 | get = get_value 8 | 9 | 10 | func set_value(v): 11 | if value != v: 12 | value = v 13 | emit_signal("value_changed", v) 14 | emit_signal("display_string_changed", display_string()) 15 | 16 | func get_value(): 17 | return value 18 | 19 | func compare(v): 20 | if typeof(v) != TYPE_BOOL: 21 | return false 22 | return super.compare(v) 23 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/src/conditions/IntegerCondition.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValueCondition 3 | class_name IntegerCondition 4 | 5 | 6 | @export var value: int: 7 | set = set_value, 8 | get = get_value 9 | 10 | 11 | func set_value(v): 12 | if value != v: 13 | value = v 14 | emit_signal("value_changed", v) 15 | emit_signal("display_string_changed", display_string()) 16 | 17 | func get_value(): 18 | return value 19 | 20 | func compare(v): 21 | if typeof(v) != TYPE_INT: 22 | return false 23 | return super.compare(v) 24 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/src/conditions/Condition.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Resource 3 | class_name Condition 4 | 5 | signal name_changed(old, new) 6 | signal display_string_changed(new) 7 | 8 | @export var name: = "": # Name of condition, unique to Transition 9 | set = set_name 10 | 11 | 12 | func _init(p_name=""): 13 | name = p_name 14 | 15 | func set_name(n): 16 | if name != n: 17 | var old = name 18 | name = n 19 | emit_signal("name_changed", old, n) 20 | emit_signal("display_string_changed", display_string()) 21 | 22 | func display_string(): 23 | return name 24 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/src/conditions/StringCondition.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValueCondition 3 | class_name StringCondition 4 | 5 | @export var value: String: 6 | set = set_value, 7 | get = get_value 8 | 9 | 10 | func set_value(v): 11 | if value != v: 12 | value = v 13 | emit_signal("value_changed", v) 14 | emit_signal("display_string_changed", display_string()) 15 | 16 | func get_value(): 17 | return value 18 | 19 | func get_value_string(): 20 | return "\"%s\"" % value 21 | 22 | func compare(v): 23 | if typeof(v) != TYPE_STRING: 24 | return false 25 | return super.compare(v) 26 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/src/conditions/FloatCondition.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValueCondition 3 | class_name FloatCondition 4 | 5 | @export var value: float: 6 | set = set_value, 7 | get = get_value 8 | 9 | 10 | func set_value(v): 11 | if not is_equal_approx(value, v): 12 | value = v 13 | emit_signal("value_changed", v) 14 | emit_signal("display_string_changed", display_string()) 15 | 16 | func get_value(): 17 | return value 18 | 19 | func get_value_string(): 20 | return str(snapped(value, 0.01)).pad_decimals(2) 21 | 22 | func compare(v): 23 | if typeof(v) != TYPE_FLOAT: 24 | return false 25 | return super.compare(v) 26 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/condition_editors/BoolConditionEditor.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "ValueConditionEditor.gd" 3 | 4 | @onready var boolean_value = $MarginContainer/BooleanValue 5 | 6 | func _ready(): 7 | super._ready() 8 | 9 | boolean_value.pressed.connect(_on_boolean_value_pressed) 10 | 11 | 12 | func _on_value_changed(new_value): 13 | if boolean_value.button_pressed != new_value: 14 | boolean_value.button_pressed = new_value 15 | 16 | func _on_boolean_value_pressed(): 17 | change_value_action(condition.value, boolean_value.button_pressed) 18 | 19 | func _on_condition_changed(new_condition): 20 | super._on_condition_changed(new_condition) 21 | if new_condition: 22 | boolean_value.button_pressed = new_condition.value 23 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/src/debugger/StackPlayerDebugger.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://addons/imjp94.yafsm/src/debugger/StackPlayerDebugger.gd" type="Script" id=1] 4 | 5 | [node name="StackPlayerDebugger" type="Control"] 6 | modulate = Color( 1, 1, 1, 0.498039 ) 7 | anchor_right = 1.0 8 | anchor_bottom = 1.0 9 | script = ExtResource( 1 ) 10 | __meta__ = { 11 | "_edit_use_anchors_": false 12 | } 13 | 14 | [node name="MarginContainer" type="MarginContainer" parent="."] 15 | anchor_right = 1.0 16 | anchor_bottom = 1.0 17 | mouse_filter = 2 18 | __meta__ = { 19 | "_edit_use_anchors_": false 20 | } 21 | 22 | [node name="Stack" type="VBoxContainer" parent="MarginContainer"] 23 | margin_top = 600.0 24 | margin_bottom = 600.0 25 | mouse_filter = 2 26 | size_flags_horizontal = 0 27 | size_flags_vertical = 8 28 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/condition_editors/ConditionEditor.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://cie8lb6ww58ck"] 2 | 3 | [ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/condition_editors/ConditionEditor.gd" id="1"] 4 | [ext_resource type="Texture2D" uid="uid://l78bjwo7shm" path="res://addons/imjp94.yafsm/assets/icons/close-white-18dp.svg" id="2"] 5 | 6 | [node name="ConditionEditor" type="HBoxContainer"] 7 | script = ExtResource("1") 8 | 9 | [node name="Name" type="LineEdit" parent="."] 10 | layout_mode = 2 11 | offset_right = 67.0 12 | offset_bottom = 31.0 13 | size_flags_horizontal = 3 14 | size_flags_vertical = 4 15 | text = "Param" 16 | 17 | [node name="Remove" type="Button" parent="."] 18 | layout_mode = 2 19 | offset_left = 71.0 20 | offset_right = 97.0 21 | offset_bottom = 31.0 22 | size_flags_horizontal = 9 23 | icon = ExtResource("2") 24 | flat = true 25 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/YAFSM.gd: -------------------------------------------------------------------------------- 1 | # Node 2 | const StackPlayer = preload("src/StackPlayer.gd") 3 | const StateMachinePlayer = preload("src/StateMachinePlayer.gd") 4 | 5 | # Reference 6 | const StateDirectory = preload("src/StateDirectory.gd") 7 | 8 | # Resources 9 | # States 10 | const State = preload("src/states/State.gd") 11 | const StateMachine = preload("src/states/StateMachine.gd") 12 | # Transitions 13 | const Transition = preload("src/transitions/Transition.gd") 14 | # Conditions 15 | const Condition = preload("src/conditions/Condition.gd") 16 | const ValueCondition = preload("src/conditions/ValueCondition.gd") 17 | const BooleanCondition = preload("src/conditions/BooleanCondition.gd") 18 | const IntegerCondition = preload("src/conditions/IntegerCondition.gd") 19 | const FloatCondition = preload("src/conditions/FloatCondition.gd") 20 | const StringCondition = preload("src/conditions/StringCondition.gd") 21 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/flowchart/FlowChartNode.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Container 3 | # Custom style normal, focus 4 | 5 | var selected: = false: 6 | set = set_selected 7 | 8 | 9 | func _init(): 10 | 11 | focus_mode = FOCUS_NONE # Let FlowChart has the focus to handle gui_input 12 | mouse_filter = MOUSE_FILTER_PASS 13 | 14 | func _draw(): 15 | if selected: 16 | draw_style_box(get_theme_stylebox("focus", "FlowChartNode"), Rect2(Vector2.ZERO, size)) 17 | else: 18 | draw_style_box(get_theme_stylebox("normal", "FlowChartNode"), Rect2(Vector2.ZERO, size)) 19 | 20 | func _notification(what): 21 | match what: 22 | NOTIFICATION_SORT_CHILDREN: 23 | for child in get_children(): 24 | if child is Control: 25 | fit_child_in_rect(child, Rect2(Vector2.ZERO, size)) 26 | 27 | func _get_minimum_size(): 28 | return Vector2(50, 50) 29 | 30 | func set_selected(v): 31 | if selected != v: 32 | selected = v 33 | queue_redraw() 34 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/assets/icons/stack_player_icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bcc8ni3mjf55j" 6 | path="res://.godot/imported/stack_player_icon.png-bf093c6193b73dc7a03c728b884edd0b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/imjp94.yafsm/assets/icons/stack_player_icon.png" 14 | dest_files=["res://.godot/imported/stack_player_icon.png-bf093c6193b73dc7a03c728b884edd0b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=false 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/assets/icons/state_machine_icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://quofx2t3tj1b" 6 | path="res://.godot/imported/state_machine_icon.png-9917b22df6299aea6994b92cacbcef16.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/imjp94.yafsm/assets/icons/state_machine_icon.png" 14 | dest_files=["res://.godot/imported/state_machine_icon.png-9917b22df6299aea6994b92cacbcef16.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=false 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/condition_editors/FloatConditionEditor.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://doq6lkdh20j15"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://blnscdhcxvpmk" path="res://addons/imjp94.yafsm/scenes/condition_editors/ValueConditionEditor.tscn" id="1"] 4 | [ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/condition_editors/FloatConditionEditor.gd" id="2"] 5 | 6 | [node name="ValueConditionEditor" instance=ExtResource("1")] 7 | script = ExtResource("2") 8 | 9 | [node name="Comparation" parent="." index="1"] 10 | layout_mode = 2 11 | 12 | [node name="MarginContainer" parent="." index="2"] 13 | layout_mode = 2 14 | offset_right = 169.0 15 | size_flags_horizontal = 3 16 | size_flags_vertical = 4 17 | 18 | [node name="FloatValue" type="LineEdit" parent="MarginContainer" index="0"] 19 | layout_mode = 2 20 | offset_right = 67.0 21 | offset_bottom = 31.0 22 | size_flags_horizontal = 3 23 | 24 | [node name="Remove" parent="." index="3"] 25 | offset_left = 173.0 26 | offset_right = 199.0 27 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/condition_editors/IntegerConditionEditor.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://d1ib30424prpf"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://blnscdhcxvpmk" path="res://addons/imjp94.yafsm/scenes/condition_editors/ValueConditionEditor.tscn" id="1"] 4 | [ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/condition_editors/IntegerConditionEditor.gd" id="2"] 5 | 6 | [node name="IntegerConditionEditor" instance=ExtResource("1")] 7 | script = ExtResource("2") 8 | 9 | [node name="Comparation" parent="." index="1"] 10 | layout_mode = 2 11 | 12 | [node name="MarginContainer" parent="." index="2"] 13 | layout_mode = 2 14 | offset_right = 169.0 15 | size_flags_horizontal = 3 16 | size_flags_vertical = 4 17 | 18 | [node name="IntegerValue" type="LineEdit" parent="MarginContainer" index="0"] 19 | layout_mode = 2 20 | offset_right = 67.0 21 | offset_bottom = 31.0 22 | size_flags_horizontal = 3 23 | 24 | [node name="Remove" parent="." index="3"] 25 | offset_left = 173.0 26 | offset_right = 199.0 27 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/src/states/State.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Resource 3 | class_name State 4 | 5 | signal name_changed(new_name) 6 | 7 | # Reserved state name for Entry/Exit 8 | const ENTRY_STATE = "Entry" 9 | const EXIT_STATE = "Exit" 10 | 11 | const META_GRAPH_OFFSET = "graph_offset" # Meta key for graph_offset 12 | 13 | @export var name: = "": # Name of state, unique within StateMachine 14 | set = set_name 15 | 16 | var graph_offset: # Position in FlowChart stored as meta, for editor only 17 | set = set_graph_offset, 18 | get = get_graph_offset 19 | 20 | 21 | func _init(p_name=""): 22 | name = p_name 23 | 24 | func is_entry(): 25 | return name == ENTRY_STATE 26 | 27 | func is_exit(): 28 | return name == EXIT_STATE 29 | 30 | func set_graph_offset(offset): 31 | set_meta(META_GRAPH_OFFSET, offset) 32 | 33 | func get_graph_offset(): 34 | return get_meta(META_GRAPH_OFFSET) if has_meta(META_GRAPH_OFFSET) else Vector2.ZERO 35 | 36 | func set_name(n): 37 | if name != n: 38 | name = n 39 | emit_signal("name_changed", name) 40 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/assets/icons/state_machine_player_icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://crcg0exl13kdd" 6 | path="res://.godot/imported/state_machine_player_icon.png-12d6c36cda302327e8c107292c578aa4.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/imjp94.yafsm/assets/icons/state_machine_player_icon.png" 14 | dest_files=["res://.godot/imported/state_machine_player_icon.png-12d6c36cda302327e8c107292c578aa4.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=false 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=0 35 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/assets/icons/add-white-18dp.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dg8cmn5ubq6r5" 6 | path="res://.godot/imported/add-white-18dp.svg-06b50d941748dbfd6e0203dec68494ea.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/imjp94.yafsm/assets/icons/add-white-18dp.svg" 14 | dest_files=["res://.godot/imported/add-white-18dp.svg-06b50d941748dbfd6e0203dec68494ea.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=false 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/assets/icons/close-white-18dp.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://l78bjwo7shm" 6 | path="res://.godot/imported/close-white-18dp.svg-3d0e2341eb99a6dc45a6aecef969301b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/imjp94.yafsm/assets/icons/close-white-18dp.svg" 14 | dest_files=["res://.godot/imported/close-white-18dp.svg-3d0e2341eb99a6dc45a6aecef969301b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=false 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/condition_editors/StringConditionEditor.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://qfw0snt5kss6"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://blnscdhcxvpmk" path="res://addons/imjp94.yafsm/scenes/condition_editors/ValueConditionEditor.tscn" id="1"] 4 | [ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/condition_editors/StringConditionEditor.gd" id="2"] 5 | 6 | [node name="StringConditionEditor" instance=ExtResource("1")] 7 | script = ExtResource("2") 8 | 9 | [node name="Comparation" parent="." index="1"] 10 | layout_mode = 2 11 | 12 | [node name="PopupMenu" parent="Comparation" index="0"] 13 | item_count = 2 14 | 15 | [node name="MarginContainer" parent="." index="2"] 16 | layout_mode = 2 17 | offset_right = 169.0 18 | size_flags_horizontal = 3 19 | size_flags_vertical = 4 20 | 21 | [node name="StringValue" type="LineEdit" parent="MarginContainer" index="0"] 22 | layout_mode = 2 23 | offset_right = 67.0 24 | offset_bottom = 31.0 25 | size_flags_horizontal = 3 26 | 27 | [node name="Remove" parent="." index="3"] 28 | offset_left = 173.0 29 | offset_right = 199.0 30 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/assets/icons/remove-white-18dp.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://p2md5n42lcqj" 6 | path="res://.godot/imported/remove-white-18dp.svg-984af3406d3d64ea0f778da7f0f5a4c3.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/imjp94.yafsm/assets/icons/remove-white-18dp.svg" 14 | dest_files=["res://.godot/imported/remove-white-18dp.svg-984af3406d3d64ea0f778da7f0f5a4c3.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=false 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/assets/icons/arrow_right-white-18dp.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://yw43hcwiudst" 6 | path="res://.godot/imported/arrow_right-white-18dp.svg-10d349447e9bd513637eade1f10225f0.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/imjp94.yafsm/assets/icons/arrow_right-white-18dp.svg" 14 | dest_files=["res://.godot/imported/arrow_right-white-18dp.svg-10d349447e9bd513637eade1f10225f0.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=false 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=4.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/assets/icons/compare_arrows-white-18dp.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cnkaa2ky1f4jq" 6 | path="res://.godot/imported/compare_arrows-white-18dp.svg-7313ec3b54f05c948521b16e0efaaeed.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/imjp94.yafsm/assets/icons/compare_arrows-white-18dp.svg" 14 | dest_files=["res://.godot/imported/compare_arrows-white-18dp.svg-7313ec3b54f05c948521b16e0efaaeed.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=false 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/transition_editors/TransitionInspector.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorInspectorPlugin 3 | const Transition = preload("res://addons/imjp94.yafsm/src/transitions/Transition.gd") 4 | 5 | const TransitionEditor = preload("res://addons/imjp94.yafsm/scenes/transition_editors/TransitionEditor.tscn") 6 | 7 | var undo_redo 8 | 9 | var transition_icon 10 | 11 | func _can_handle(object): 12 | return object is Transition 13 | 14 | func _parse_property(object, type, path, hint, hint_text, usage, wide) -> bool: 15 | match path: 16 | "from": 17 | return true 18 | "to": 19 | return true 20 | "conditions": 21 | var transition_editor = TransitionEditor.instantiate() # Will be freed by editor 22 | transition_editor.undo_redo = undo_redo 23 | add_custom_control(transition_editor) 24 | transition_editor.ready.connect(_on_transition_editor_tree_entered.bind(transition_editor, object)) 25 | return true 26 | "priority": 27 | return true 28 | return false 29 | 30 | func _on_transition_editor_tree_entered(editor, transition): 31 | editor.transition = transition 32 | if transition_icon: 33 | editor.title_icon.texture = transition_icon 34 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/transition_editors/TransitionLine.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://cwb2nrjai7fao"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://creoglbeckyhs" path="res://addons/imjp94.yafsm/scenes/flowchart/FlowChartLine.tscn" id="1"] 4 | [ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/transition_editors/TransitionLine.gd" id="2"] 5 | [ext_resource type="SystemFont" uid="uid://dmcxm8gxsonbq" path="res://addons/imjp94.yafsm/assets/fonts/sans_serif.tres" id="3_y6xyv"] 6 | 7 | [node name="TransitionLine" instance=ExtResource("1")] 8 | script = ExtResource("2") 9 | upright_angle_range = 0.0 10 | 11 | [node name="MarginContainer" type="MarginContainer" parent="." index="0"] 12 | layout_mode = 2 13 | mouse_filter = 2 14 | 15 | [node name="Label" type="Label" parent="MarginContainer" index="0"] 16 | visible = false 17 | layout_mode = 2 18 | size_flags_horizontal = 6 19 | size_flags_vertical = 6 20 | theme_override_fonts/font = ExtResource("3_y6xyv") 21 | text = "Transition" 22 | 23 | [node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer" index="1"] 24 | layout_mode = 2 25 | size_flags_horizontal = 4 26 | size_flags_vertical = 4 27 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/assets/icons/subdirectory_arrow_right-white-18dp.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://b2coah58shtq1" 6 | path="res://.godot/imported/subdirectory_arrow_right-white-18dp.svg-09b2961410e6b2c0e48e0cf1138c3548.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/imjp94.yafsm/assets/icons/subdirectory_arrow_right-white-18dp.svg" 14 | dest_files=["res://.godot/imported/subdirectory_arrow_right-white-18dp.svg-09b2961410e6b2c0e48e0cf1138c3548.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=false 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/flowchart/FlowChartNode.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://bar1eob74t82f"] 2 | 3 | [ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/flowchart/FlowChartNode.gd" id="1"] 4 | 5 | [sub_resource type="StyleBoxFlat" id="1"] 6 | bg_color = Color(0.164706, 0.164706, 0.164706, 1) 7 | border_width_left = 1 8 | border_width_top = 1 9 | border_width_right = 1 10 | border_width_bottom = 1 11 | border_color = Color(0.901961, 0.756863, 0.243137, 1) 12 | corner_radius_top_left = 4 13 | corner_radius_top_right = 4 14 | corner_radius_bottom_right = 4 15 | corner_radius_bottom_left = 4 16 | 17 | [sub_resource type="StyleBoxFlat" id="2"] 18 | bg_color = Color(0.164706, 0.164706, 0.164706, 1) 19 | border_width_left = 1 20 | border_width_top = 1 21 | border_width_right = 1 22 | border_width_bottom = 1 23 | corner_radius_top_left = 4 24 | corner_radius_top_right = 4 25 | corner_radius_bottom_right = 4 26 | corner_radius_bottom_left = 4 27 | 28 | [sub_resource type="Theme" id="3"] 29 | FlowChartNode/styles/focus = SubResource("1") 30 | FlowChartNode/styles/normal = SubResource("2") 31 | 32 | [node name="FlowChartNode" type="Container"] 33 | theme = SubResource("3") 34 | script = ExtResource("1") 35 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/condition_editors/BoolConditionEditor.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://bmpwx6h3ckekr"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://blnscdhcxvpmk" path="res://addons/imjp94.yafsm/scenes/condition_editors/ValueConditionEditor.tscn" id="1"] 4 | [ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/condition_editors/BoolConditionEditor.gd" id="2"] 5 | 6 | [node name="BoolConditionEditor" instance=ExtResource("1")] 7 | script = ExtResource("2") 8 | 9 | [node name="Name" parent="." index="0"] 10 | layout_mode = 2 11 | 12 | [node name="Comparation" parent="." index="1"] 13 | layout_mode = 2 14 | 15 | [node name="PopupMenu" parent="Comparation" index="0"] 16 | item_count = 2 17 | 18 | [node name="MarginContainer" parent="." index="2"] 19 | layout_mode = 2 20 | offset_top = 3.0 21 | offset_right = 146.0 22 | offset_bottom = 27.0 23 | size_flags_horizontal = 3 24 | size_flags_vertical = 4 25 | 26 | [node name="BooleanValue" type="CheckButton" parent="MarginContainer" index="0"] 27 | layout_mode = 2 28 | offset_right = 44.0 29 | offset_bottom = 24.0 30 | size_flags_horizontal = 6 31 | 32 | [node name="Remove" parent="." index="3"] 33 | layout_mode = 2 34 | offset_left = 150.0 35 | offset_right = 176.0 36 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/condition_editors/ValueConditionEditor.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://blnscdhcxvpmk"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://cie8lb6ww58ck" path="res://addons/imjp94.yafsm/scenes/condition_editors/ConditionEditor.tscn" id="1"] 4 | [ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/condition_editors/ValueConditionEditor.gd" id="2"] 5 | 6 | [node name="ValueConditionEditor" instance=ExtResource("1")] 7 | script = ExtResource("2") 8 | 9 | [node name="Comparation" type="Button" parent="." index="1"] 10 | layout_mode = 2 11 | offset_left = 71.0 12 | offset_right = 98.0 13 | offset_bottom = 31.0 14 | size_flags_horizontal = 5 15 | size_flags_vertical = 4 16 | text = "==" 17 | 18 | [node name="PopupMenu" type="PopupMenu" parent="Comparation" index="0"] 19 | item_count = 6 20 | item_0/text = "==" 21 | item_0/id = 0 22 | item_1/text = "!=" 23 | item_1/id = 1 24 | item_2/text = ">" 25 | item_2/id = 2 26 | item_3/text = "<" 27 | item_3/id = 3 28 | item_4/text = "≥" 29 | item_4/id = 4 30 | item_5/text = "≤" 31 | item_5/id = 5 32 | 33 | [node name="MarginContainer" type="MarginContainer" parent="." index="2"] 34 | layout_mode = 2 35 | offset_left = 102.0 36 | offset_right = 102.0 37 | offset_bottom = 31.0 38 | 39 | [node name="Remove" parent="." index="3"] 40 | offset_left = 106.0 41 | offset_right = 132.0 42 | tooltip_text = "Remove Condition" 43 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/condition_editors/IntegerConditionEditor.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "ValueConditionEditor.gd" 3 | 4 | @onready var integer_value = $MarginContainer/IntegerValue 5 | 6 | var _old_value = 0 7 | 8 | 9 | func _ready(): 10 | super._ready() 11 | 12 | integer_value.text_submitted.connect(_on_integer_value_text_submitted) 13 | integer_value.focus_entered.connect(_on_integer_value_focus_entered) 14 | integer_value.focus_exited.connect(_on_integer_value_focus_exited) 15 | set_process_input(false) 16 | 17 | func _input(event): 18 | super._input(event) 19 | 20 | if event is InputEventMouseButton: 21 | if event.pressed: 22 | if get_viewport().gui_get_focus_owner() == integer_value: 23 | var local_event = integer_value.make_input_local(event) 24 | if not integer_value.get_rect().has_point(local_event.position): 25 | integer_value.release_focus() 26 | 27 | func _on_value_changed(new_value): 28 | integer_value.text = str(new_value) 29 | 30 | func _on_integer_value_text_submitted(new_text): 31 | change_value_action(_old_value, int(new_text)) 32 | integer_value.release_focus() 33 | 34 | func _on_integer_value_focus_entered(): 35 | set_process_input(true) 36 | _old_value = int(integer_value.text) 37 | 38 | func _on_integer_value_focus_exited(): 39 | set_process_input(false) 40 | change_value_action(_old_value, int(integer_value.text)) 41 | 42 | func _on_condition_changed(new_condition): 43 | super._on_condition_changed(new_condition) 44 | if new_condition: 45 | integer_value.text = str(new_condition.value) 46 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/condition_editors/StringConditionEditor.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "res://addons/imjp94.yafsm/scenes/condition_editors/ValueConditionEditor.gd" 3 | 4 | 5 | @onready var string_value = $MarginContainer/StringValue 6 | 7 | var _old_value = 0 8 | 9 | 10 | func _ready(): 11 | super._ready() 12 | 13 | string_value.text_submitted.connect(_on_string_value_text_submitted) 14 | string_value.focus_entered.connect(_on_string_value_focus_entered) 15 | string_value.focus_exited.connect(_on_string_value_focus_exited) 16 | set_process_input(false) 17 | 18 | func _input(event): 19 | super._input(event) 20 | 21 | if event is InputEventMouseButton: 22 | if event.pressed: 23 | if get_viewport().gui_get_focus_owner() == string_value: 24 | var local_event = string_value.make_input_local(event) 25 | if not string_value.get_rect().has_point(local_event.position): 26 | string_value.release_focus() 27 | 28 | func _on_value_changed(new_value): 29 | string_value.text = new_value 30 | 31 | func _on_string_value_text_submitted(new_text): 32 | change_value_action(_old_value, new_text) 33 | string_value.release_focus() 34 | 35 | func _on_string_value_focus_entered(): 36 | set_process_input(true) 37 | _old_value = string_value.text 38 | 39 | func _on_string_value_focus_exited(): 40 | set_process_input(false) 41 | change_value_action(_old_value, string_value.text) 42 | 43 | func _on_condition_changed(new_condition): 44 | super._on_condition_changed(new_condition) 45 | if new_condition: 46 | string_value.text = new_condition.value 47 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/condition_editors/FloatConditionEditor.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "ValueConditionEditor.gd" 3 | 4 | @onready var float_value = $MarginContainer/FloatValue 5 | 6 | var _old_value = 0.0 7 | 8 | func _ready(): 9 | super._ready() 10 | 11 | float_value.text_submitted.connect(_on_float_value_text_submitted) 12 | float_value.focus_entered.connect(_on_float_value_focus_entered) 13 | float_value.focus_exited.connect(_on_float_value_focus_exited) 14 | set_process_input(false) 15 | 16 | func _input(event): 17 | super._input(event) 18 | 19 | if event is InputEventMouseButton: 20 | if event.pressed: 21 | if get_viewport().gui_get_focus_owner() == float_value: 22 | var local_event = float_value.make_input_local(event) 23 | if not float_value.get_rect().has_point(local_event.position): 24 | float_value.release_focus() 25 | 26 | func _on_value_changed(new_value): 27 | float_value.text = str(snapped(new_value, 0.01)).pad_decimals(2) 28 | 29 | func _on_float_value_text_submitted(new_text): 30 | change_value_action(_old_value, float(new_text)) 31 | float_value.release_focus() 32 | 33 | func _on_float_value_focus_entered(): 34 | set_process_input(true) 35 | _old_value = float(float_value.text) 36 | 37 | func _on_float_value_focus_exited(): 38 | set_process_input(false) 39 | change_value_action(_old_value, float(float_value.text)) 40 | 41 | func _on_condition_changed(new_condition): 42 | super._on_condition_changed(new_condition) 43 | if new_condition: 44 | float_value.text = str(snapped(new_condition.value, 0.01)).pad_decimals(2) 45 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/src/debugger/StackPlayerDebugger.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | const StackPlayer = preload("../StackPlayer.gd") 4 | const StackItem = preload("StackItem.tscn") 5 | 6 | @onready var Stack = $MarginContainer/Stack 7 | 8 | 9 | func _get_configuration_warning(): 10 | if not (get_parent() is StackPlayer): 11 | return "Debugger must be child of StackPlayer" 12 | return "" 13 | 14 | func _ready(): 15 | if Engine.is_editor_hint(): 16 | return 17 | 18 | get_parent().pushed.connect(_on_StackPlayer_pushed) 19 | get_parent().popped.connect(_on_StackPlayer_popped) 20 | sync_stack() 21 | 22 | # Override to handle custom object presentation 23 | func _on_set_label(label, obj): 24 | label.text = obj 25 | 26 | func _on_StackPlayer_pushed(to): 27 | var stack_item = StackItem.instantiate() 28 | _on_set_label(stack_item.get_node("Label"), to) 29 | Stack.add_child(stack_item) 30 | Stack.move_child(stack_item, 0) 31 | 32 | func _on_StackPlayer_popped(from): 33 | # Sync whole stack instead of just popping top item, as ResetEventTrigger passed to reset() may be varied 34 | sync_stack() 35 | 36 | func sync_stack(): 37 | var diff = Stack.get_child_count() - get_parent().stack.size() 38 | for i in abs(diff): 39 | if diff < 0: 40 | var stack_item = StackItem.instantiate() 41 | Stack.add_child(stack_item) 42 | else: 43 | var child = Stack.get_child(0) 44 | Stack.remove_child(child) 45 | child.queue_free() 46 | var stack = get_parent().stack 47 | for i in stack.size(): 48 | var obj = stack[stack.size()-1 - i] # Descending order, to list from bottom to top in VBoxContainer 49 | var child = Stack.get_child(i) 50 | _on_set_label(child.get_node("Label"), obj) 51 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/PathViewer.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends HBoxContainer 3 | 4 | signal dir_pressed(dir, index) 5 | 6 | 7 | func _init(): 8 | add_dir("root") 9 | 10 | # Select parent dir & return its path 11 | func back(): 12 | return select_dir(get_child(max(get_child_count()-1 - 1, 0)).name) 13 | 14 | # Select dir & return its path 15 | func select_dir(dir): 16 | for i in get_child_count(): 17 | var child = get_child(i) 18 | if child.name == dir: 19 | remove_dir_until(i) 20 | return get_dir_until(i) 21 | 22 | # Add directory button 23 | func add_dir(dir): 24 | var button = Button.new() 25 | button.name = dir 26 | button.flat = true 27 | button.text = dir 28 | add_child(button) 29 | button.pressed.connect(_on_button_pressed.bind(button)) 30 | return button 31 | 32 | # Remove directory until index(exclusive) 33 | func remove_dir_until(index): 34 | var to_remove = [] 35 | for i in get_child_count(): 36 | if index == get_child_count()-1 - i: 37 | break 38 | var child = get_child(get_child_count()-1 - i) 39 | to_remove.append(child) 40 | for n in to_remove: 41 | remove_child(n) 42 | n.queue_free() 43 | 44 | # Return current working directory 45 | func get_cwd(): 46 | return get_dir_until(get_child_count()-1) 47 | 48 | # Return path until index(inclusive) of directory 49 | func get_dir_until(index): 50 | var path = "" 51 | for i in get_child_count(): 52 | if i > index: 53 | break 54 | var child = get_child(i) 55 | if i == 0: 56 | path = "root" 57 | else: 58 | path = str(path, "/", child.text) 59 | return path 60 | 61 | func _on_button_pressed(button): 62 | var index = button.get_index() 63 | var dir = button.name 64 | emit_signal("dir_pressed", dir, index) 65 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/ParametersPanel.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends MarginContainer 3 | 4 | 5 | @onready var grid = $PanelContainer/MarginContainer/VBoxContainer/GridContainer 6 | @onready var button = $PanelContainer/MarginContainer/VBoxContainer/MarginContainer/Button 7 | 8 | 9 | func _ready(): 10 | button.pressed.connect(_on_button_pressed) 11 | 12 | func update_params(params, local_params): 13 | # Remove erased parameters from param panel 14 | for param in grid.get_children(): 15 | if not (param.name in params): 16 | remove_param(param.name) 17 | for param in params: 18 | var value = params[param] 19 | if value == null: # Ignore trigger 20 | continue 21 | set_param(param, str(value)) 22 | 23 | # Remove erased local parameters from param panel 24 | for param in grid.get_children(): 25 | if not (param.name in local_params) and not (param.name in params): 26 | remove_param(param.name) 27 | for param in local_params: 28 | var nested_params = local_params[param] 29 | for nested_param in nested_params: 30 | var value = nested_params[nested_param] 31 | if value == null: # Ignore trigger 32 | continue 33 | set_param(str(param, "/", nested_param), str(value)) 34 | 35 | func set_param(param, value): 36 | var label = grid.get_node_or_null(NodePath(param)) 37 | if not label: 38 | label = Label.new() 39 | label.name = param 40 | grid.add_child(label) 41 | 42 | label.text = "%s = %s" % [param, value] 43 | 44 | func remove_param(param): 45 | var label = grid.get_node_or_null(NodePath(param)) 46 | if label: 47 | grid.remove_child(label) 48 | label.queue_free() 49 | set_anchors_preset(PRESET_BOTTOM_RIGHT) 50 | 51 | func clear_params(): 52 | for child in grid.get_children(): 53 | grid.remove_child(child) 54 | child.queue_free() 55 | 56 | func _on_button_pressed(): 57 | grid.visible = !grid.visible 58 | if grid.visible: 59 | button.text = "Hide params" 60 | else: 61 | button.text = "Show params" 62 | 63 | set_anchors_preset(PRESET_BOTTOM_RIGHT) 64 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/condition_editors/ValueConditionEditor.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "ConditionEditor.gd" 3 | const Utils = preload("../../scripts/Utils.gd") 4 | const Comparation = preload("../../src/conditions/ValueCondition.gd").Comparation 5 | 6 | @onready var comparation_button = $Comparation 7 | @onready var comparation_popup_menu = $Comparation/PopupMenu 8 | 9 | 10 | func _ready(): 11 | super._ready() 12 | 13 | comparation_button.pressed.connect(_on_comparation_button_pressed) 14 | comparation_popup_menu.id_pressed.connect(_on_comparation_popup_menu_id_pressed) 15 | 16 | func _on_comparation_button_pressed(): 17 | Utils.popup_on_target(comparation_popup_menu, comparation_button) 18 | 19 | func _on_comparation_popup_menu_id_pressed(id): 20 | change_comparation_action(id) 21 | 22 | func _on_condition_changed(new_condition): 23 | super._on_condition_changed(new_condition) 24 | if new_condition: 25 | comparation_button.text = comparation_popup_menu.get_item_text(new_condition.comparation) 26 | 27 | func _on_value_changed(new_value): 28 | pass 29 | 30 | func change_comparation(id): 31 | if id > Comparation.size() - 1: 32 | push_error("Unexpected id(%d) from PopupMenu" % id) 33 | return 34 | condition.comparation = id 35 | comparation_button.text = comparation_popup_menu.get_item_text(id) 36 | 37 | func change_comparation_action(id): 38 | var from = condition.comparation 39 | var to = id 40 | 41 | undo_redo.create_action("Change Condition Comparation") 42 | undo_redo.add_do_method(self, "change_comparation", to) 43 | undo_redo.add_undo_method(self, "change_comparation", from) 44 | undo_redo.commit_action() 45 | 46 | func set_value(v): 47 | if condition.value != v: 48 | condition.value = v 49 | _on_value_changed(v) 50 | 51 | func change_value_action(from, to): 52 | if from == to: 53 | return 54 | undo_redo.create_action("Change Condition Value") 55 | undo_redo.add_do_method(self, "set_value", to) 56 | undo_redo.add_undo_method(self, "set_value", from) 57 | undo_redo.commit_action() 58 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/src/conditions/ValueCondition.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Condition 3 | class_name ValueCondition 4 | 5 | signal comparation_changed(new_comparation) # Comparation hanged 6 | signal value_changed(new_value) # Value changed 7 | 8 | # Enum to define how to compare value 9 | enum Comparation { 10 | EQUAL, 11 | INEQUAL, 12 | GREATER, 13 | LESSER, 14 | GREATER_OR_EQUAL, 15 | LESSER_OR_EQUAL 16 | } 17 | # Comparation symbols arranged in order as enum Comparation 18 | const COMPARATION_SYMBOLS = [ 19 | "==", 20 | "!=", 21 | ">", 22 | "<", 23 | "≥", 24 | "≤" 25 | ] 26 | 27 | @export var comparation: Comparation = Comparation.EQUAL: 28 | set = set_comparation 29 | 30 | func _init(p_name="", p_comparation=Comparation.EQUAL): 31 | super._init(p_name) 32 | comparation = p_comparation 33 | 34 | func set_comparation(c): 35 | if comparation != c: 36 | comparation = c 37 | emit_signal("comparation_changed", c) 38 | emit_signal("display_string_changed", display_string()) 39 | 40 | # To be overrided by child class and emit value_changed signal 41 | func set_value(v): 42 | pass 43 | 44 | # To be overrided by child class, as it is impossible to export(Variant) 45 | func get_value(): 46 | pass 47 | 48 | # To be used in _to_string() 49 | func get_value_string(): 50 | return get_value() 51 | 52 | # Compare value against this condition, return true if succeeded 53 | func compare(v): 54 | if v == null: 55 | return false 56 | 57 | match comparation: 58 | Comparation.EQUAL: 59 | return v == get_value() 60 | Comparation.INEQUAL: 61 | return v != get_value() 62 | Comparation.GREATER: 63 | return v > get_value() 64 | Comparation.LESSER: 65 | return v < get_value() 66 | Comparation.GREATER_OR_EQUAL: 67 | return v >= get_value() 68 | Comparation.LESSER_OR_EQUAL: 69 | return v <= get_value() 70 | 71 | # Return human readable display string, for example, "condition_name == True" 72 | func display_string(): 73 | return "%s %s %s" % [super.display_string(), COMPARATION_SYMBOLS[comparation], get_value_string()] 74 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/flowchart/FlowChartGrid.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | var flowchart 4 | 5 | func _ready(): 6 | flowchart = get_parent().get_parent() 7 | queue_redraw() 8 | 9 | # Original Draw in FlowChart.gd inspired by: 10 | # https://github.com/godotengine/godot/blob/6019dab0b45e1291e556e6d9e01b625b5076cc3c/scene/gui/graph_edit.cpp#L442 11 | func _draw(): 12 | 13 | self.position = flowchart.position 14 | # Extents of the grid. 15 | self.size = flowchart.size*100 # good with min_zoom = 0.5 e max_zoom = 2.0 16 | 17 | var zoom = flowchart.zoom 18 | var snap = flowchart.snap 19 | 20 | # Origin of the grid. 21 | var offset = -Vector2(1, 1)*10000 # good with min_zoom = 0.5 e max_zoom = 2.0 22 | 23 | var corrected_size = size/zoom 24 | 25 | var from = (offset / snap).floor() 26 | var l = (corrected_size / snap).floor() + Vector2(1, 1) 27 | 28 | var grid_minor = flowchart.grid_minor_color 29 | var grid_major = flowchart.grid_major_color 30 | 31 | var multi_line_vector_array: PackedVector2Array = PackedVector2Array() 32 | var multi_line_color_array: PackedColorArray = PackedColorArray () 33 | 34 | # for (int i = from.x; i < from.x + len.x; i++) { 35 | for i in range(from.x, from.x + l.x): 36 | var color 37 | 38 | if (int(abs(i)) % 10 == 0): 39 | color = grid_major 40 | else: 41 | color = grid_minor 42 | 43 | var base_ofs = i * snap 44 | 45 | multi_line_vector_array.append(Vector2(base_ofs, offset.y)) 46 | multi_line_vector_array.append(Vector2(base_ofs, corrected_size.y)) 47 | multi_line_color_array.append(color) 48 | 49 | # for (int i = from.y; i < from.y + len.y; i++) { 50 | for i in range(from.y, from.y + l.y): 51 | var color 52 | 53 | if (int(abs(i)) % 10 == 0): 54 | color = grid_major 55 | else: 56 | color = grid_minor 57 | 58 | var base_ofs = i * snap 59 | 60 | multi_line_vector_array.append(Vector2(offset.x, base_ofs)) 61 | multi_line_vector_array.append(Vector2(corrected_size.x, base_ofs)) 62 | multi_line_color_array.append(color) 63 | 64 | draw_multiline_colors(multi_line_vector_array, multi_line_color_array, -1) -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/condition_editors/ConditionEditor.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends HBoxContainer 3 | 4 | @onready var name_edit = $Name 5 | @onready var remove = $Remove 6 | 7 | var undo_redo 8 | 9 | var condition: 10 | set = set_condition 11 | 12 | 13 | func _ready(): 14 | name_edit.text_submitted.connect(_on_name_edit_text_submitted) 15 | name_edit.focus_entered.connect(_on_name_edit_focus_entered) 16 | name_edit.focus_exited.connect(_on_name_edit_focus_exited) 17 | name_edit.text_changed.connect(_on_name_edit_text_changed) 18 | set_process_input(false) 19 | 20 | func _input(event): 21 | if event is InputEventMouseButton: 22 | if event.pressed: 23 | if get_viewport().gui_get_focus_owner() == name_edit: 24 | var local_event = name_edit.make_input_local(event) 25 | if not name_edit.get_rect().has_point(local_event.position): 26 | name_edit.release_focus() 27 | 28 | func _on_name_edit_text_changed(new_text): 29 | # name_edit.release_focus() 30 | if condition.name == new_text: # Avoid infinite loop 31 | return 32 | 33 | rename_edit_action(new_text) 34 | 35 | func _on_name_edit_focus_entered(): 36 | set_process_input(true) 37 | 38 | func _on_name_edit_focus_exited(): 39 | set_process_input(false) 40 | if condition.name == name_edit.text: 41 | return 42 | 43 | rename_edit_action(name_edit.text) 44 | 45 | func _on_name_edit_text_submitted(new_text): 46 | name_edit.tooltip_text = new_text 47 | 48 | func change_name_edit(from, to): 49 | var transition = get_parent().get_parent().get_parent().transition # TODO: Better way to get Transition object 50 | if transition.change_condition_name(from, to): 51 | if name_edit.text != to: # Manually update name_edit.text, in case called from undo_redo 52 | name_edit.text = to 53 | else: 54 | name_edit.text = from 55 | push_warning("Change Condition name_edit from (%s) to (%s) failed, name_edit existed" % [from, to]) 56 | 57 | func rename_edit_action(new_name_edit): 58 | var old_name_edit = condition.name 59 | undo_redo.create_action("Rename_edit Condition") 60 | undo_redo.add_do_method(self, "change_name_edit", old_name_edit, new_name_edit) 61 | undo_redo.add_undo_method(self, "change_name_edit", new_name_edit, old_name_edit) 62 | undo_redo.commit_action() 63 | 64 | func _on_condition_changed(new_condition): 65 | if new_condition: 66 | name_edit.text = new_condition.name 67 | name_edit.tooltip_text = name_edit.text 68 | 69 | func set_condition(c): 70 | if condition != c: 71 | condition = c 72 | _on_condition_changed(c) 73 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/src/StackPlayer.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name StackPlayer extends Node 3 | 4 | signal pushed(to) # When item pushed to stack 5 | signal popped(from) # When item popped from stack 6 | 7 | # Enum to specify how reseting state stack should trigger event(transit, push, pop etc.) 8 | enum ResetEventTrigger { 9 | NONE = -1, # No event 10 | ALL = 0, # All removed state will emit event 11 | LAST_TO_DEST = 1 # Only last state and destination will emit event 12 | } 13 | 14 | var current: # Current item on top of stack 15 | get = get_current 16 | var stack: 17 | set = _set_stack, 18 | get = _get_stack 19 | 20 | var _stack 21 | 22 | 23 | func _init(): 24 | _stack = [] 25 | 26 | # Push an item to the top of stack 27 | func push(to): 28 | var from = get_current() 29 | _stack.push_back(to) 30 | _on_pushed(from, to) 31 | emit_signal("pushed", to) 32 | 33 | # Remove the current item on top of stack 34 | func pop(): 35 | var to = get_previous() 36 | var from = _stack.pop_back() 37 | _on_popped(from, to) 38 | emit_signal("popped", from) 39 | 40 | # Called when item pushed 41 | func _on_pushed(from, to): 42 | pass 43 | 44 | # Called when item popped 45 | func _on_popped(from, to): 46 | pass 47 | 48 | # Reset stack to given index, -1 to clear all item by default 49 | # Use ResetEventTrigger to define how _on_popped should be called 50 | func reset(to=-1, event=ResetEventTrigger.ALL): 51 | assert(to > -2 and to < _stack.size(), "Reset to index out of bounds") 52 | var last_index = _stack.size() - 1 53 | var first_state = "" 54 | var num_to_pop = last_index - to 55 | 56 | if num_to_pop > 0: 57 | for i in range(num_to_pop): 58 | first_state = get_current() if i == 0 else first_state 59 | match event: 60 | ResetEventTrigger.LAST_TO_DEST: 61 | _stack.pop_back() 62 | if i == num_to_pop - 1: 63 | _stack.push_back(first_state) 64 | pop() 65 | ResetEventTrigger.ALL: 66 | pop() 67 | _: 68 | _stack.pop_back() 69 | elif num_to_pop == 0: 70 | match event: 71 | ResetEventTrigger.NONE: 72 | _stack.pop_back() 73 | _: 74 | pop() 75 | 76 | func _set_stack(val): 77 | push_warning("Attempting to edit read-only state stack directly. " \ 78 | + "Control state machine from setting parameters or call update() instead") 79 | 80 | # Get duplicate of the stack being played 81 | func _get_stack(): 82 | return _stack.duplicate() 83 | 84 | func get_current(): 85 | return _stack.back() if not _stack.is_empty() else null 86 | 87 | func get_previous(): 88 | return _stack[_stack.size() - 2] if _stack.size() > 1 else null 89 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/src/StateDirectory.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends RefCounted 3 | 4 | const State = preload("states/State.gd") 5 | 6 | var path 7 | var current: 8 | get = get_current 9 | var base: 10 | get = get_base 11 | var end: 12 | get = get_end 13 | 14 | var _current_index = 0 15 | var _dirs = [""] # Empty string equals to root 16 | 17 | 18 | func _init(p): 19 | path = p 20 | _dirs += Array(p.split("/")) 21 | 22 | # Move to next level and return state if exists, else null 23 | func next(): 24 | if has_next(): 25 | _current_index += 1 26 | return get_current_end() 27 | 28 | return null 29 | 30 | # Move to previous level and return state if exists, else null 31 | func back(): 32 | if has_back(): 33 | _current_index -= 1 34 | return get_current_end() 35 | 36 | return null 37 | 38 | # Move to specified index and return state 39 | func goto(index): 40 | assert(index > -1 and index < _dirs.size()) 41 | _current_index = index 42 | return get_current_end() 43 | 44 | # Check if directory has next level 45 | func has_next(): 46 | return _current_index < _dirs.size() - 1 47 | 48 | # Check if directory has previous level 49 | func has_back(): 50 | return _current_index > 0 51 | 52 | # Get current full path 53 | func get_current(): 54 | # In Godot 4.x the end parameter of Array.slice() is EXCLUSIVE! 55 | # https://docs.godotengine.org/en/latest/classes/class_array.html#class-array-method-slice 56 | var packed_string_array: PackedStringArray = PackedStringArray(_dirs.slice(get_base_index(), _current_index+1)) 57 | return "/".join(packed_string_array) 58 | 59 | # Get current end state name of path 60 | func get_current_end(): 61 | var current_path = get_current() 62 | return current_path.right(current_path.length()-1 - current_path.rfind("/")) 63 | 64 | # Get index of base state 65 | func get_base_index(): 66 | return 1 # Root(empty string) at index 0, base at index 1 67 | 68 | # Get level index of end state 69 | func get_end_index(): 70 | return _dirs.size() - 1 71 | 72 | # Get base state name 73 | func get_base(): 74 | return _dirs[get_base_index()] 75 | 76 | # Get end state name 77 | func get_end(): 78 | return _dirs[get_end_index()] 79 | 80 | # Get arrays of directories 81 | func get_dirs(): 82 | return _dirs.duplicate() 83 | 84 | # Check if it is Entry state 85 | func is_entry(): 86 | return get_end() == State.ENTRY_STATE 87 | 88 | # Check if it is Exit state 89 | func is_exit(): 90 | return get_end() == State.EXIT_STATE 91 | 92 | # Check if it is nested. ("Base" is not nested, "Base/NextState" is nested) 93 | func is_nested(): 94 | return _dirs.size() > 2 # Root(empty string) & base taken 2 place 95 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/state_nodes/StateNode.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "res://addons/imjp94.yafsm/scenes/flowchart/FlowChartNode.gd" 3 | const State = preload("../../src/states/State.gd") 4 | const StateMachine = preload("../../src/states/StateMachine.gd") 5 | 6 | signal name_edit_entered(new_name) # Emits when focused exit or Enter pressed 7 | 8 | @onready var name_edit = $MarginContainer/NameEdit 9 | 10 | var undo_redo 11 | 12 | var state: 13 | set = set_state 14 | 15 | 16 | func _init(): 17 | super._init() 18 | 19 | set_state(State.new()) 20 | 21 | func _ready(): 22 | name_edit.focus_exited.connect(_on_NameEdit_focus_exited) 23 | name_edit.text_submitted.connect(_on_NameEdit_text_submitted) 24 | set_process_input(false) # _input only required when name_edit enabled to check mouse click outside 25 | 26 | func _draw(): 27 | if state is StateMachine: 28 | if selected: 29 | draw_style_box(get_theme_stylebox("nested_focus", "StateNode"), Rect2(Vector2.ZERO, size)) 30 | else: 31 | draw_style_box(get_theme_stylebox("nested_normal", "StateNode"), Rect2(Vector2.ZERO, size)) 32 | else: 33 | super._draw() 34 | 35 | func _input(event): 36 | if event is InputEventMouseButton: 37 | if event.pressed: 38 | # Detect click outside rect 39 | if get_viewport().gui_get_focus_owner() == name_edit: 40 | var local_event = make_input_local(event) 41 | if not name_edit.get_rect().has_point(local_event.position): 42 | name_edit.release_focus() 43 | 44 | func enable_name_edit(v): 45 | if v: 46 | set_process_input(true) 47 | name_edit.editable = true 48 | name_edit.selecting_enabled = true 49 | name_edit.mouse_filter = MOUSE_FILTER_PASS 50 | mouse_default_cursor_shape = CURSOR_IBEAM 51 | name_edit.grab_focus() 52 | else: 53 | set_process_input(false) 54 | name_edit.editable = false 55 | name_edit.selecting_enabled = false 56 | name_edit.mouse_filter = MOUSE_FILTER_IGNORE 57 | mouse_default_cursor_shape = CURSOR_ARROW 58 | name_edit.release_focus() 59 | 60 | func _on_state_name_changed(new_name): 61 | name_edit.text = new_name 62 | size.x = 0 # Force reset horizontal size 63 | 64 | func _on_state_changed(new_state): 65 | if state: 66 | state.name_changed.connect(_on_state_name_changed) 67 | if name_edit: 68 | name_edit.text = state.name 69 | 70 | func _on_NameEdit_focus_exited(): 71 | enable_name_edit(false) 72 | name_edit.deselect() 73 | emit_signal("name_edit_entered", name_edit.text) 74 | 75 | func _on_NameEdit_text_submitted(new_text): 76 | enable_name_edit(false) 77 | emit_signal("name_edit_entered", new_text) 78 | 79 | func set_state(s): 80 | if state != s: 81 | state = s 82 | _on_state_changed(s) 83 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/flowchart/FlowChartLine.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Container 3 | # Custom style normal, focus, arrow 4 | 5 | var selected: = false: 6 | set = set_selected 7 | 8 | 9 | func _init(): 10 | 11 | focus_mode = FOCUS_CLICK 12 | mouse_filter = MOUSE_FILTER_IGNORE 13 | 14 | func _draw(): 15 | pivot_at_line_start() 16 | var from = Vector2.ZERO 17 | from.y += size.y / 2.0 18 | var to = size 19 | to.y -= size.y / 2.0 20 | var arrow = get_theme_icon("arrow", "FlowChartLine") 21 | var tint = Color.WHITE 22 | if selected: 23 | tint = get_theme_stylebox("focus", "FlowChartLine").shadow_color 24 | draw_style_box(get_theme_stylebox("focus", "FlowChartLine"), Rect2(Vector2.ZERO, size)) 25 | else: 26 | draw_style_box(get_theme_stylebox("normal", "FlowChartLine"), Rect2(Vector2.ZERO, size)) 27 | 28 | 29 | draw_texture(arrow, Vector2.ZERO - arrow.get_size() / 2 + size / 2, tint) 30 | 31 | func _get_minimum_size(): 32 | return Vector2(0, 5) 33 | 34 | func pivot_at_line_start(): 35 | pivot_offset.x = 0 36 | pivot_offset.y = size.y / 2.0 37 | 38 | func join(from, to, offset=Vector2.ZERO, clip_rects=[]): 39 | # Offset along perpendicular direction 40 | var perp_dir = from.direction_to(to).rotated(deg_to_rad(90.0)).normalized() 41 | from -= perp_dir * offset 42 | to -= perp_dir * offset 43 | 44 | var dist = from.distance_to(to) 45 | var dir = from.direction_to(to) 46 | var center = from + dir * dist / 2 47 | 48 | # Clip line with provided Rect2 array 49 | var clipped = [[from, to]] 50 | var line_from = from 51 | var line_to = to 52 | for clip_rect in clip_rects: 53 | if clipped.size() == 0: 54 | break 55 | 56 | line_from = clipped[0][0] 57 | line_to = clipped[0][1] 58 | clipped = Geometry2D.clip_polyline_with_polygon( 59 | [line_from, line_to], 60 | [clip_rect.position, Vector2(clip_rect.position.x, clip_rect.end.y), 61 | clip_rect.end, Vector2(clip_rect.end.x, clip_rect.position.y)] 62 | ) 63 | 64 | if clipped.size() > 0: 65 | from = clipped[0][0] 66 | to = clipped[0][1] 67 | else: # Line is totally overlapped 68 | from = center 69 | to = center + dir * 0.1 70 | 71 | # Extends line by 2px to minimise ugly seam 72 | from -= dir * 2.0 73 | to += dir * 2.0 74 | 75 | size.x = to.distance_to(from) 76 | # size.y equals to the thickness of line 77 | position = from 78 | position.y -= size.y / 2.0 79 | rotation = Vector2.RIGHT.angle_to(dir) 80 | pivot_at_line_start() 81 | 82 | func set_selected(v): 83 | if selected != v: 84 | selected = v 85 | queue_redraw() 86 | 87 | func get_from_pos(): 88 | return get_transform() * (position) 89 | 90 | func get_to_pos(): 91 | return get_transform() * (position + size) 92 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/state_nodes/StateNode.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=8 format=3 uid="uid://l3mqbqjwjkc3"] 2 | 3 | [ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/state_nodes/StateNode.gd" id="2"] 4 | [ext_resource type="SystemFont" uid="uid://dmcxm8gxsonbq" path="res://addons/imjp94.yafsm/assets/fonts/sans_serif.tres" id="2_352m3"] 5 | 6 | [sub_resource type="StyleBoxFlat" id="1"] 7 | bg_color = Color(0.164706, 0.164706, 0.164706, 1) 8 | border_width_left = 1 9 | border_width_top = 1 10 | border_width_right = 1 11 | border_width_bottom = 1 12 | border_color = Color(0.44, 0.73, 0.98, 1) 13 | corner_radius_top_left = 4 14 | corner_radius_top_right = 4 15 | corner_radius_bottom_right = 4 16 | corner_radius_bottom_left = 4 17 | 18 | [sub_resource type="StyleBoxFlat" id="2"] 19 | bg_color = Color(0.164706, 0.164706, 0.164706, 1) 20 | border_width_left = 1 21 | border_width_top = 1 22 | border_width_right = 1 23 | border_width_bottom = 1 24 | corner_radius_top_left = 4 25 | corner_radius_top_right = 4 26 | corner_radius_bottom_right = 4 27 | corner_radius_bottom_left = 4 28 | corner_detail = 2 29 | 30 | [sub_resource type="StyleBoxFlat" id="3"] 31 | bg_color = Color(0.164706, 0.164706, 0.164706, 1) 32 | border_width_left = 3 33 | border_width_top = 3 34 | border_width_right = 3 35 | border_width_bottom = 3 36 | border_color = Color(0.960784, 0.772549, 0.333333, 1) 37 | shadow_size = 2 38 | 39 | [sub_resource type="StyleBoxFlat" id="4"] 40 | bg_color = Color(0.164706, 0.164706, 0.164706, 1) 41 | border_width_left = 3 42 | border_width_top = 3 43 | border_width_right = 3 44 | border_width_bottom = 3 45 | shadow_size = 2 46 | 47 | [sub_resource type="Theme" id="5"] 48 | FlowChartNode/styles/focus = SubResource("1") 49 | FlowChartNode/styles/normal = SubResource("2") 50 | StateNode/styles/nested_focus = SubResource("3") 51 | StateNode/styles/nested_normal = SubResource("4") 52 | 53 | [node name="StateNode" type="HBoxContainer"] 54 | grow_horizontal = 2 55 | grow_vertical = 2 56 | theme = SubResource("5") 57 | script = ExtResource("2") 58 | 59 | [node name="MarginContainer" type="MarginContainer" parent="."] 60 | layout_mode = 2 61 | mouse_filter = 2 62 | theme_override_constants/margin_left = 5 63 | theme_override_constants/margin_top = 5 64 | theme_override_constants/margin_right = 5 65 | theme_override_constants/margin_bottom = 5 66 | 67 | [node name="NameEdit" type="LineEdit" parent="MarginContainer"] 68 | layout_mode = 2 69 | size_flags_horizontal = 4 70 | size_flags_vertical = 4 71 | mouse_filter = 2 72 | mouse_default_cursor_shape = 0 73 | theme_override_fonts/font = ExtResource("2_352m3") 74 | text = "State" 75 | alignment = 1 76 | editable = false 77 | expand_to_text_length = true 78 | selecting_enabled = false 79 | caret_blink = true 80 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/src/transitions/Transition.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Resource 3 | class_name Transition 4 | 5 | signal condition_added(condition) 6 | signal condition_removed(condition) 7 | 8 | @export var from: String # Name of state transiting from 9 | @export var to: String # Name of state transiting to 10 | @export var conditions: Dictionary: # Conditions to transit successfuly, keyed by Condition.name 11 | set = set_conditions, 12 | get = get_conditions 13 | @export var priority: = 0 # Higher the number, higher the priority 14 | 15 | var _conditions 16 | 17 | 18 | func _init(p_from="", p_to="", p_conditions={}): 19 | from = p_from 20 | to = p_to 21 | _conditions = p_conditions 22 | 23 | # Attempt to transit with parameters given, return name of next state if succeeded else null 24 | func transit(params={}, local_params={}): 25 | var can_transit = _conditions.size() > 0 26 | for condition in _conditions.values(): 27 | var has_param = params.has(condition.name) 28 | var has_local_param = local_params.has(condition.name) 29 | if has_param or has_local_param: 30 | # local_params > params 31 | var value = local_params.get(condition.name) if has_local_param else params.get(condition.name) 32 | if value == null: # null value is treated as trigger 33 | can_transit = can_transit and true 34 | else: 35 | if "value" in condition: 36 | can_transit = can_transit and condition.compare(value) 37 | else: 38 | can_transit = false 39 | if can_transit or _conditions.size() == 0: 40 | return to 41 | return null 42 | 43 | # Add condition, return true if succeeded 44 | func add_condition(condition): 45 | if condition.name in _conditions: 46 | return false 47 | 48 | _conditions[condition.name] = condition 49 | emit_signal("condition_added", condition) 50 | return true 51 | 52 | # Remove condition by name of condition 53 | func remove_condition(name): 54 | var condition = _conditions.get(name) 55 | if condition: 56 | _conditions.erase(name) 57 | emit_signal("condition_removed", condition) 58 | return true 59 | return false 60 | 61 | # Change condition name, return true if succeeded 62 | func change_condition_name(from, to): 63 | if not (from in _conditions) or to in _conditions: 64 | return false 65 | 66 | var condition = _conditions[from] 67 | condition.name = to 68 | _conditions.erase(from) 69 | _conditions[to] = condition 70 | return true 71 | 72 | func get_unique_name(name): 73 | var new_name = name 74 | var i = 1 75 | while new_name in _conditions: 76 | new_name = name + str(i) 77 | i += 1 78 | return new_name 79 | 80 | func equals(obj): 81 | if obj == null: 82 | return false 83 | if not ("from" in obj and "to" in obj): 84 | return false 85 | 86 | return from == obj.from and to == obj.to 87 | 88 | # Get duplicate of conditions dictionary 89 | func get_conditions(): 90 | return _conditions.duplicate() 91 | 92 | func set_conditions(val): 93 | _conditions = val 94 | 95 | static func sort(a, b): 96 | if a.priority > b.priority: 97 | return true 98 | return false 99 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/StateMachineEditor.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://bp2f3rs2sgn8g"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://ccv81pntbud75" path="res://addons/imjp94.yafsm/scenes/StateNodeContextMenu.tscn" id="1"] 4 | [ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/StateMachineEditor.gd" id="2"] 5 | [ext_resource type="PackedScene" uid="uid://cflltb00e10be" path="res://addons/imjp94.yafsm/scenes/ContextMenu.tscn" id="3"] 6 | [ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/ParametersPanel.gd" id="4"] 7 | 8 | [node name="StateMachineEditor" type="Control"] 9 | visible = false 10 | clip_contents = true 11 | custom_minimum_size = Vector2i(0, 200) 12 | layout_mode = 3 13 | anchors_preset = 15 14 | anchor_right = 1.0 15 | anchor_bottom = 1.0 16 | grow_horizontal = 2 17 | grow_vertical = 2 18 | size_flags_horizontal = 3 19 | size_flags_vertical = 3 20 | focus_mode = 2 21 | mouse_filter = 1 22 | script = ExtResource("2") 23 | 24 | [node name="MarginContainer" type="MarginContainer" parent="."] 25 | visible = false 26 | layout_mode = 1 27 | anchors_preset = 15 28 | anchor_right = 1.0 29 | anchor_bottom = 1.0 30 | grow_horizontal = 2 31 | grow_vertical = 2 32 | size_flags_horizontal = 3 33 | size_flags_vertical = 3 34 | 35 | [node name="Panel" type="Panel" parent="MarginContainer"] 36 | layout_mode = 2 37 | offset_right = 1152.0 38 | offset_bottom = 648.0 39 | 40 | [node name="CreateNewStateMachine" type="Button" parent="MarginContainer"] 41 | layout_mode = 2 42 | offset_left = 473.0 43 | offset_top = 308.0 44 | offset_right = 679.0 45 | offset_bottom = 339.0 46 | size_flags_horizontal = 4 47 | size_flags_vertical = 4 48 | theme_override_colors/font_color = Color(0.87451, 0.87451, 0.87451, 1) 49 | text = "Create new StateMachine" 50 | 51 | [node name="ContextMenu" parent="." instance=ExtResource("3")] 52 | visible = false 53 | 54 | [node name="StateNodeContextMenu" parent="." instance=ExtResource("1")] 55 | visible = false 56 | 57 | [node name="SaveDialog" type="ConfirmationDialog" parent="."] 58 | 59 | [node name="ConvertToStateConfirmation" type="ConfirmationDialog" parent="."] 60 | dialog_text = "All nested states beneath it will be lost, are you sure about that?" 61 | dialog_autowrap = true 62 | 63 | [node name="ParametersPanel" type="MarginContainer" parent="."] 64 | visible = false 65 | layout_mode = 1 66 | anchors_preset = 3 67 | anchor_left = 1.0 68 | anchor_top = 1.0 69 | anchor_right = 1.0 70 | anchor_bottom = 1.0 71 | grow_horizontal = 0 72 | grow_vertical = 0 73 | script = ExtResource("4") 74 | 75 | [node name="PanelContainer" type="PanelContainer" parent="ParametersPanel"] 76 | layout_mode = 2 77 | offset_right = 113.0 78 | offset_bottom = 31.0 79 | 80 | [node name="MarginContainer" type="MarginContainer" parent="ParametersPanel/PanelContainer"] 81 | layout_mode = 2 82 | 83 | [node name="VBoxContainer" type="VBoxContainer" parent="ParametersPanel/PanelContainer/MarginContainer"] 84 | layout_mode = 2 85 | offset_right = 113.0 86 | offset_bottom = 31.0 87 | 88 | [node name="MarginContainer" type="MarginContainer" parent="ParametersPanel/PanelContainer/MarginContainer/VBoxContainer"] 89 | layout_mode = 2 90 | 91 | [node name="Button" type="Button" parent="ParametersPanel/PanelContainer/MarginContainer/VBoxContainer/MarginContainer"] 92 | layout_mode = 2 93 | offset_right = 113.0 94 | offset_bottom = 31.0 95 | size_flags_horizontal = 10 96 | text = "Show Params" 97 | 98 | [node name="GridContainer" type="GridContainer" parent="ParametersPanel/PanelContainer/MarginContainer/VBoxContainer"] 99 | visible = false 100 | layout_mode = 2 101 | columns = 4 102 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## Classes 4 | 5 | All of the class are located in `res://addons/imjp94.yafsm/src` but you can just preload `res://addons/imjp94.yafsm/YAFSM.gd` to import all class available: 6 | 7 | ```gdscript 8 | const YAFSM = preload("res://addons/imjp94.yafsm/YAFSM.gd") 9 | const StackPlayer = YAFSM.StackPlayer 10 | const StateMachinePlayer = YAFSM.StateMachinePlayer 11 | const StateMachine = YAFSM.StateMachine 12 | const State = YAFSM.State 13 | ``` 14 | 15 | ### Node 16 | 17 | - [StackPlayer](src/StackPlayer.gd) ![StackPlayer icon](assets/icons/stack_player_icon.png) 18 | > Manage stack of item, use push/pop function to set current item on top of stack 19 | - `current # Current item on top of stack` 20 | - `stack` 21 | - signals: 22 | - `pushed(to) # When item pushed to stack` 23 | - `popped(from) # When item popped from stack` 24 | - [StateMachinePlayer](src/StateMachinePlayer.gd)(extends StackPlayer) ![StateMachinePlayer icon](assets/icons/state_machine_player_icon.png) 25 | > Manage state based on `StateMachine` and parameters inputted 26 | - `state_machine # StateMachine being played` 27 | - `active # Activeness of player` 28 | - `autostart # Automatically enter Entry state on ready if true` 29 | - `process_mode # ProcessMode of player` 30 | - signals: 31 | - `transited(from, to) # Transition of state` 32 | - `entered(to) # Entry of state machine(including nested), empty string equals to root` 33 | - `exited(from) # Exit of state machine(including nested, empty string equals to root` 34 | - `updated(state, delta) # Time to update(based on process_mode), up to user to handle any logic, for example, update movement of KinematicBody` 35 | 36 | ### Control 37 | 38 | - [StackPlayerDebugger](src/debugger/StackPlayerDebugger.gd) 39 | > Visualize stack of parent StackPlayer on screen 40 | 41 | ### Reference 42 | 43 | - [StateDirectory](src/StateDirectory.gd) 44 | > Convert state path to directory object for traversal, mainly used for nested state 45 | 46 | ### Resource 47 | 48 | Relationship between all `Resource`s can be best represented as below: 49 | 50 | ```gdscript 51 | var state_machine = state_machine_player.state_machine 52 | var state = state_machine.states[state_name] # keyed by state name 53 | var transition = state_machine.transitions[from][to] # keyed by state name transition from/to 54 | var condition = transition.conditions[condition_name] # keyed by condition name 55 | ``` 56 | 57 | > For normal usage, you really don't have to access any `Resource` during runtime as they only store static data that describe the state machine, accessing `StackPlayer`/`StateMachinePlayer` alone should be sufficient. 58 | 59 | - [State](src/states/State.gd) 60 | > Resource that represent a state 61 | - `name` 62 | - [StateMachine](src/states/StateMachine.gd)(`extends State`) ![StateMachine icon](assets/icons/state_machine_icon.png) 63 | > `StateMachine` is also a `State`, but mainly used as container of `State`s and `Transitions`s 64 | - `states` 65 | - `transitions` 66 | - [Transition](src/transitions/Transition.gd) 67 | > Describing connection from one state to another, all conditions must be fulfilled to transit to next state 68 | - `from` 69 | - `to` 70 | - `conditions` 71 | - [Condition](src/conditions/Condition.gd) 72 | > Empty condition with just a name, treated as trigger 73 | - `name` 74 | - [ValueCondition](src/conditions/ValueCondition.gd)(`extends Condition`) 75 | > Condition with value, fulfilled by comparing values based on comparation 76 | - `comparation` 77 | - `value` 78 | - [BooleanCondition](src/conditions/BooleanCondition.gd)(`extends ValueCondition`) 79 | - [IntegerCondition](src/conditions/IntegerCondition.gd)(`extends ValueCondition`) 80 | - [FloatCondition](src/conditions/FloatCondition.gd)(`extends ValueCondition`) 81 | - [StringCondition](src/conditions/StringCondition.gd)(`extends ValueCondition`) 82 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scripts/Utils.gd: -------------------------------------------------------------------------------- 1 | # Position Popup near to its target while within window, solution from ColorPickerButton source code(https://github.com/godotengine/godot/blob/6d8c14f849376905e1577f9fc3f9512bcffb1e3c/scene/gui/color_picker.cpp#L878) 2 | static func popup_on_target(popup: Popup, target: Control): 3 | popup.reset_size() 4 | var usable_rect = Rect2(Vector2.ZERO, DisplayServer.window_get_size_with_decorations()) 5 | var cp_rect = Rect2(Vector2.ZERO, popup.get_size()) 6 | for i in 4: 7 | if i > 1: 8 | cp_rect.position.y = target.global_position.y - cp_rect.size.y 9 | else: 10 | cp_rect.position.y = target.global_position.y + target.get_size().y 11 | 12 | if i & 1: 13 | cp_rect.position.x = target.global_position.x 14 | else: 15 | cp_rect.position.x = target.global_position.x - max(0, cp_rect.size.x - target.get_size().x) 16 | 17 | if usable_rect.encloses(cp_rect): 18 | break 19 | var main_window_position = DisplayServer.window_get_position() 20 | var popup_position = main_window_position + Vector2i(cp_rect.position) # make it work in multi-screen setups 21 | popup.set_position(popup_position) 22 | popup.popup() 23 | 24 | static func get_complementary_color(color): 25 | var r = max(color.r, max(color.b, color.g)) + min(color.r, min(color.b, color.g)) - color.r 26 | var g = max(color.r, max(color.b, color.g)) + min(color.r, min(color.b, color.g)) - color.g 27 | var b = max(color.r, max(color.b, color.g)) + min(color.r, min(color.b, color.g)) - color.b 28 | return Color(r, g, b) 29 | 30 | class CohenSutherland: 31 | const INSIDE = 0 # 0000 32 | const LEFT = 1 # 0001 33 | const RIGHT = 2 # 0010 34 | const BOTTOM = 4 # 0100 35 | const TOP = 8 # 1000 36 | 37 | # Compute bit code for a point(x, y) using the clip 38 | static func compute_code(x, y, x_min, y_min, x_max, y_max): 39 | var code = INSIDE # initialised as being inside of clip window 40 | if x < x_min: # to the left of clip window 41 | code |= LEFT 42 | elif x > x_max: # to the right of clip window 43 | code |= RIGHT 44 | 45 | if y < y_min: # below the clip window 46 | code |= BOTTOM 47 | elif y > y_max: # above the clip window 48 | code |= TOP 49 | 50 | return code 51 | 52 | # Cohen-Sutherland clipping algorithm clips a line from 53 | # P0 = (x0, y0) to P1 = (x1, y1) against a rectangle with 54 | # diagonal from start(x_min, y_min) to end(x_max, y_max) 55 | static func line_intersect_rectangle(from, to, rect): 56 | var x_min = rect.position.x 57 | var y_min = rect.position.y 58 | var x_max = rect.end.x 59 | var y_max = rect.end.y 60 | 61 | var code0 = compute_code(from.x, from.y, x_min, y_min, x_max, y_max) 62 | var code1 = compute_code(to.x, to.y, x_min, y_min, x_max, y_max) 63 | 64 | var i = 0 65 | while true: 66 | i += 1 67 | if !(code0 | code1): # bitwise OR 0, both points inside window 68 | return true 69 | elif code0 & code1: # Bitwise AND not 0, both points share an outside zone 70 | return false 71 | else: 72 | # Failed both test, so calculate line segment to clip 73 | # from outside point to intersection with clip edge 74 | var x 75 | var y 76 | var code_out = max(code0, code1) # Pick the one outside window 77 | 78 | # Find intersection points 79 | # slope = (y1 - y0) / (x1 - x0) 80 | # x = x0 + (1 / slope) * (ym - y0), where ym is y_mix/y_max 81 | # y = y0 + slope * (xm - x0), where xm is x_min/x_max 82 | if code_out & TOP: # Point above clip window 83 | x = from.x + (to.x - from.x) * (y_max - from.y) / (to.y - from.y) 84 | y = y_max 85 | elif code_out & BOTTOM: # Point below clip window 86 | x = from.x + (to.x - from.x) * (y_min - from.y) / (to.y - from.y) 87 | y = y_min 88 | elif code_out & RIGHT: # Point is to the right of clip window 89 | y = from.y + (to.y - from.y) * (x_max - from.x) / (to.x - from.x) 90 | x = x_max 91 | elif code_out & LEFT: # Point is to the left of clip window 92 | y = from.y + (to.y - from.y) * (x_min - from.x) / (to.x - from.x) 93 | x = x_min 94 | 95 | # Now move outside point to intersection point to clip and ready for next pass 96 | if code_out == code0: 97 | from.x = x 98 | from.y = y 99 | code0 = compute_code(from.x, from.y, x_min, y_min, x_max, y_max) 100 | else: 101 | to.x = x 102 | to.y = y 103 | code1 = compute_code(to.x ,to.y, x_min, y_min, x_max, y_max) 104 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/transition_editors/TransitionLine.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "res://addons/imjp94.yafsm/scenes/flowchart/FlowChartLine.gd" 3 | const Transition = preload("../../src/transitions/Transition.gd") 4 | const ValueCondition = preload("../../src/conditions/ValueCondition.gd") 5 | 6 | const hi_res_font: Font = preload("res://addons/imjp94.yafsm/assets/fonts/sans_serif.tres") 7 | 8 | @export var upright_angle_range: = 5.0 9 | 10 | @onready var label_margin = $MarginContainer 11 | @onready var vbox = $MarginContainer/VBoxContainer 12 | 13 | var undo_redo 14 | 15 | var transition: 16 | set = set_transition 17 | var template = "{condition_name} {condition_comparation} {condition_value}" 18 | 19 | var _template_var = {} 20 | 21 | func _init(): 22 | super._init() 23 | 24 | set_transition(Transition.new()) 25 | 26 | func _draw(): 27 | super._draw() 28 | 29 | var abs_rotation = abs(rotation) 30 | var is_flip = abs_rotation > deg_to_rad(90.0) 31 | var is_upright = (abs_rotation > (deg_to_rad(90.0) - deg_to_rad(upright_angle_range))) and (abs_rotation < (deg_to_rad(90.0) + deg_to_rad(upright_angle_range))) 32 | 33 | if is_upright: 34 | var x_offset = label_margin.size.x / 2 35 | var y_offset = -label_margin.size.y 36 | label_margin.position = Vector2((size.x - x_offset) / 2, 0) 37 | else: 38 | var x_offset = label_margin.size.x 39 | var y_offset = -label_margin.size.y 40 | if is_flip: 41 | label_margin.rotation = deg_to_rad(180) 42 | label_margin.position = Vector2((size.x + x_offset) / 2, 0) 43 | else: 44 | label_margin.rotation = deg_to_rad(0) 45 | label_margin.position = Vector2((size.x - x_offset) / 2, y_offset) 46 | 47 | # Update overlay text 48 | func update_label(): 49 | if transition: 50 | var template_var = {"condition_name": "", "condition_comparation": "", "condition_value": null} 51 | for label in vbox.get_children(): 52 | if not (str(label.name) in transition.conditions.keys()): # Names of nodes are now of type StringName, not simple strings! 53 | vbox.remove_child(label) 54 | label.queue_free() 55 | for condition in transition.conditions.values(): 56 | var label = vbox.get_node_or_null(NodePath(condition.name)) 57 | if not label: 58 | label = Label.new() 59 | label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER 60 | label.add_theme_font_override("font", hi_res_font) 61 | label.name = condition.name 62 | vbox.add_child(label) 63 | if "value" in condition: 64 | template_var["condition_name"] = condition.name 65 | template_var["condition_comparation"] = ValueCondition.COMPARATION_SYMBOLS[condition.comparation] 66 | template_var["condition_value"] = condition.get_value_string() 67 | label.text = template.format(template_var) 68 | var override_template_var = _template_var.get(condition.name) 69 | if override_template_var: 70 | label.text = label.text.format(override_template_var) 71 | else: 72 | label.text = condition.name 73 | queue_redraw() 74 | 75 | func _on_transition_changed(new_transition): 76 | if not is_inside_tree(): 77 | return 78 | 79 | if new_transition: 80 | new_transition.condition_added.connect(_on_transition_condition_added) 81 | new_transition.condition_removed.connect(_on_transition_condition_removed) 82 | for condition in new_transition.conditions.values(): 83 | condition.name_changed.connect(_on_condition_name_changed) 84 | condition.display_string_changed.connect(_on_condition_display_string_changed) 85 | update_label() 86 | 87 | func _on_transition_condition_added(condition): 88 | condition.name_changed.connect(_on_condition_name_changed) 89 | condition.display_string_changed.connect(_on_condition_display_string_changed) 90 | update_label() 91 | 92 | func _on_transition_condition_removed(condition): 93 | condition.name_changed.disconnect(_on_condition_name_changed) 94 | condition.display_string_changed.disconnect(_on_condition_display_string_changed) 95 | update_label() 96 | 97 | func _on_condition_name_changed(from, to): 98 | var label = vbox.get_node_or_null(NodePath(from)) 99 | if label: 100 | label.name = to 101 | update_label() 102 | 103 | func _on_condition_display_string_changed(display_string): 104 | update_label() 105 | 106 | func set_transition(t): 107 | if transition != t: 108 | if transition: 109 | if transition.condition_added.is_connected(_on_transition_condition_added): 110 | transition.condition_added.disconnect(_on_transition_condition_added) 111 | transition = t 112 | _on_transition_changed(transition) 113 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/flowchart/FlowChartLayer.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | const FlowChartNode = preload("res://addons/imjp94.yafsm/scenes/flowchart/FlowChartNode.gd") 4 | 5 | var content_lines = Control.new() # Node that hold all flowchart lines 6 | var content_nodes = Control.new() # Node that hold all flowchart nodes 7 | 8 | var _connections = {} 9 | 10 | func _init(): 11 | 12 | name = "FlowChartLayer" 13 | mouse_filter = MOUSE_FILTER_IGNORE 14 | 15 | content_lines.name = "content_lines" 16 | content_lines.mouse_filter = MOUSE_FILTER_IGNORE 17 | add_child(content_lines) 18 | move_child(content_lines, 0) # Make sure content_lines always behind nodes 19 | 20 | content_nodes.name = "content_nodes" 21 | content_nodes.mouse_filter = MOUSE_FILTER_IGNORE 22 | add_child(content_nodes) 23 | 24 | func hide_content(): 25 | content_nodes.hide() 26 | content_lines.hide() 27 | 28 | func show_content(): 29 | content_nodes.show() 30 | content_lines.show() 31 | 32 | # Get required scroll rect base on content 33 | func get_scroll_rect(scroll_margin=0): 34 | var rect = Rect2() 35 | for child in content_nodes.get_children(): 36 | # Every child is a state/statemachine node 37 | var child_rect = child.get_rect() 38 | rect = rect.merge(child_rect) 39 | return rect.grow(scroll_margin) 40 | 41 | # Add node 42 | func add_node(node): 43 | content_nodes.add_child(node) 44 | 45 | # Remove node 46 | func remove_node(node): 47 | if node: 48 | content_nodes.remove_child(node) 49 | 50 | # Called after connection established 51 | func _connect_node(connection): 52 | content_lines.add_child(connection.line) 53 | connection.join() 54 | 55 | # Called after connection broken 56 | func _disconnect_node(connection): 57 | content_lines.remove_child(connection.line) 58 | return connection.line 59 | 60 | # Rename node 61 | func rename_node(old, new): 62 | for from in _connections.keys(): 63 | if from == old: # Connection from 64 | var from_connections = _connections[from] 65 | _connections.erase(old) 66 | _connections[new] = from_connections 67 | else: # Connection to 68 | for to in _connections[from].keys(): 69 | if to == old: 70 | var from_connection = _connections[from] 71 | var value = from_connection[old] 72 | from_connection.erase(old) 73 | from_connection[new] = value 74 | 75 | # Connect two nodes with a line 76 | func connect_node(line, from, to, interconnection_offset=0): 77 | if from == to: 78 | return # Connect to self 79 | var connections_from = _connections.get(from) 80 | if connections_from: 81 | if to in connections_from: 82 | return # Connection existed 83 | var connection = Connection.new(line, content_nodes.get_node(NodePath(from)), content_nodes.get_node(NodePath(to))) 84 | if connections_from == null: 85 | connections_from = {} 86 | _connections[from] = connections_from 87 | connections_from[to] = connection 88 | _connect_node(connection) 89 | 90 | # Check if connection in both ways 91 | connections_from = _connections.get(to) 92 | if connections_from: 93 | var inv_connection = connections_from.get(from) 94 | if inv_connection: 95 | connection.offset = interconnection_offset 96 | inv_connection.offset = interconnection_offset 97 | connection.join() 98 | inv_connection.join() 99 | 100 | # Break a connection between two node 101 | func disconnect_node(from, to): 102 | var connections_from = _connections.get(from) 103 | var connection = connections_from.get(to) 104 | if connection == null: 105 | return 106 | 107 | _disconnect_node(connection) 108 | if connections_from.size() == 1: 109 | _connections.erase(from) 110 | else: 111 | connections_from.erase(to) 112 | 113 | connections_from = _connections.get(to) 114 | if connections_from: 115 | var inv_connection = connections_from.get(from) 116 | if inv_connection: 117 | inv_connection.offset = 0 118 | inv_connection.join() 119 | return connection.line 120 | 121 | # Clear all selection 122 | func clear_connections(): 123 | for connections_from in _connections.values(): 124 | for connection in connections_from.values(): 125 | connection.line.queue_free() 126 | _connections.clear() 127 | 128 | # Return array of dictionary of connection as such [{"from1": "to1"}, {"from2": "to2"}] 129 | func get_connection_list(): 130 | var connection_list = [] 131 | for connections_from in _connections.values(): 132 | for connection in connections_from.values(): 133 | connection_list.append({"from": connection.from_node.name, "to": connection.to_node.name}) 134 | return connection_list 135 | 136 | class Connection: 137 | var line # Control node that draw line 138 | var from_node 139 | var to_node 140 | var offset = 0 # line's y offset to make space for two interconnecting lines 141 | 142 | func _init(p_line, p_from_node, p_to_node): 143 | line = p_line 144 | from_node = p_from_node 145 | to_node = p_to_node 146 | 147 | # Update line position 148 | func join(): 149 | line.join(get_from_pos(), get_to_pos(), offset, [from_node.get_rect() if from_node else Rect2(), to_node.get_rect() if to_node else Rect2()]) 150 | 151 | # Return start position of line 152 | func get_from_pos(): 153 | return from_node.position + from_node.size / 2 154 | 155 | # Return destination position of line 156 | func get_to_pos(): 157 | return to_node.position + to_node.size / 2 if to_node else line.position 158 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/StateMachineEditorLayer.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "res://addons/imjp94.yafsm/scenes/flowchart/FlowChartLayer.gd" 3 | 4 | const Utils = preload("res://addons/imjp94.yafsm/scripts/Utils.gd") 5 | const StateNode = preload("res://addons/imjp94.yafsm/scenes/state_nodes/StateNode.tscn") 6 | const StateNodeScript = preload("res://addons/imjp94.yafsm/scenes/state_nodes/StateNode.gd") 7 | const StateDirectory = preload("../src/StateDirectory.gd") 8 | 9 | var editor_accent_color: = Color.WHITE: 10 | set = set_editor_accent_color 11 | var editor_complementary_color = Color.WHITE 12 | 13 | var state_machine 14 | var tween_lines 15 | var tween_labels 16 | var tween_nodes 17 | 18 | 19 | func debug_update(current_state, parameters, local_parameters): 20 | _init_tweens() 21 | if not state_machine: 22 | return 23 | var current_dir = StateDirectory.new(current_state) 24 | var transitions = state_machine.transitions.get(current_state, {}) 25 | if current_dir.is_nested(): 26 | transitions = state_machine.transitions.get(current_dir.get_end(), {}) 27 | for transition in transitions.values(): 28 | # Check all possible transitions from current state, update labels, color them accordingly 29 | var line = content_lines.get_node_or_null(NodePath("%s>%s" % [transition.from, transition.to])) 30 | if line: 31 | # Blinking alpha of TransitionLine 32 | var color1 = Color.WHITE 33 | color1.a = 0.1 34 | var color2 = Color.WHITE 35 | color2.a = 0.5 36 | if line.self_modulate == color1: 37 | tween_lines.tween_property(line, "self_modulate", color2, 0.5) 38 | elif line.self_modulate == color2: 39 | tween_lines.tween_property(line, "self_modulate", color1, 0.5) 40 | elif line.self_modulate == Color.WHITE: 41 | tween_lines.tween_property(line, "self_modulate", color2, 0.5) 42 | # Update TransitionLine condition labels 43 | for condition in transition.conditions.values(): 44 | if not ("value" in condition): # Ignore trigger 45 | continue 46 | var value = parameters.get(str(condition.name)) 47 | value = str(value) if value != null else "?" 48 | var label = line.vbox.get_node_or_null(NodePath(str(condition.name))) 49 | var override_template_var = line._template_var.get(str(condition.name)) 50 | if override_template_var == null: 51 | override_template_var = {} 52 | line._template_var[str(condition.name)] = override_template_var 53 | override_template_var["value"] = str(value) 54 | line.update_label() 55 | # Condition label color based on comparation 56 | var cond_1: bool = condition.compare(parameters.get(str(condition.name))) 57 | var cond_2: bool = condition.compare(local_parameters.get(str(condition.name))) 58 | if cond_1 or cond_2: 59 | tween_labels.tween_property(label, "self_modulate", Color.GREEN.lightened(0.5), 0.01) 60 | else: 61 | tween_labels.tween_property(label, "self_modulate", Color.RED.lightened(0.5), 0.01) 62 | _start_tweens() 63 | 64 | func debug_transit_out(from, to): 65 | _init_tweens() 66 | var from_dir = StateDirectory.new(from) 67 | var to_dir = StateDirectory.new(to) 68 | var from_node = content_nodes.get_node_or_null(NodePath(from_dir.get_end())) 69 | if from_node != null: 70 | tween_nodes.tween_property(from_node, "self_modulate", editor_complementary_color, 0.01) 71 | tween_nodes.tween_property(from_node, "self_modulate", Color.WHITE, 1) 72 | var transitions = state_machine.transitions.get(from, {}) 73 | if from_dir.is_nested(): 74 | transitions = state_machine.transitions.get(from_dir.get_end(), {}) 75 | # Fade out color of StateNode 76 | for transition in transitions.values(): 77 | var line = content_lines.get_node_or_null(NodePath("%s>%s" % [transition.from, transition.to])) 78 | if line: 79 | line.template = "{condition_name} {condition_comparation} {condition_value}" 80 | line.update_label() 81 | if transition.to == to_dir.get_end(): 82 | tween_lines.tween_property(line, "self_modulate", editor_complementary_color, 0.01) 83 | tween_lines.tween_property(line, "self_modulate", Color.WHITE, 1).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_IN) 84 | # Highlight all the conditions of the transition that just happened 85 | for condition in transition.conditions.values(): 86 | if not ("value" in condition): # Ignore trigger 87 | continue 88 | var label = line.vbox.get_node_or_null(NodePath(condition.name)) 89 | tween_labels.tween_property(label, "self_modulate", editor_complementary_color, 0.01) 90 | tween_labels.tween_property(label, "self_modulate", Color.WHITE, 1) 91 | else: 92 | tween_lines.tween_property(line, "self_modulate", Color.WHITE, 0.1) 93 | # Revert color of TransitionLine condition labels 94 | for condition in transition.conditions.values(): 95 | if not ("value" in condition): # Ignore trigger 96 | continue 97 | var label = line.vbox.get_node_or_null(NodePath(condition.name)) 98 | if label.self_modulate != Color.WHITE: 99 | tween_labels.tween_property(label, "self_modulate", Color.WHITE, 0.5) 100 | if from_dir.is_nested() and from_dir.is_exit(): 101 | # Transition from nested state 102 | transitions = state_machine.transitions.get(from_dir.get_base(), {}) 103 | tween_lines.set_parallel(true) 104 | for transition in transitions.values(): 105 | var line = content_lines.get_node_or_null(NodePath("%s>%s" % [transition.from, transition.to])) 106 | if line: 107 | tween_lines.tween_property(line, "self_modulate", editor_complementary_color.lightened(0.5), 0.1) 108 | for transition in transitions.values(): 109 | var line = content_lines.get_node_or_null(NodePath("%s>%s" % [transition.from, transition.to])) 110 | if line: 111 | tween_lines.tween_property(line, "self_modulate", Color.WHITE, 0.1) 112 | _start_tweens() 113 | 114 | func debug_transit_in(from, to): 115 | _init_tweens() 116 | var to_dir = StateDirectory.new(to) 117 | var to_node = content_nodes.get_node_or_null(NodePath(to_dir.get_end())) 118 | if to_node: 119 | tween_nodes.tween_property(to_node, "self_modulate", editor_complementary_color, 0.5) 120 | var transitions = state_machine.transitions.get(to, {}) 121 | if to_dir.is_nested(): 122 | transitions = state_machine.transitions.get(to_dir.get_end(), {}) 123 | # Change string template for current TransitionLines 124 | for transition in transitions.values(): 125 | var line = content_lines.get_node_or_null(NodePath("%s>%s" % [transition.from, transition.to])) 126 | line.template = "{condition_name} {condition_comparation} {condition_value}({value})" 127 | _start_tweens() 128 | 129 | func set_editor_accent_color(color): 130 | editor_accent_color = color 131 | editor_complementary_color = Utils.get_complementary_color(color) 132 | 133 | 134 | func _init_tweens(): 135 | tween_lines = get_tree().create_tween() 136 | tween_lines.stop() 137 | tween_labels = get_tree().create_tween() 138 | tween_labels.stop() 139 | tween_nodes = get_tree().create_tween() 140 | tween_nodes.stop() 141 | 142 | 143 | func _start_tweens(): 144 | tween_lines.tween_interval(0.001) 145 | tween_lines.play() 146 | tween_labels.tween_interval(0.001) 147 | tween_labels.play() 148 | tween_nodes.tween_interval(0.001) 149 | tween_nodes.play() 150 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | const StateMachineEditor = preload("scenes/StateMachineEditor.tscn") 5 | const TransitionInspector = preload("scenes/transition_editors/TransitionInspector.gd") 6 | const StateInspector = preload("scenes/state_nodes/StateInspector.gd") 7 | 8 | const StackPlayerIcon = preload("assets/icons/stack_player_icon.png") 9 | const StateMachinePlayerIcon = preload("assets/icons/state_machine_player_icon.png") 10 | 11 | var state_machine_editor = StateMachineEditor.instantiate() 12 | var transition_inspector = TransitionInspector.new() 13 | var state_inspector = StateInspector.new() 14 | 15 | var focused_object: # Can be StateMachine/StateMachinePlayer 16 | set = set_focused_object 17 | var editor_selection 18 | 19 | var _handled_and_ready_to_edit = false # forces _handles => _edit flow 20 | 21 | func _enter_tree(): 22 | editor_selection = get_editor_interface().get_selection() 23 | editor_selection.selection_changed.connect(_on_EditorSelection_selection_changed) 24 | var editor_base_control = get_editor_interface().get_base_control() 25 | add_custom_type("StackPlayer", "Node", StackPlayer, StackPlayerIcon) 26 | add_custom_type("StateMachinePlayer", "Node", StateMachinePlayer, StateMachinePlayerIcon) 27 | 28 | state_machine_editor.undo_redo = get_undo_redo() 29 | state_machine_editor.selection_stylebox.bg_color = editor_base_control.get_theme_color("box_selection_fill_color", "Editor") 30 | state_machine_editor.selection_stylebox.border_color = editor_base_control.get_theme_color("box_selection_stroke_color", "Editor") 31 | state_machine_editor.zoom_minus.icon = editor_base_control.get_theme_icon("ZoomLess", "EditorIcons") 32 | state_machine_editor.zoom_reset.icon = editor_base_control.get_theme_icon("ZoomReset", "EditorIcons") 33 | state_machine_editor.zoom_plus.icon = editor_base_control.get_theme_icon("ZoomMore", "EditorIcons") 34 | state_machine_editor.snap_button.icon = editor_base_control.get_theme_icon("SnapGrid", "EditorIcons") 35 | state_machine_editor.condition_visibility.texture_pressed = editor_base_control.get_theme_icon("GuiVisibilityVisible", "EditorIcons") 36 | state_machine_editor.condition_visibility.texture_normal = editor_base_control.get_theme_icon("GuiVisibilityHidden", "EditorIcons") 37 | state_machine_editor.editor_accent_color = editor_base_control.get_theme_color("accent_color", "Editor") 38 | state_machine_editor.current_layer.editor_accent_color = state_machine_editor.editor_accent_color 39 | state_machine_editor.transition_arrow_icon = editor_base_control.get_theme_icon("TransitionImmediateBig", "EditorIcons") 40 | state_machine_editor.inspector_changed.connect(_on_inspector_changed) 41 | state_machine_editor.node_selected.connect(_on_StateMachineEditor_node_selected) 42 | state_machine_editor.node_deselected.connect(_on_StateMachineEditor_node_deselected) 43 | state_machine_editor.debug_mode_changed.connect(_on_StateMachineEditor_debug_mode_changed) 44 | # Force anti-alias for default font, so rotated text will looks smoother 45 | var font = editor_base_control.get_theme_font("main", "EditorFonts") 46 | # font.use_filter = true 47 | 48 | transition_inspector.undo_redo = get_undo_redo() 49 | transition_inspector.transition_icon = editor_base_control.get_theme_icon("ToolConnect", "EditorIcons") 50 | add_inspector_plugin(transition_inspector) 51 | add_inspector_plugin(state_inspector) 52 | 53 | func _exit_tree(): 54 | remove_custom_type("StackPlayer") 55 | remove_custom_type("StateMachinePlayer") 56 | 57 | remove_inspector_plugin(transition_inspector) 58 | remove_inspector_plugin(state_inspector) 59 | 60 | if state_machine_editor: 61 | hide_state_machine_editor() 62 | state_machine_editor.queue_free() 63 | 64 | func _handles(object): 65 | if object is StateMachine: 66 | _handled_and_ready_to_edit = true # this should not be necessary, but it seemingly is (Godot 4.0-rc1) 67 | return true # when return true from _handles, _edit can proceed. 68 | if object is StateMachinePlayer: 69 | if object.get_class() == "EditorDebuggerRemoteObjects": 70 | set_focused_object(object) 71 | state_machine_editor.debug_mode = true 72 | return false 73 | return false 74 | 75 | func _edit(object): 76 | if _handled_and_ready_to_edit: # Forces _handles => _edit flow. This should not be necessary, but it seemingly is (Godot 4.0-rc1) 77 | _handled_and_ready_to_edit = false 78 | set_focused_object(object) 79 | 80 | func show_state_machine_editor(): 81 | if focused_object and state_machine_editor: 82 | if not state_machine_editor.is_inside_tree(): 83 | add_control_to_bottom_panel(state_machine_editor, "StateMachine") 84 | make_bottom_panel_item_visible(state_machine_editor) 85 | 86 | func hide_state_machine_editor(): 87 | if state_machine_editor.is_inside_tree(): 88 | state_machine_editor.state_machine = null 89 | remove_control_from_bottom_panel(state_machine_editor) 90 | 91 | func _on_EditorSelection_selection_changed(): 92 | if editor_selection == null: 93 | return 94 | 95 | var selected_nodes = editor_selection.get_selected_nodes() 96 | if selected_nodes.size() == 1: 97 | var selected_node = selected_nodes[0] 98 | if selected_node is StateMachinePlayer: 99 | set_focused_object(selected_node) 100 | return 101 | set_focused_object(null) 102 | 103 | func _on_focused_object_changed(new_obj): 104 | if new_obj: 105 | # Must be shown first, otherwise StateMachineEditor can't execute ui action as it is not added to scene tree 106 | show_state_machine_editor() 107 | var state_machine 108 | if focused_object is StateMachinePlayer: 109 | if focused_object.get_class() == "EditorDebuggerRemoteObjects": 110 | state_machine = focused_object.get("Members/state_machine") 111 | if state_machine == null: 112 | state_machine = focused_object.get("Members/StateMachinePlayer.gd/state_machine") 113 | else: 114 | state_machine = focused_object.state_machine 115 | state_machine_editor.state_machine_player = focused_object 116 | elif focused_object is StateMachine: 117 | state_machine = focused_object 118 | state_machine_editor.state_machine_player = null 119 | state_machine_editor.state_machine = state_machine 120 | else: 121 | hide_state_machine_editor() 122 | 123 | func _on_inspector_changed(property): 124 | #get_editor_interface().get_inspector().refresh() 125 | notify_property_list_changed() 126 | 127 | func _on_StateMachineEditor_node_selected(node): 128 | var to_inspect 129 | if "state" in node: 130 | if node.state is StateMachine: # Ignore, inspect state machine will trigger edit() 131 | return 132 | to_inspect = node.state 133 | elif "transition" in node: 134 | to_inspect = node.transition 135 | get_editor_interface().inspect_object(to_inspect) 136 | 137 | func _on_StateMachineEditor_node_deselected(node): 138 | # editor_selection.remove_node(node) 139 | get_editor_interface().inspect_object(state_machine_editor.state_machine) 140 | 141 | func _on_StateMachineEditor_debug_mode_changed(new_debug_mode): 142 | if not new_debug_mode: 143 | state_machine_editor.debug_mode = false 144 | state_machine_editor.state_machine_player = null 145 | set_focused_object(null) 146 | hide_state_machine_editor() 147 | 148 | func set_focused_object(obj): 149 | if focused_object != obj: 150 | focused_object = obj 151 | _on_focused_object_changed(obj) 152 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/transition_editors/TransitionEditor.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends VBoxContainer 3 | const Utils = preload("../../scripts/Utils.gd") 4 | const ConditionEditor = preload("../condition_editors/ConditionEditor.tscn") 5 | const BoolConditionEditor = preload("../condition_editors/BoolConditionEditor.tscn") 6 | const IntegerConditionEditor = preload("../condition_editors/IntegerConditionEditor.tscn") 7 | const FloatConditionEditor = preload("../condition_editors/FloatConditionEditor.tscn") 8 | const StringConditionEditor = preload("../condition_editors/StringConditionEditor.tscn") 9 | 10 | @onready var header = $HeaderContainer/Header 11 | @onready var title = $HeaderContainer/Header/Title 12 | @onready var title_icon = $HeaderContainer/Header/Title/Icon 13 | @onready var from = $HeaderContainer/Header/Title/From 14 | @onready var to = $HeaderContainer/Header/Title/To 15 | @onready var condition_count_icon = $HeaderContainer/Header/ConditionCount/Icon 16 | @onready var condition_count_label = $HeaderContainer/Header/ConditionCount/Label 17 | @onready var priority_icon = $HeaderContainer/Header/Priority/Icon 18 | @onready var priority_spinbox = $HeaderContainer/Header/Priority/SpinBox 19 | @onready var add = $HeaderContainer/Header/HBoxContainer/Add 20 | @onready var add_popup_menu = $HeaderContainer/Header/HBoxContainer/Add/PopupMenu 21 | @onready var content_container = $MarginContainer 22 | @onready var condition_list = $MarginContainer/Conditions 23 | 24 | var undo_redo 25 | 26 | var transition: 27 | set = set_transition 28 | 29 | var _to_free 30 | 31 | 32 | func _init(): 33 | _to_free = [] 34 | 35 | func _ready(): 36 | header.gui_input.connect(_on_header_gui_input) 37 | priority_spinbox.value_changed.connect(_on_priority_spinbox_value_changed) 38 | add.pressed.connect(_on_add_pressed) 39 | add_popup_menu.index_pressed.connect(_on_add_popup_menu_index_pressed) 40 | 41 | condition_count_icon.texture = get_theme_icon("MirrorX", "EditorIcons") 42 | priority_icon.texture = get_theme_icon("AnimationTrackGroup", "EditorIcons") 43 | 44 | func _exit_tree(): 45 | free_node_from_undo_redo() # Managed by EditorInspector 46 | 47 | func _on_header_gui_input(event): 48 | if event is InputEventMouseButton: 49 | if event.button_index == MOUSE_BUTTON_LEFT and event.pressed: 50 | toggle_conditions() 51 | 52 | func _on_priority_spinbox_value_changed(val: int) -> void: 53 | set_priority(val) 54 | 55 | func _on_add_pressed(): 56 | Utils.popup_on_target(add_popup_menu, add) 57 | 58 | func _on_add_popup_menu_index_pressed(index): 59 | ## Handle condition name duplication (4.x changed how duplicates are 60 | ## automatically handled and gave a random index instead of a progressive one) 61 | var default_new_condition_name = "Param" 62 | var condition_dup_index = 0 63 | var new_name = default_new_condition_name 64 | for condition_editor in condition_list.get_children(): 65 | var condition_name = condition_editor.condition.name 66 | if (condition_name == new_name): 67 | condition_dup_index += 1 68 | new_name = "%s%s" % [default_new_condition_name, condition_dup_index] 69 | var condition 70 | match index: 71 | 0: # Trigger 72 | condition = Condition.new(new_name) 73 | 1: # Boolean 74 | condition = BooleanCondition.new(new_name) 75 | 2: # Integer 76 | condition = IntegerCondition.new(new_name) 77 | 3: # Float 78 | condition = FloatCondition.new(new_name) 79 | 4: # String 80 | condition = StringCondition.new(new_name) 81 | _: 82 | push_error("Unexpected index(%d) from PopupMenu" % index) 83 | var editor = create_condition_editor(condition) 84 | add_condition_editor_action(editor, condition) 85 | 86 | func _on_ConditionEditorRemove_pressed(editor): 87 | remove_condition_editor_action(editor) 88 | 89 | func _on_transition_changed(new_transition): 90 | if not new_transition: 91 | return 92 | 93 | for condition in transition.conditions.values(): 94 | var editor = create_condition_editor(condition) 95 | add_condition_editor(editor, condition) 96 | update_title() 97 | update_condition_count() 98 | update_priority_spinbox_value() 99 | 100 | func _on_condition_editor_added(editor): 101 | editor.undo_redo = undo_redo 102 | if not editor.remove.pressed.is_connected(_on_ConditionEditorRemove_pressed): 103 | editor.remove.pressed.connect(_on_ConditionEditorRemove_pressed.bind(editor)) 104 | transition.add_condition(editor.condition) 105 | update_condition_count() 106 | 107 | func add_condition_editor(editor, condition): 108 | condition_list.add_child(editor) 109 | editor.condition = condition # Must be assigned after enter tree, as assignment would trigger ui code 110 | _on_condition_editor_added(editor) 111 | 112 | func remove_condition_editor(editor): 113 | transition.remove_condition(editor.condition.name) 114 | condition_list.remove_child(editor) 115 | _to_free.append(editor) # Freeing immediately after removal will break undo/redo 116 | update_condition_count() 117 | 118 | func update_title(): 119 | from.text = transition.from 120 | to.text = transition.to 121 | 122 | func update_condition_count(): 123 | var count = transition.conditions.size() 124 | condition_count_label.text = str(count) 125 | if count == 0: 126 | hide_conditions() 127 | else: 128 | show_conditions() 129 | 130 | func update_priority_spinbox_value(): 131 | priority_spinbox.value = transition.priority 132 | priority_spinbox.apply() 133 | 134 | func set_priority(value): 135 | transition.priority = value 136 | 137 | func show_conditions(): 138 | content_container.visible = true 139 | 140 | func hide_conditions(): 141 | content_container.visible = false 142 | 143 | func toggle_conditions(): 144 | content_container.visible = !content_container.visible 145 | 146 | func create_condition_editor(condition): 147 | var editor 148 | if condition is BooleanCondition: 149 | editor = BoolConditionEditor.instantiate() 150 | elif condition is IntegerCondition: 151 | editor = IntegerConditionEditor.instantiate() 152 | elif condition is FloatCondition: 153 | editor = FloatConditionEditor.instantiate() 154 | elif condition is StringCondition: 155 | editor = StringConditionEditor.instantiate() 156 | else: 157 | editor = ConditionEditor.instantiate() 158 | return editor 159 | 160 | func add_condition_editor_action(editor, condition): 161 | undo_redo.create_action("Add Transition Condition") 162 | undo_redo.add_do_method(self, "add_condition_editor", editor, condition) 163 | undo_redo.add_undo_method(self, "remove_condition_editor", editor) 164 | undo_redo.commit_action() 165 | 166 | func remove_condition_editor_action(editor): 167 | undo_redo.create_action("Remove Transition Condition") 168 | undo_redo.add_do_method(self, "remove_condition_editor", editor) 169 | undo_redo.add_undo_method(self, "add_condition_editor", editor, editor.condition) 170 | undo_redo.commit_action() 171 | 172 | func set_transition(t): 173 | if transition != t: 174 | transition = t 175 | _on_transition_changed(t) 176 | 177 | # Free nodes cached in UndoRedo stack 178 | func free_node_from_undo_redo(): 179 | for node in _to_free: 180 | if is_instance_valid(node): 181 | var history_id = undo_redo.get_object_history_id(node) 182 | undo_redo.get_history_undo_redo(history_id).clear_history(false) # TODO: Should be handled by plugin.gd (Temporary solution as only TransitionEditor support undo/redo) 183 | node.queue_free() 184 | 185 | _to_free.clear() 186 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/flowchart/FlowChartLine.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=7 format=3 uid="uid://creoglbeckyhs"] 2 | 3 | [ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/flowchart/FlowChartLine.gd" id="1"] 4 | 5 | [sub_resource type="Image" id="Image_jnerc"] 6 | data = { 7 | "data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 85, 85, 85, 6, 65, 65, 68, 94, 66, 66, 66, 93, 71, 71, 71, 18, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 128, 128, 128, 2, 66, 64, 67, 193, 65, 64, 66, 255, 65, 64, 66, 255, 66, 65, 67, 243, 67, 64, 67, 99, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 69, 64, 69, 44, 65, 64, 66, 255, 142, 141, 143, 255, 187, 187, 188, 255, 93, 92, 93, 254, 65, 64, 66, 255, 66, 65, 68, 184, 73, 64, 73, 28, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 68, 64, 68, 60, 65, 64, 66, 255, 172, 171, 172, 255, 224, 224, 224, 255, 220, 220, 220, 255, 144, 143, 144, 255, 67, 66, 68, 255, 66, 65, 67, 243, 67, 64, 67, 99, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 68, 64, 68, 60, 65, 64, 66, 255, 172, 171, 172, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 193, 193, 193, 255, 93, 92, 93, 254, 65, 64, 66, 255, 66, 65, 68, 184, 73, 64, 73, 28, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 68, 64, 68, 60, 65, 64, 66, 255, 172, 171, 172, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 220, 220, 220, 255, 144, 143, 144, 255, 67, 66, 68, 255, 66, 65, 67, 243, 67, 64, 67, 99, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 68, 64, 68, 60, 65, 64, 66, 255, 172, 171, 172, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 193, 193, 193, 255, 93, 92, 93, 254, 65, 64, 66, 255, 66, 65, 68, 184, 73, 64, 73, 28, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 68, 64, 68, 60, 65, 64, 66, 255, 172, 171, 172, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 220, 220, 220, 255, 144, 143, 144, 255, 67, 66, 68, 255, 66, 65, 67, 242, 65, 65, 70, 47, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 68, 64, 68, 60, 65, 64, 66, 255, 172, 171, 172, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 193, 193, 193, 255, 79, 78, 80, 253, 67, 66, 69, 189, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 68, 64, 68, 60, 65, 64, 66, 255, 172, 171, 172, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 220, 219, 220, 255, 95, 94, 96, 254, 68, 67, 69, 210, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 68, 64, 68, 60, 65, 64, 66, 255, 172, 171, 172, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 188, 188, 188, 255, 89, 88, 89, 253, 65, 64, 66, 255, 66, 66, 66, 93, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 68, 64, 68, 60, 65, 64, 66, 255, 172, 171, 172, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 220, 219, 220, 255, 137, 136, 138, 255, 67, 66, 68, 255, 67, 66, 68, 239, 66, 64, 66, 88, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 68, 64, 68, 60, 65, 64, 66, 255, 172, 171, 172, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 188, 188, 188, 255, 89, 88, 89, 253, 65, 64, 66, 255, 66, 64, 66, 174, 66, 66, 66, 23, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 68, 64, 68, 60, 65, 64, 66, 255, 172, 171, 172, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 220, 219, 220, 255, 137, 136, 138, 255, 67, 66, 68, 255, 67, 66, 68, 239, 66, 64, 66, 88, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 68, 64, 68, 60, 65, 64, 66, 255, 172, 171, 172, 255, 224, 224, 224, 255, 224, 224, 224, 255, 188, 188, 188, 255, 89, 88, 89, 253, 65, 64, 66, 255, 66, 64, 66, 174, 66, 66, 66, 23, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 69, 64, 69, 48, 65, 64, 66, 255, 165, 165, 166, 255, 220, 219, 220, 255, 137, 136, 138, 255, 67, 66, 68, 255, 67, 66, 68, 239, 66, 64, 66, 88, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 128, 128, 128, 2, 67, 66, 68, 227, 71, 70, 72, 253, 77, 76, 78, 253, 65, 64, 66, 255, 66, 64, 66, 174, 66, 66, 66, 23, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 66, 66, 66, 31, 65, 64, 67, 156, 65, 65, 66, 169, 68, 64, 68, 79, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), 8 | "format": "RGBA8", 9 | "height": 20, 10 | "mipmaps": false, 11 | "width": 20 12 | } 13 | 14 | [sub_resource type="ImageTexture" id="ImageTexture_wopk1"] 15 | image = SubResource("Image_jnerc") 16 | 17 | [sub_resource type="StyleBoxFlat" id="3"] 18 | bg_color = Color(1, 1, 1, 1) 19 | shadow_color = Color(0.44, 0.73, 0.98, 1) 20 | shadow_size = 2 21 | 22 | [sub_resource type="StyleBoxFlat" id="4"] 23 | bg_color = Color(1, 1, 1, 1) 24 | border_width_left = 1 25 | border_width_top = 1 26 | border_width_right = 1 27 | border_width_bottom = 1 28 | border_color = Color(0.2, 0.2, 0.2, 1) 29 | border_blend = true 30 | shadow_color = Color(0.2, 0.2, 0.2, 1) 31 | shadow_size = 1 32 | 33 | [sub_resource type="Theme" id="5"] 34 | FlowChartLine/icons/arrow = SubResource("ImageTexture_wopk1") 35 | FlowChartLine/styles/focus = SubResource("3") 36 | FlowChartLine/styles/normal = SubResource("4") 37 | 38 | [node name="FlowChartLine" type="Container"] 39 | offset_bottom = 5.0 40 | pivot_offset = Vector2(0, 2.5) 41 | focus_mode = 1 42 | mouse_filter = 2 43 | theme = SubResource("5") 44 | script = ExtResource("1") 45 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/transition_editors/TransitionEditor.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=7 format=3 uid="uid://dw0ecw2wdeosi"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://dg8cmn5ubq6r5" path="res://addons/imjp94.yafsm/assets/icons/add-white-18dp.svg" id="1"] 4 | [ext_resource type="Script" path="res://addons/imjp94.yafsm/scenes/transition_editors/TransitionEditor.gd" id="3"] 5 | 6 | [sub_resource type="Gradient" id="Gradient_hw7k8"] 7 | offsets = PackedFloat32Array(1) 8 | colors = PackedColorArray(1, 1, 1, 1) 9 | 10 | [sub_resource type="GradientTexture2D" id="GradientTexture2D_ipxab"] 11 | gradient = SubResource("Gradient_hw7k8") 12 | width = 18 13 | height = 18 14 | 15 | [sub_resource type="Image" id="Image_o35y7"] 16 | data = { 17 | "data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), 18 | "format": "RGBA8", 19 | "height": 16, 20 | "mipmaps": false, 21 | "width": 16 22 | } 23 | 24 | [sub_resource type="ImageTexture" id="ImageTexture_v636r"] 25 | image = SubResource("Image_o35y7") 26 | 27 | [node name="TransitionEditor" type="VBoxContainer"] 28 | script = ExtResource("3") 29 | 30 | [node name="HeaderContainer" type="MarginContainer" parent="."] 31 | layout_mode = 2 32 | 33 | [node name="Panel" type="Panel" parent="HeaderContainer"] 34 | layout_mode = 2 35 | 36 | [node name="Header" type="HBoxContainer" parent="HeaderContainer"] 37 | layout_mode = 2 38 | 39 | [node name="Title" type="HBoxContainer" parent="HeaderContainer/Header"] 40 | layout_mode = 2 41 | tooltip_text = "Next State" 42 | 43 | [node name="From" type="Label" parent="HeaderContainer/Header/Title"] 44 | layout_mode = 2 45 | size_flags_horizontal = 3 46 | text = "From" 47 | 48 | [node name="Icon" type="TextureRect" parent="HeaderContainer/Header/Title"] 49 | texture_filter = 1 50 | layout_mode = 2 51 | texture = SubResource("GradientTexture2D_ipxab") 52 | expand_mode = 3 53 | stretch_mode = 3 54 | 55 | [node name="To" type="Label" parent="HeaderContainer/Header/Title"] 56 | layout_mode = 2 57 | size_flags_horizontal = 3 58 | text = "To" 59 | 60 | [node name="VSeparator" type="VSeparator" parent="HeaderContainer/Header"] 61 | layout_mode = 2 62 | 63 | [node name="ConditionCount" type="HBoxContainer" parent="HeaderContainer/Header"] 64 | layout_mode = 2 65 | tooltip_text = "Number of Conditions" 66 | 67 | [node name="Icon" type="TextureRect" parent="HeaderContainer/Header/ConditionCount"] 68 | texture_filter = 1 69 | layout_mode = 2 70 | texture = SubResource("ImageTexture_v636r") 71 | expand_mode = 3 72 | stretch_mode = 3 73 | 74 | [node name="Label" type="Label" parent="HeaderContainer/Header/ConditionCount"] 75 | layout_mode = 2 76 | text = "No." 77 | 78 | [node name="VSeparator2" type="VSeparator" parent="HeaderContainer/Header"] 79 | layout_mode = 2 80 | 81 | [node name="Priority" type="HBoxContainer" parent="HeaderContainer/Header"] 82 | layout_mode = 2 83 | tooltip_text = "Priority" 84 | 85 | [node name="Icon" type="TextureRect" parent="HeaderContainer/Header/Priority"] 86 | texture_filter = 1 87 | layout_mode = 2 88 | texture = SubResource("ImageTexture_v636r") 89 | expand_mode = 3 90 | stretch_mode = 3 91 | 92 | [node name="SpinBox" type="SpinBox" parent="HeaderContainer/Header/Priority"] 93 | layout_mode = 2 94 | max_value = 10.0 95 | rounded = true 96 | allow_greater = true 97 | 98 | [node name="VSeparator3" type="VSeparator" parent="HeaderContainer/Header"] 99 | layout_mode = 2 100 | 101 | [node name="HBoxContainer" type="HBoxContainer" parent="HeaderContainer/Header"] 102 | layout_mode = 2 103 | size_flags_horizontal = 10 104 | 105 | [node name="Add" type="Button" parent="HeaderContainer/Header/HBoxContainer"] 106 | layout_mode = 2 107 | tooltip_text = "Add Condition" 108 | icon = ExtResource("1") 109 | flat = true 110 | 111 | [node name="PopupMenu" type="PopupMenu" parent="HeaderContainer/Header/HBoxContainer/Add"] 112 | item_count = 5 113 | item_0/text = "Trigger" 114 | item_0/id = 0 115 | item_1/text = "Boolean" 116 | item_1/id = 1 117 | item_2/text = "Integer" 118 | item_2/id = 2 119 | item_3/text = "Float" 120 | item_3/id = 3 121 | item_4/text = "String" 122 | item_4/id = 4 123 | 124 | [node name="MarginContainer" type="MarginContainer" parent="."] 125 | layout_mode = 2 126 | 127 | [node name="Panel" type="Panel" parent="MarginContainer"] 128 | layout_mode = 2 129 | size_flags_horizontal = 3 130 | size_flags_vertical = 3 131 | 132 | [node name="Conditions" type="VBoxContainer" parent="MarginContainer"] 133 | layout_mode = 2 134 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/src/states/StateMachine.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | @icon("../../assets/icons/state_machine_icon.png") 3 | extends State 4 | class_name StateMachine 5 | 6 | signal transition_added(transition) # Transition added 7 | signal transition_removed(to_state) # Transition removed 8 | 9 | @export var states: Dictionary: # States within this StateMachine, keyed by State.name 10 | get = get_states, 11 | set = set_states 12 | @export var transitions: Dictionary: # Transitions from this state, keyed by Transition.to 13 | get = get_transitions, 14 | set = set_transitions 15 | 16 | var _states 17 | var _transitions 18 | 19 | 20 | func _init(p_name="", p_transitions={}, p_states={}): 21 | super._init(p_name) 22 | _transitions = p_transitions 23 | _states = p_states 24 | 25 | # Attempt to transit with global/local parameters, where local_params override params 26 | func transit(current_state, params={}, local_params={}): 27 | var nested_states = current_state.split("/") 28 | var is_nested = nested_states.size() > 1 29 | var end_state_machine = self 30 | var base_path = "" 31 | for i in nested_states.size() - 1: # Ignore last one, to get its parent StateMachine 32 | var state = nested_states[i] 33 | # Construct absolute base path 34 | base_path = join_path(base_path, [state]) 35 | if end_state_machine != self: 36 | end_state_machine = end_state_machine.states[state] 37 | else: 38 | end_state_machine = _states[state] # First level state 39 | 40 | # Nested StateMachine in Exit state 41 | if is_nested: 42 | var is_nested_exit = nested_states[nested_states.size()-1] == State.EXIT_STATE 43 | if is_nested_exit: 44 | # Normalize path to transit again with parent of end_state_machine 45 | var end_state_machine_parent_path = "" 46 | for i in nested_states.size() - 2: # Ignore last two state(which is end_state_machine/end_state) 47 | end_state_machine_parent_path = join_path(end_state_machine_parent_path, [nested_states[i]]) 48 | var end_state_machine_parent = get_state(end_state_machine_parent_path) 49 | var normalized_current_state = end_state_machine.name 50 | var next_state = end_state_machine_parent.transit(normalized_current_state, params) 51 | if next_state: 52 | # Construct next state into absolute path 53 | next_state = join_path(end_state_machine_parent_path, [next_state]) 54 | return next_state 55 | 56 | # Transit with current running nested state machine 57 | var from_transitions = end_state_machine.transitions.get(nested_states[nested_states.size()-1]) 58 | if from_transitions: 59 | var from_transitions_array = from_transitions.values() 60 | from_transitions_array.sort_custom(func(a, b): Transition.sort(a, b)) 61 | 62 | for transition in from_transitions_array: 63 | var next_state = transition.transit(params, local_params) 64 | if next_state: 65 | if "states" in end_state_machine.states[next_state]: 66 | # Next state is a StateMachine, return entry state of the state machine in absolute path 67 | next_state = join_path(base_path, [next_state, State.ENTRY_STATE]) 68 | else: 69 | # Construct next state into absolute path 70 | next_state = join_path(base_path, [next_state]) 71 | return next_state 72 | return null 73 | 74 | # Get state from absolute path, for exmaple, "path/to/state" (root == empty string) 75 | # *It is impossible to get parent state machine with path like "../sibling", as StateMachine is not structed as a Tree 76 | func get_state(path): 77 | var state 78 | if path.is_empty(): 79 | state = self 80 | else: 81 | var nested_states = path.split("/") 82 | for i in nested_states.size(): 83 | var dir = nested_states[i] 84 | if state: 85 | state = state.states[dir] 86 | else: 87 | state = _states[dir] # First level state 88 | return state 89 | 90 | # Add state, state name must be unique within this StateMachine, return state added if succeed else return null 91 | func add_state(state): 92 | if not state: 93 | return null 94 | if state.name in _states: 95 | return null 96 | 97 | _states[state.name] = state 98 | return state 99 | 100 | # Remove state by its name 101 | func remove_state(state): 102 | return _states.erase(state) 103 | 104 | # Change existing state key in states(Dictionary), return true if success 105 | func change_state_name(from, to): 106 | if not (from in _states) or to in _states: 107 | return false 108 | 109 | for state_key in _states.keys(): 110 | var state = _states[state_key] 111 | var is_name_changing_state = state_key == from 112 | if is_name_changing_state: 113 | state.name = to 114 | _states[to] = state 115 | _states.erase(from) 116 | for from_key in _transitions.keys(): 117 | var from_transitions = _transitions[from_key] 118 | if from_key == from: 119 | _transitions.erase(from) 120 | _transitions[to] = from_transitions 121 | for to_key in from_transitions.keys(): 122 | var transition = from_transitions[to_key] 123 | if transition.from == from: 124 | transition.from = to 125 | elif transition.to == from: 126 | transition.to = to 127 | if not is_name_changing_state: 128 | # Transitions to name changed state needs to be updated 129 | from_transitions.erase(from) 130 | from_transitions[to] = transition 131 | return true 132 | 133 | # Add transition, Transition.from must be equal to this state's name and Transition.to not added yet 134 | func add_transition(transition): 135 | if transition.from == "" or transition.to == "": 136 | push_warning("Transition missing from/to (%s/%s)" % [transition.from, transition.to]) 137 | return 138 | 139 | var from_transitions 140 | if transition.from in _transitions: 141 | from_transitions = _transitions[transition.from] 142 | else: 143 | from_transitions = {} 144 | _transitions[transition.from] = from_transitions 145 | 146 | from_transitions[transition.to] = transition 147 | emit_signal("transition_added", transition) 148 | 149 | # Remove transition with Transition.to(name of state transiting to) 150 | func remove_transition(from_state, to_state): 151 | var from_transitions = _transitions.get(from_state) 152 | if from_transitions: 153 | if to_state in from_transitions: 154 | from_transitions.erase(to_state) 155 | if from_transitions.is_empty(): 156 | _transitions.erase(from_state) 157 | emit_signal("transition_removed", from_state, to_state) 158 | 159 | func get_entries(): 160 | return _transitions[State.ENTRY_STATE].values() 161 | 162 | func get_exits(): 163 | return _transitions[State.EXIT_STATE].values() 164 | 165 | func has_entry(): 166 | return State.ENTRY_STATE in _states 167 | 168 | func has_exit(): 169 | return State.EXIT_STATE in _states 170 | 171 | # Get duplicate of states dictionary 172 | func get_states(): 173 | return _states.duplicate() 174 | 175 | func set_states(val): 176 | _states = val 177 | 178 | # Get duplicate of transitions dictionary 179 | func get_transitions(): 180 | return _transitions.duplicate() 181 | 182 | func set_transitions(val): 183 | _transitions = val 184 | 185 | static func join_path(base, dirs): 186 | var path = base 187 | for dir in dirs: 188 | if path.is_empty(): 189 | path = dir 190 | else: 191 | path = str(path, "/", dir) 192 | return path 193 | 194 | # Validate state machine resource to identify and fix error 195 | static func validate(state_machine): 196 | var validated = false 197 | for from_key in state_machine.transitions.keys(): 198 | # Non-existing state found in StateMachine.transitions 199 | # See https://github.com/imjp94/gd-YAFSM/issues/6 200 | if not (from_key in state_machine.states): 201 | validated = true 202 | push_warning("gd-YAFSM ValidationError: Non-existing state(%s) found in transition" % from_key) 203 | state_machine.transitions.erase(from_key) 204 | continue 205 | 206 | var from_transition = state_machine.transitions[from_key] 207 | for to_key in from_transition.keys(): 208 | # Non-existing state found in StateMachine.transitions 209 | # See https://github.com/imjp94/gd-YAFSM/issues/6 210 | if not (to_key in state_machine.states): 211 | validated = true 212 | push_warning("gd-YAFSM ValidationError: Non-existing state(%s) found in transition(%s -> %s)" % [to_key, from_key, to_key]) 213 | from_transition.erase(to_key) 214 | continue 215 | 216 | # Mismatch of StateMachine.transitions with Transition.to 217 | # See https://github.com/imjp94/gd-YAFSM/issues/6 218 | var to_transition = from_transition[to_key] 219 | if to_key != to_transition.to: 220 | validated = true 221 | push_warning("gd-YAFSM ValidationError: Mismatch of StateMachine.transitions key(%s) with Transition.to(%s)" % [to_key, to_transition.to]) 222 | to_transition.to = to_key 223 | 224 | # Self connecting transition 225 | # See https://github.com/imjp94/gd-YAFSM/issues/5 226 | if to_transition.from == to_transition.to: 227 | validated = true 228 | push_warning("gd-YAFSM ValidationError: Self connecting transition(%s -> %s)" % [to_transition.from, to_transition.to]) 229 | from_transition.erase(to_key) 230 | return validated 231 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/src/StateMachinePlayer.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | class_name StateMachinePlayer extends StackPlayer 3 | 4 | 5 | signal transited(from, to) # Transition of state 6 | signal entered(to) # Entry of state machine(including nested), empty string equals to root 7 | signal exited(from) # Exit of state machine(including nested, empty string equals to root 8 | signal updated(state, delta) # Time to update(based on process_mode), up to user to handle any logic, for example, update movement of KinematicBody 9 | 10 | # Enum to define how state machine should be updated 11 | enum UpdateProcessMode { 12 | PHYSICS, 13 | IDLE, 14 | MANUAL 15 | } 16 | 17 | @export var state_machine: StateMachine # StateMachine being played 18 | @export var active: = true: # Activeness of player 19 | set = set_active 20 | @export var autostart: = true # Automatically enter Entry state on ready if true 21 | @export var update_process_mode: UpdateProcessMode = UpdateProcessMode.IDLE: # ProcessMode of player 22 | set = set_update_process_mode 23 | 24 | var _is_started = false 25 | var _parameters # Parameters to be passed to condition 26 | var _local_parameters 27 | var _is_update_locked = true 28 | var _was_transited = false # If last transition was successful 29 | var _is_param_edited = false 30 | 31 | 32 | func _init(): 33 | super._init() 34 | 35 | if Engine.is_editor_hint(): 36 | return 37 | 38 | _parameters = {} 39 | _local_parameters = {} 40 | _was_transited = true # Trigger _transit on _ready 41 | 42 | func _get_configuration_warnings() -> PackedStringArray: 43 | var _errors: Array[String] = [] 44 | 45 | if state_machine: 46 | if not state_machine.has_entry(): 47 | _errors.append("The StateMachine provided does not have an Entry node.\nPlease create one to it works properly.") 48 | else: 49 | _errors.append("StateMachinePlayer needs a StateMachine to run.\nPlease create a StateMachine resource to it.") 50 | 51 | return PackedStringArray(_errors) 52 | 53 | func _ready(): 54 | if Engine.is_editor_hint(): 55 | return 56 | 57 | set_process(false) 58 | set_physics_process(false) 59 | call_deferred("_initiate") # Make sure connection of signals can be done in _ready to receive all signal callback 60 | 61 | func _initiate(): 62 | if autostart: 63 | start() 64 | _on_active_changed() 65 | _on_update_process_mode_changed() 66 | 67 | func _process(delta): 68 | if Engine.is_editor_hint(): 69 | return 70 | 71 | _update_start() 72 | update(delta) 73 | _update_end() 74 | 75 | func _physics_process(delta): 76 | if Engine.is_editor_hint(): 77 | return 78 | 79 | _update_start() 80 | update(delta) 81 | _update_end() 82 | 83 | # Only get called in 2 condition, _parameters edited or last transition was successful 84 | func _transit(): 85 | if not active: 86 | return 87 | # Attempt to transit if parameter edited or last transition was successful 88 | if not _is_param_edited and not _was_transited: 89 | return 90 | 91 | var from = get_current() 92 | var local_params = _local_parameters.get(path_backward(from), {}) 93 | var next_state = state_machine.transit(get_current(), _parameters, local_params) 94 | if next_state: 95 | if stack.has(next_state): 96 | reset(stack.find(next_state)) 97 | else: 98 | push(next_state) 99 | var to = next_state 100 | _was_transited = next_state != null and next_state != "" 101 | _is_param_edited = false 102 | _flush_trigger(_parameters) 103 | _flush_trigger(_local_parameters, true) 104 | 105 | if _was_transited: 106 | _on_state_changed(from, to) 107 | 108 | func _on_state_changed(from, to): 109 | match to: 110 | State.ENTRY_STATE: 111 | emit_signal("entered", "") 112 | State.EXIT_STATE: 113 | set_active(false) # Disable on exit 114 | emit_signal("exited", "") 115 | 116 | if to.ends_with(State.ENTRY_STATE) and to.length() > State.ENTRY_STATE.length(): 117 | # Nexted Entry state 118 | var state = path_backward(get_current()) 119 | emit_signal("entered", state) 120 | elif to.ends_with(State.EXIT_STATE) and to.length() > State.EXIT_STATE.length(): 121 | # Nested Exit state, clear "local" params 122 | var state = path_backward(get_current()) 123 | clear_param(state, false) # Clearing params internally, do not update 124 | emit_signal("exited", state) 125 | 126 | emit_signal("transited", from, to) 127 | 128 | # Called internally if process_mode is PHYSICS/IDLE to unlock update() 129 | func _update_start(): 130 | _is_update_locked = false 131 | 132 | # Called internally if process_mode is PHYSICS/IDLE to lock update() from external call 133 | func _update_end(): 134 | _is_update_locked = true 135 | 136 | # Called after update() which is dependant on process_mode, override to process current state 137 | func _on_updated(state, delta): 138 | pass 139 | 140 | func _on_update_process_mode_changed(): 141 | if not active: 142 | return 143 | 144 | match update_process_mode: 145 | UpdateProcessMode.PHYSICS: 146 | set_physics_process(true) 147 | set_process(false) 148 | UpdateProcessMode.IDLE: 149 | set_physics_process(false) 150 | set_process(true) 151 | UpdateProcessMode.MANUAL: 152 | set_physics_process(false) 153 | set_process(false) 154 | 155 | func _on_active_changed(): 156 | if Engine.is_editor_hint(): 157 | return 158 | 159 | if active: 160 | _on_update_process_mode_changed() 161 | _transit() 162 | else: 163 | set_physics_process(false) 164 | set_process(false) 165 | 166 | # Remove all trigger(param with null value) from provided params, only get called after _transit 167 | # Trigger another call of _flush_trigger on first layer of dictionary if nested is true 168 | func _flush_trigger(params, nested=false): 169 | for param_key in params.keys(): 170 | var value = params[param_key] 171 | if nested and value is Dictionary: 172 | _flush_trigger(value) 173 | if value == null: # Param with null as value is treated as trigger 174 | params.erase(param_key) 175 | 176 | func reset(to=-1, event=ResetEventTrigger.LAST_TO_DEST): 177 | super.reset(to, event) 178 | _was_transited = true # Make sure to call _transit on next update 179 | 180 | # Manually start the player, automatically called if autostart is true 181 | func start(): 182 | assert(state_machine != null, "A StateMachine resource is required to start this StateMachinePlayer.") 183 | assert(state_machine.has_entry(), "The StateMachine provided does not have an Entry node.") 184 | push(State.ENTRY_STATE) 185 | emit_signal("entered", "") 186 | _was_transited = true 187 | _is_started = true 188 | 189 | # Restart player 190 | func restart(is_active=true, preserve_params=false): 191 | reset() 192 | set_active(is_active) 193 | if not preserve_params: 194 | clear_param("", false) 195 | start() 196 | 197 | # Update player to, first initiate transition, then call _on_updated, finally emit "update" signal, delta will be given based on process_mode. 198 | # Can only be called manually if process_mode is MANUAL, otherwise, assertion error will be raised. 199 | # *delta provided will be reflected in signal updated(state, delta) 200 | func update(delta=get_physics_process_delta_time()): 201 | if not active: 202 | return 203 | if update_process_mode != UpdateProcessMode.MANUAL: 204 | assert(not _is_update_locked, "Attempting to update manually with ProcessMode %s" % UpdateProcessMode.keys()[update_process_mode]) 205 | 206 | _transit() 207 | var current_state = get_current() 208 | _on_updated(current_state, delta) 209 | emit_signal("updated", current_state, delta) 210 | if update_process_mode == UpdateProcessMode.MANUAL: 211 | # Make sure to auto advance even in MANUAL mode 212 | if _was_transited: 213 | call_deferred("update") 214 | 215 | # Set trigger to be tested with condition, then trigger _transit on next update, 216 | # automatically call update() if process_mode set to MANUAL and auto_update true 217 | # Nested trigger can be accessed through path "path/to/param_name", for example, "App/Game/is_playing" 218 | func set_trigger(name, auto_update=true): 219 | set_param(name, null, auto_update) 220 | 221 | func set_nested_trigger(path, name, auto_update=true): 222 | set_nested_param(path, name, null, auto_update) 223 | 224 | # Set param(null value treated as trigger) to be tested with condition, then trigger _transit on next update, 225 | # automatically call update() if process_mode set to MANUAL and auto_update true 226 | # Nested param can be accessed through path "path/to/param_name", for example, "App/Game/is_playing" 227 | func set_param(name, value, auto_update=true): 228 | var path = "" 229 | if "/" in name: 230 | path = path_backward(name) 231 | name = path_end_dir(name) 232 | set_nested_param(path, name, value, auto_update) 233 | 234 | func set_nested_param(path, name, value, auto_update=true): 235 | if path.is_empty(): 236 | _parameters[name] = value 237 | else: 238 | var local_params = _local_parameters.get(path) 239 | if local_params is Dictionary: 240 | local_params[name] = value 241 | else: 242 | local_params = {} 243 | local_params[name] = value 244 | _local_parameters[path] = local_params 245 | _on_param_edited(auto_update) 246 | 247 | # Remove param, then trigger _transit on next update, 248 | # automatically call update() if process_mode set to MANUAL and auto_update true 249 | # Nested param can be accessed through path "path/to/param_name", for example, "App/Game/is_playing" 250 | func erase_param(name, auto_update=true): 251 | var path = "" 252 | if "/" in name: 253 | path = path_backward(name) 254 | name = path_end_dir(name) 255 | return erase_nested_param(path, name, auto_update) 256 | 257 | func erase_nested_param(path, name, auto_update=true): 258 | var result = false 259 | if path.is_empty(): 260 | result = _parameters.erase(name) 261 | else: 262 | result = _local_parameters.get(path, {}).erase(name) 263 | _on_param_edited(auto_update) 264 | return result 265 | 266 | # Clear params from specified path, empty string to clear all, then trigger _transit on next update, 267 | # automatically call update() if process_mode set to MANUAL and auto_update true 268 | # Nested param can be accessed through path "path/to/param_name", for example, "App/Game/is_playing" 269 | func clear_param(path="", auto_update=true): 270 | if path.is_empty(): 271 | _parameters.clear() 272 | else: 273 | _local_parameters.get(path, {}).clear() 274 | # Clear nested params 275 | for param_key in _local_parameters.keys(): 276 | if param_key.begins_with(path): 277 | _local_parameters.erase(param_key) 278 | 279 | # Called when param edited, automatically call update() if process_mode set to MANUAL and auto_update true 280 | func _on_param_edited(auto_update=true): 281 | _is_param_edited = true 282 | if update_process_mode == UpdateProcessMode.MANUAL and auto_update and _is_started: 283 | update() 284 | 285 | # Get value of param 286 | # Nested param can be accessed through path "path/to/param_name", for example, "App/Game/is_playing" 287 | func get_param(name, default=null): 288 | var path = "" 289 | if "/" in name: 290 | path = path_backward(name) 291 | name = path_end_dir(name) 292 | return get_nested_param(path, name, default) 293 | 294 | func get_nested_param(path, name, default=null): 295 | if path.is_empty(): 296 | return _parameters.get(name, default) 297 | else: 298 | var local_params = _local_parameters.get(path, {}) 299 | return local_params.get(name, default) 300 | 301 | # Get duplicate of whole parameter dictionary 302 | func get_params(): 303 | return _parameters.duplicate() 304 | 305 | # Return true if param exists 306 | # Nested param can be accessed through path "path/to/param_name", for example, "App/Game/is_playing" 307 | func has_param(name): 308 | var path = "" 309 | if "/" in name: 310 | path = path_backward(name) 311 | name = path_end_dir(name) 312 | return has_nested_param(path, name) 313 | 314 | func has_nested_param(path, name): 315 | if path.is_empty(): 316 | return name in _parameters 317 | else: 318 | var local_params = _local_parameters.get(path, {}) 319 | return name in local_params 320 | 321 | # Return if player started 322 | func is_entered(): 323 | return State.ENTRY_STATE in stack 324 | 325 | # Return if player ended 326 | func is_exited(): 327 | return get_current() == State.EXIT_STATE 328 | 329 | func set_active(v): 330 | if active != v: 331 | if v: 332 | if is_exited(): 333 | push_warning("Attempting to make exited StateMachinePlayer active, call reset() then set_active() instead") 334 | return 335 | active = v 336 | _on_active_changed() 337 | 338 | func set_update_process_mode(mode): 339 | if update_process_mode != mode: 340 | update_process_mode = mode 341 | _on_update_process_mode_changed() 342 | 343 | func get_current(): 344 | var v = super.get_current() 345 | return v if v else "" 346 | 347 | func get_previous(): 348 | var v = super.get_previous() 349 | return v if v else "" 350 | 351 | # Convert node path to state path that can be used to query state with StateMachine.get_state. 352 | # Node path, "root/path/to/state", equals to State path, "path/to/state" 353 | static func node_path_to_state_path(node_path): 354 | var p = node_path.replace("root", "") 355 | if p.begins_with("/"): 356 | p = p.substr(1) 357 | return p 358 | 359 | # Convert state path to node path that can be used for query node in scene tree. 360 | # State path, "path/to/state", equals to Node path, "root/path/to/state" 361 | static func state_path_to_node_path(state_path): 362 | var path = state_path 363 | if path.is_empty(): 364 | path = "root" 365 | else: 366 | path = str("root/", path) 367 | return path 368 | 369 | # Return parent path, "path/to/state" return "path/to" 370 | static func path_backward(path): 371 | return path.substr(0, path.rfind("/")) 372 | 373 | # Return end directory of path, "path/to/state" returns "state" 374 | static func path_end_dir(path): 375 | # In Godot 4.x the old behaviour of String.right() can be achieved with 376 | # a negative length. Check the docs: 377 | # https://docs.godotengine.org/en/stable/classes/class_string.html#class-string-method-right 378 | return path.right(path.length()-1 - path.rfind("/")) 379 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/flowchart/FlowChart.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | 4 | const Utils = preload("res://addons/imjp94.yafsm/scripts/Utils.gd") 5 | const CohenSutherland = Utils.CohenSutherland 6 | const FlowChartNode = preload("FlowChartNode.gd") 7 | const FlowChartNodeScene = preload("FlowChartNode.tscn") 8 | const FlowChartLine = preload("FlowChartLine.gd") 9 | const FlowChartLineScene = preload("FlowChartLine.tscn") 10 | const FlowChartLayer = preload("FlowChartLayer.gd") 11 | const FlowChartGrid = preload("FlowChartGrid.gd") 12 | const Connection = FlowChartLayer.Connection 13 | 14 | signal connection(from, to, line) # When a connection established 15 | signal disconnection(from, to, line) # When a connection broken 16 | signal node_selected(node) # When a node selected 17 | signal node_deselected(node) # When a node deselected 18 | signal dragged(node, distance) # When a node dragged 19 | 20 | # Margin of content from edge of FlowChart 21 | @export var scroll_margin: = 50 22 | # Offset between two line that interconnecting 23 | @export var interconnection_offset: = 10 24 | # Snap amount 25 | @export var snap: = 20 26 | # Zoom amount 27 | @export var zoom: = 1.0: 28 | set = set_zoom 29 | @export var zoom_step: = 0.2 30 | @export var max_zoom: = 2.0 31 | @export var min_zoom: = 0.5 32 | 33 | var grid = FlowChartGrid.new() # Grid 34 | var content = Control.new() # Root node that hold anything drawn in the flowchart 35 | var current_layer 36 | var h_scroll = HScrollBar.new() 37 | var v_scroll = VScrollBar.new() 38 | var top_bar = VBoxContainer.new() 39 | var gadget = HBoxContainer.new() # Root node of top overlay controls 40 | var zoom_minus = Button.new() 41 | var zoom_reset = Button.new() 42 | var zoom_plus = Button.new() 43 | var snap_button = Button.new() 44 | var snap_amount = SpinBox.new() 45 | 46 | var is_snapping = true 47 | var can_gui_select_node = true 48 | var can_gui_delete_node = true 49 | var can_gui_connect_node = true 50 | 51 | var _is_connecting = false 52 | var _current_connection 53 | var _is_dragging = false 54 | var _is_dragging_node = false 55 | var _drag_start_pos = Vector2.ZERO 56 | var _drag_end_pos = Vector2.ZERO 57 | var _drag_origins = [] 58 | var _selection = [] 59 | var _copying_nodes = [] 60 | 61 | var selection_stylebox = StyleBoxFlat.new() 62 | var grid_major_color = Color(1, 1, 1, 0.2) 63 | var grid_minor_color = Color(1, 1, 1, 0.05) 64 | 65 | 66 | func _init(): 67 | 68 | focus_mode = FOCUS_ALL 69 | selection_stylebox.bg_color = Color(0, 0, 0, 0.3) 70 | selection_stylebox.set_border_width_all(1) 71 | 72 | self.z_index = 0 73 | 74 | content.mouse_filter = MOUSE_FILTER_IGNORE 75 | add_child(content) 76 | content.z_index = 1 77 | 78 | grid.mouse_filter = MOUSE_FILTER_IGNORE 79 | content.add_child.call_deferred(grid) 80 | grid.z_index = -1 81 | 82 | add_child(h_scroll) 83 | h_scroll.set_anchors_and_offsets_preset(PRESET_BOTTOM_WIDE) 84 | h_scroll.value_changed.connect(_on_h_scroll_changed) 85 | h_scroll.gui_input.connect(_on_h_scroll_gui_input) 86 | 87 | add_child(v_scroll) 88 | v_scroll.set_anchors_and_offsets_preset(PRESET_RIGHT_WIDE) 89 | v_scroll.value_changed.connect(_on_v_scroll_changed) 90 | v_scroll.gui_input.connect(_on_v_scroll_gui_input) 91 | 92 | h_scroll.offset_right = -v_scroll.size.x 93 | v_scroll.offset_bottom = -h_scroll.size.y 94 | 95 | h_scroll.min_value = 0 96 | v_scroll.max_value = 0 97 | 98 | add_layer_to(content) 99 | select_layer_at(0) 100 | 101 | top_bar.set_anchors_and_offsets_preset(PRESET_TOP_WIDE) 102 | top_bar.mouse_filter = MOUSE_FILTER_IGNORE 103 | add_child(top_bar) 104 | 105 | gadget.mouse_filter = MOUSE_FILTER_IGNORE 106 | top_bar.add_child(gadget) 107 | 108 | zoom_minus.flat = true 109 | zoom_minus.tooltip_text = "Zoom Out" 110 | zoom_minus.pressed.connect(_on_zoom_minus_pressed) 111 | zoom_minus.focus_mode = FOCUS_NONE 112 | gadget.add_child(zoom_minus) 113 | 114 | zoom_reset.flat = true 115 | zoom_reset.tooltip_text = "Zoom Reset" 116 | zoom_reset.pressed.connect(_on_zoom_reset_pressed) 117 | zoom_reset.focus_mode = FOCUS_NONE 118 | gadget.add_child(zoom_reset) 119 | 120 | zoom_plus.flat = true 121 | zoom_plus.tooltip_text = "Zoom In" 122 | zoom_plus.pressed.connect(_on_zoom_plus_pressed) 123 | zoom_plus.focus_mode = FOCUS_NONE 124 | gadget.add_child(zoom_plus) 125 | 126 | snap_button.flat = true 127 | snap_button.toggle_mode = true 128 | snap_button.tooltip_text = "Enable snap and show grid" 129 | snap_button.pressed.connect(_on_snap_button_pressed) 130 | snap_button.button_pressed = true 131 | snap_button.focus_mode = FOCUS_NONE 132 | gadget.add_child(snap_button) 133 | 134 | snap_amount.value = snap 135 | snap_amount.value_changed.connect(_on_snap_amount_value_changed) 136 | gadget.add_child(snap_amount) 137 | 138 | func _on_h_scroll_gui_input(event): 139 | if event is InputEventMouseButton: 140 | var v = (h_scroll.max_value - h_scroll.min_value) * 0.01 # Scroll at 0.1% step 141 | match event.button_index: 142 | MOUSE_BUTTON_WHEEL_UP: 143 | h_scroll.value -= v 144 | MOUSE_BUTTON_WHEEL_DOWN: 145 | h_scroll.value += v 146 | 147 | func _on_v_scroll_gui_input(event): 148 | if event is InputEventMouseButton: 149 | var v = (v_scroll.max_value - v_scroll.min_value) * 0.01 # Scroll at 0.1% step 150 | match event.button_index: 151 | MOUSE_BUTTON_WHEEL_UP: 152 | v_scroll.value -= v # scroll left 153 | MOUSE_BUTTON_WHEEL_DOWN: 154 | v_scroll.value += v # scroll right 155 | 156 | func _on_h_scroll_changed(value): 157 | content.position.x = -value 158 | 159 | func _on_v_scroll_changed(value): 160 | content.position.y = -value 161 | 162 | func set_zoom(v): 163 | zoom = clampf(v, min_zoom, max_zoom) 164 | content.scale = Vector2.ONE * zoom 165 | queue_redraw() 166 | grid.queue_redraw() 167 | 168 | func _on_zoom_minus_pressed(): 169 | set_zoom(zoom - zoom_step) 170 | queue_redraw() 171 | 172 | func _on_zoom_reset_pressed(): 173 | set_zoom(1.0) 174 | queue_redraw() 175 | 176 | func _on_zoom_plus_pressed(): 177 | set_zoom(zoom + zoom_step) 178 | queue_redraw() 179 | 180 | func _on_snap_button_pressed(): 181 | is_snapping = snap_button.button_pressed 182 | queue_redraw() 183 | 184 | func _on_snap_amount_value_changed(value): 185 | snap = value 186 | queue_redraw() 187 | 188 | func _draw(): 189 | # Update scrolls 190 | var content_rect: Rect2 = get_scroll_rect(current_layer, 0) 191 | content.pivot_offset = content_rect.size / 2.0 # Scale from center 192 | var flowchart_rect: Rect2 = get_rect() 193 | # ENCLOSE CONDITIONS 194 | var is_content_enclosed = (flowchart_rect.size.x >= content_rect.size.x) 195 | is_content_enclosed = is_content_enclosed and (flowchart_rect.size.y >= content_rect.size.y) 196 | is_content_enclosed = is_content_enclosed and (flowchart_rect.position.x <= content_rect.position.x) 197 | is_content_enclosed = is_content_enclosed and (flowchart_rect.position.y >= content_rect.position.y) 198 | if not is_content_enclosed or (h_scroll.min_value==h_scroll.max_value) or (v_scroll.min_value==v_scroll.max_value): 199 | var h_min = 0 # content_rect.position.x - scroll_margin/2 - content_rect.get_center().x/2 200 | var h_max = content_rect.size.x - content_rect.position.x - size.x + scroll_margin + content_rect.get_center().x 201 | var v_min = 0 # content_rect.position.y - scroll_margin/2 - content_rect.get_center().y/2 202 | var v_max = content_rect.size.y - content_rect.position.y - size.y + scroll_margin + content_rect.get_center().y 203 | if h_min == h_max: # Otherwise scroll bar will complain no ratio 204 | h_min -= 0.1 205 | h_max += 0.1 206 | if v_min == v_max: # Otherwise scroll bar will complain no ratio 207 | v_min -= 0.1 208 | v_max += 0.1 209 | h_scroll.min_value = h_min 210 | h_scroll.max_value = h_max 211 | h_scroll.page = content_rect.size.x / 100 212 | v_scroll.min_value = v_min 213 | v_scroll.max_value = v_max 214 | v_scroll.page = content_rect.size.y / 100 215 | 216 | # Draw selection box 217 | if not _is_dragging_node and not _is_connecting: 218 | var selection_box_rect = get_selection_box_rect() 219 | draw_style_box(selection_stylebox, selection_box_rect) 220 | 221 | if is_snapping: 222 | grid.visible = true 223 | grid.queue_redraw() 224 | else: 225 | grid.visible = false 226 | 227 | # Debug draw 228 | # for node in content_nodes.get_children(): 229 | # var rect = get_transform() * (content.get_transform() * (node.get_rect())) 230 | # draw_style_box(selection_stylebox, rect) 231 | 232 | # var connection_list = get_connection_list() 233 | # for i in connection_list.size(): 234 | # var connection = _connections[connection_list[i].from][connection_list[i].to] 235 | # # Line's offset along its down-vector 236 | # var line_local_up_offset = connection.line.position - connection.line.get_transform() * (Vector2.UP * connection.offset) 237 | # var from_pos = content.get_transform() * (connection.get_from_pos() + line_local_up_offset) 238 | # var to_pos = content.get_transform() * (connection.get_to_pos() + line_local_up_offset) 239 | # draw_line(from_pos, to_pos, Color.yellow) 240 | 241 | func _gui_input(event): 242 | 243 | var OS_KEY_DELETE = KEY_BACKSPACE if ( ["macOS", "OSX"].has(OS.get_name()) ) else KEY_DELETE 244 | if event is InputEventKey: 245 | match event.keycode: 246 | OS_KEY_DELETE: 247 | if event.pressed and can_gui_delete_node: 248 | # Delete nodes 249 | for node in _selection.duplicate(): 250 | if node is FlowChartLine: 251 | # TODO: More efficient way to get connection from Line node 252 | for connections_from in current_layer._connections.duplicate().values(): 253 | for connection in connections_from.duplicate().values(): 254 | if connection.line == node: 255 | disconnect_node(current_layer, connection.from_node.name, connection.to_node.name).queue_free() 256 | elif node is FlowChartNode: 257 | remove_node(current_layer, node.name) 258 | for connection_pair in current_layer.get_connection_list(): 259 | if connection_pair.from == node.name or connection_pair.to == node.name: 260 | disconnect_node(current_layer, connection_pair.from, connection_pair.to).queue_free() 261 | accept_event() 262 | KEY_C: 263 | if event.pressed and event.ctrl_pressed: 264 | # Copy node 265 | _copying_nodes = _selection.duplicate() 266 | accept_event() 267 | KEY_D: 268 | if event.pressed and event.ctrl_pressed: 269 | # Duplicate node directly from selection 270 | duplicate_nodes(current_layer, _selection.duplicate()) 271 | accept_event() 272 | KEY_V: 273 | if event.pressed and event.ctrl_pressed: 274 | # Paste node from _copying_nodes 275 | duplicate_nodes(current_layer, _copying_nodes) 276 | accept_event() 277 | 278 | if event is InputEventMouseMotion: 279 | match event.button_mask: 280 | MOUSE_BUTTON_MASK_MIDDLE: 281 | # Panning 282 | h_scroll.value -= event.relative.x 283 | v_scroll.value -= event.relative.y 284 | queue_redraw() 285 | MOUSE_BUTTON_LEFT: 286 | # Dragging 287 | if _is_dragging: 288 | if _is_connecting: 289 | # Connecting 290 | if _current_connection: 291 | var pos = content_position(get_local_mouse_position()) 292 | var clip_rects = [_current_connection.from_node.get_rect()] 293 | 294 | # Snapping connecting line 295 | for i in current_layer.content_nodes.get_child_count(): 296 | var child = current_layer.content_nodes.get_child(current_layer.content_nodes.get_child_count()-1 - i) # Inverse order to check from top to bottom of canvas 297 | if child is FlowChartNode and child.name != _current_connection.from_node.name: 298 | if _request_connect_to(current_layer, child.name): 299 | if child.get_rect().has_point(pos): 300 | pos = child.position + child.size / 2 301 | clip_rects.append(child.get_rect()) 302 | break 303 | _current_connection.line.join(_current_connection.get_from_pos(), pos, Vector2.ZERO, clip_rects) 304 | elif _is_dragging_node: 305 | # Dragging nodes 306 | var dragged = content_position(_drag_end_pos) - content_position(_drag_start_pos) 307 | for i in _selection.size(): 308 | var selected = _selection[i] 309 | if not (selected is FlowChartNode): 310 | continue 311 | selected.position = (_drag_origins[i] + selected.size / 2.0 + dragged) 312 | selected.modulate.a = 0.3 313 | if is_snapping: 314 | selected.position = selected.position.snapped(Vector2.ONE * snap) 315 | selected.position -= selected.size / 2.0 316 | _on_node_dragged(current_layer, selected, dragged) 317 | emit_signal("dragged", selected, dragged) 318 | # Update connection pos 319 | for from in current_layer._connections: 320 | var connections_from = current_layer._connections[from] 321 | for to in connections_from: 322 | if from == selected.name or to == selected.name: 323 | var connection = current_layer._connections[from][to] 324 | connection.join() 325 | _drag_end_pos = get_local_mouse_position() 326 | queue_redraw() 327 | 328 | if event is InputEventMouseButton: 329 | match event.button_index: 330 | MOUSE_BUTTON_MIDDLE: 331 | # Reset zoom 332 | if event.double_click: 333 | set_zoom(1.0) 334 | queue_redraw() 335 | MOUSE_BUTTON_WHEEL_UP: 336 | # Zoom in 337 | set_zoom(zoom + zoom_step/10) 338 | queue_redraw() 339 | MOUSE_BUTTON_WHEEL_DOWN: 340 | # Zoom out 341 | set_zoom(zoom - zoom_step/10) 342 | queue_redraw() 343 | MOUSE_BUTTON_LEFT: 344 | # Hit detection 345 | var hit_node 346 | for i in current_layer.content_nodes.get_child_count(): 347 | var child = current_layer.content_nodes.get_child(current_layer.content_nodes.get_child_count()-1 - i) # Inverse order to check from top to bottom of canvas 348 | if child is FlowChartNode: 349 | if child.get_rect().has_point(content_position(get_local_mouse_position())): 350 | hit_node = child 351 | break 352 | if not hit_node: 353 | # Test Line 354 | # Refer https://github.com/godotengine/godot/blob/master/editor/plugins/animation_state_machine_editor.cpp#L187 355 | var closest = -1 356 | var closest_d = 1e20 357 | var connection_list = get_connection_list() 358 | for i in connection_list.size(): 359 | var connection = current_layer._connections[connection_list[i].from][connection_list[i].to] 360 | # Line's offset along its down-vector 361 | var line_local_up_offset = connection.line.position - connection.line.get_transform()*(Vector2.DOWN * connection.offset) 362 | var from_pos = connection.get_from_pos() + line_local_up_offset 363 | var to_pos = connection.get_to_pos() + line_local_up_offset 364 | var cp = Geometry2D.get_closest_point_to_segment(content_position(event.position), from_pos, to_pos) 365 | var d = cp.distance_to(content_position(event.position)) 366 | if d > connection.line.size.y * 2: 367 | continue 368 | if d < closest_d: 369 | closest = i 370 | closest_d = d 371 | if closest >= 0: 372 | hit_node = current_layer._connections[connection_list[closest].from][connection_list[closest].to].line 373 | 374 | if event.pressed: 375 | if not (hit_node in _selection) and not event.shift_pressed: 376 | # Click on empty space 377 | clear_selection() 378 | if hit_node: 379 | # Click on node(can be a line) 380 | _is_dragging_node = true 381 | if hit_node is FlowChartLine: 382 | current_layer.content_lines.move_child(hit_node, current_layer.content_lines.get_child_count()-1) # Raise selected line to top 383 | if event.shift_pressed and can_gui_connect_node: 384 | # Reconnection Start 385 | for from in current_layer._connections.keys(): 386 | var from_connections = current_layer._connections[from] 387 | for to in from_connections.keys(): 388 | var connection = from_connections[to] 389 | if connection.line == hit_node: 390 | _is_connecting = true 391 | _is_dragging_node = false 392 | _current_connection = connection 393 | _on_node_reconnect_begin(current_layer, from, to) 394 | break 395 | if hit_node is FlowChartNode: 396 | current_layer.content_nodes.move_child(hit_node, current_layer.content_nodes.get_child_count()-1) # Raise selected node to top 397 | if event.shift_pressed and can_gui_connect_node: 398 | # Connection start 399 | if _request_connect_from(current_layer, hit_node.name): 400 | _is_connecting = true 401 | _is_dragging_node = false 402 | var line = create_line_instance() 403 | var connection = Connection.new(line, hit_node, null) 404 | current_layer._connect_node(connection) 405 | _current_connection = connection 406 | _current_connection.line.join(_current_connection.get_from_pos(), content_position(event.position)) 407 | accept_event() 408 | if _is_connecting: 409 | clear_selection() 410 | else: 411 | if can_gui_select_node: 412 | select(hit_node) 413 | if not _is_dragging: 414 | # Drag start 415 | _is_dragging = true 416 | for i in _selection.size(): 417 | var selected = _selection[i] 418 | _drag_origins[i] = selected.position 419 | selected.modulate.a = 1.0 420 | _drag_start_pos = event.position 421 | _drag_end_pos = event.position 422 | else: 423 | var was_connecting = _is_connecting 424 | var was_dragging_node = _is_dragging_node 425 | if _current_connection: 426 | # Connection end 427 | var from = _current_connection.from_node.name 428 | var to = hit_node.name if hit_node else null 429 | if hit_node is FlowChartNode and _request_connect_to(current_layer, to) and from != to: 430 | # Connection success 431 | var line 432 | if _current_connection.to_node: 433 | # Reconnection 434 | line = disconnect_node(current_layer, from, _current_connection.to_node.name) 435 | _current_connection.to_node = hit_node 436 | _on_node_reconnect_end(current_layer, from, to) 437 | connect_node(current_layer, from, to, line) 438 | else: 439 | # New Connection 440 | current_layer.content_lines.remove_child(_current_connection.line) 441 | line = _current_connection.line 442 | _current_connection.to_node = hit_node 443 | connect_node(current_layer, from, to, line) 444 | else: 445 | # Connection failed 446 | if _current_connection.to_node: 447 | # Reconnection 448 | _current_connection.join() 449 | _on_node_reconnect_failed(current_layer, from, name) 450 | else: 451 | # New Connection 452 | _current_connection.line.queue_free() 453 | _on_node_connect_failed(current_layer, from) 454 | _is_connecting = false 455 | _current_connection = null 456 | accept_event() 457 | 458 | if _is_dragging: 459 | # Drag end 460 | _is_dragging = false 461 | _is_dragging_node = false 462 | if not (was_connecting or was_dragging_node) and can_gui_select_node: 463 | var selection_box_rect = get_selection_box_rect() 464 | # Select node 465 | for node in current_layer.content_nodes.get_children(): 466 | var rect = get_transform() * (content.get_transform() * (node.get_rect())) 467 | if selection_box_rect.intersects(rect): 468 | if node is FlowChartNode: 469 | select(node) 470 | # Select line 471 | var connection_list = get_connection_list() 472 | for i in connection_list.size(): 473 | var connection = current_layer._connections[connection_list[i].from][connection_list[i].to] 474 | # Line's offset along its down-vector 475 | var line_local_up_offset = connection.line.position - connection.line.get_transform() * (Vector2.UP * connection.offset) 476 | var from_pos = content.get_transform() * (connection.get_from_pos() + line_local_up_offset) 477 | var to_pos = content.get_transform() * (connection.get_to_pos() + line_local_up_offset) 478 | if CohenSutherland.line_intersect_rectangle(from_pos, to_pos, selection_box_rect): 479 | select(connection.line) 480 | if was_dragging_node: 481 | # Update _drag_origins with new position after dragged 482 | for i in _selection.size(): 483 | var selected = _selection[i] 484 | _drag_origins[i] = selected.position 485 | selected.modulate.a = 1.0 486 | _drag_start_pos = _drag_end_pos 487 | queue_redraw() 488 | 489 | # Get selection box rect 490 | func get_selection_box_rect(): 491 | var pos = Vector2(min(_drag_start_pos.x, _drag_end_pos.x), min(_drag_start_pos.y, _drag_end_pos.y)) 492 | var size = (_drag_end_pos - _drag_start_pos).abs() 493 | return Rect2(pos, size) 494 | 495 | # Get required scroll rect base on content 496 | func get_scroll_rect(layer=current_layer, force_scroll_margin=null): 497 | var _scroll_margin = scroll_margin 498 | if force_scroll_margin!=null: 499 | _scroll_margin = force_scroll_margin 500 | return layer.get_scroll_rect(_scroll_margin) 501 | 502 | func add_layer_to(target): 503 | var layer = create_layer_instance() 504 | target.add_child(layer) 505 | return layer 506 | 507 | func get_layer(np): 508 | return content.get_node_or_null(NodePath(np)) 509 | 510 | func select_layer_at(i): 511 | select_layer(content.get_child(i)) 512 | 513 | func select_layer(layer): 514 | var prev_layer = current_layer 515 | _on_layer_deselected(prev_layer) 516 | current_layer = layer 517 | _on_layer_selected(layer) 518 | 519 | # Add node 520 | func add_node(layer, node): 521 | layer.add_node(node) 522 | _on_node_added(layer, node) 523 | 524 | # Remove node 525 | func remove_node(layer, node_name): 526 | var node = layer.content_nodes.get_node_or_null(NodePath(node_name)) 527 | if node: 528 | deselect(node) # Must deselct before remove to make sure _drag_origins synced with _selections 529 | layer.remove_node(node) 530 | _on_node_removed(layer, node_name) 531 | 532 | # Called after connection established 533 | func _connect_node(line, from_pos, to_pos): 534 | pass 535 | 536 | # Called after connection broken 537 | func _disconnect_node(line): 538 | if line in _selection: 539 | deselect(line) 540 | 541 | func create_layer_instance(): 542 | var layer = Control.new() 543 | layer.set_script(FlowChartLayer) 544 | return layer 545 | 546 | # Return new line instance to use, called when connecting node 547 | func create_line_instance(): 548 | return FlowChartLineScene.instantiate() 549 | 550 | # Rename node 551 | func rename_node(layer, old, new): 552 | layer.rename_node(old, new) 553 | 554 | # Connect two nodes with a line 555 | func connect_node(layer, from, to, line=null): 556 | if not line: 557 | line = create_line_instance() 558 | line.name = "%s>%s" % [from, to] # "From>To" 559 | layer.connect_node(line, from, to, interconnection_offset) 560 | _on_node_connected(layer, from, to) 561 | emit_signal("connection", from, to, line) 562 | 563 | # Break a connection between two node 564 | func disconnect_node(layer, from, to): 565 | var line = layer.disconnect_node(from, to) 566 | deselect(line) # Since line is selectable as well 567 | _on_node_disconnected(layer, from, to) 568 | emit_signal("disconnection", from, to) 569 | return line 570 | 571 | # Clear all connections 572 | func clear_connections(layer=current_layer): 573 | layer.clear_connections() 574 | 575 | # Select a node(can be a line) 576 | func select(node): 577 | if node in _selection: 578 | return 579 | 580 | _selection.append(node) 581 | node.selected = true 582 | _drag_origins.append(node.position) 583 | emit_signal("node_selected", node) 584 | 585 | # Deselect a node 586 | func deselect(node): 587 | _selection.erase(node) 588 | if is_instance_valid(node): 589 | node.selected = false 590 | _drag_origins.pop_back() 591 | emit_signal("node_deselected", node) 592 | 593 | # Clear all selection 594 | func clear_selection(): 595 | for node in _selection.duplicate(): # duplicate _selection array as deselect() edit array 596 | if not node: 597 | continue 598 | deselect(node) 599 | _selection.clear() 600 | 601 | # Duplicate given nodes in editor 602 | func duplicate_nodes(layer, nodes): 603 | clear_selection() 604 | var new_nodes = [] 605 | for i in nodes.size(): 606 | var node = nodes[i] 607 | if not (node is FlowChartNode): 608 | continue 609 | var new_node = node.duplicate(DUPLICATE_SIGNALS + DUPLICATE_SCRIPTS) 610 | var offset = content_position(get_local_mouse_position()) - content_position(_drag_end_pos) 611 | new_node.position = new_node.position + offset 612 | new_nodes.append(new_node) 613 | add_node(layer, new_node) 614 | select(new_node) 615 | # Duplicate connection within selection 616 | for i in nodes.size(): 617 | var from_node = nodes[i] 618 | for connection_pair in get_connection_list(): 619 | if from_node.name == connection_pair.from: 620 | for j in nodes.size(): 621 | var to_node = nodes[j] 622 | if to_node.name == connection_pair.to: 623 | connect_node(layer, new_nodes[i].name, new_nodes[j].name) 624 | _on_duplicated(layer, nodes, new_nodes) 625 | 626 | # Called after layer selected(current_layer changed) 627 | func _on_layer_selected(layer): 628 | pass 629 | 630 | func _on_layer_deselected(layer): 631 | pass 632 | 633 | # Called after a node added 634 | func _on_node_added(layer, node): 635 | pass 636 | 637 | # Called after a node removed 638 | func _on_node_removed(layer, node): 639 | pass 640 | 641 | # Called when a node dragged 642 | func _on_node_dragged(layer, node, dragged): 643 | pass 644 | 645 | # Called when connection established between two nodes 646 | func _on_node_connected(layer, from, to): 647 | pass 648 | 649 | # Called when connection broken 650 | func _on_node_disconnected(layer, from, to): 651 | pass 652 | 653 | func _on_node_connect_failed(layer, from): 654 | pass 655 | 656 | func _on_node_reconnect_begin(layer, from, to): 657 | pass 658 | 659 | func _on_node_reconnect_end(layer, from, to): 660 | pass 661 | 662 | func _on_node_reconnect_failed(layer, from, to): 663 | pass 664 | 665 | func _request_connect_from(layer, from): 666 | return true 667 | 668 | func _request_connect_to(layer, to): 669 | return true 670 | 671 | # Called when nodes duplicated 672 | func _on_duplicated(layer, old_nodes, new_nodes): 673 | pass 674 | 675 | # Convert position in FlowChart space to content(takes translation/scale of content into account) 676 | func content_position(pos): 677 | return (pos - content.position - content.pivot_offset * (Vector2.ONE - content.scale)) * 1.0/content.scale 678 | 679 | # Return array of dictionary of connection as such [{"from1": "to1"}, {"from2": "to2"}] 680 | func get_connection_list(layer=current_layer): 681 | return layer.get_connection_list() 682 | -------------------------------------------------------------------------------- /addons/imjp94.yafsm/scenes/StateMachineEditor.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "res://addons/imjp94.yafsm/scenes/flowchart/FlowChart.gd" 3 | 4 | const StateMachine = preload("../src/states/StateMachine.gd") 5 | const Transition = preload("../src/transitions/Transition.gd") 6 | const State = preload("../src/states/State.gd") 7 | const StateDirectory = preload("../src/StateDirectory.gd") 8 | const StateNode = preload("state_nodes/StateNode.tscn") 9 | const TransitionLine = preload("transition_editors/TransitionLine.tscn") 10 | const StateNodeScript = preload("state_nodes/StateNode.gd") 11 | const StateMachineEditorLayer = preload("StateMachineEditorLayer.gd") 12 | const PathViewer = preload("PathViewer.gd") 13 | 14 | signal inspector_changed(property) # Inform plugin to refresh inspector 15 | signal debug_mode_changed(new_debug_mode) 16 | 17 | const ENTRY_STATE_MISSING_MSG = { 18 | "key": "entry_state_missing", 19 | "text": "Entry State missing, it will never get started. Right-click -> \"Add Entry\"." 20 | } 21 | const EXIT_STATE_MISSING_MSG = { 22 | "key": "exit_state_missing", 23 | "text": "Exit State missing, it will never exit from nested state. Right-click -> \"Add Exit\"." 24 | } 25 | const DEBUG_MODE_MSG = { 26 | "key": "debug_mode", 27 | "text": "Debug Mode" 28 | } 29 | 30 | @onready var context_menu = $ContextMenu 31 | @onready var state_node_context_menu = $StateNodeContextMenu 32 | @onready var convert_to_state_confirmation = $ConvertToStateConfirmation 33 | @onready var save_dialog = $SaveDialog 34 | @onready var create_new_state_machine_container = $MarginContainer 35 | @onready var create_new_state_machine = $MarginContainer/CreateNewStateMachine 36 | @onready var param_panel = $ParametersPanel 37 | var path_viewer = HBoxContainer.new() 38 | var condition_visibility = TextureButton.new() 39 | var unsaved_indicator = Label.new() 40 | var message_box = VBoxContainer.new() 41 | 42 | var editor_accent_color = Color.WHITE 43 | var transition_arrow_icon 44 | 45 | var undo_redo: EditorUndoRedoManager 46 | 47 | var debug_mode: = false: 48 | set = set_debug_mode 49 | var state_machine_player: 50 | set = set_state_machine_player 51 | var state_machine: 52 | set = set_state_machine 53 | var can_gui_name_edit = true 54 | var can_gui_context_menu = true 55 | 56 | var _reconnecting_connection 57 | var _last_index = 0 58 | var _last_path = "" 59 | var _message_box_dict = {} 60 | var _context_node 61 | var _current_state = "" 62 | var _last_stack = [] 63 | 64 | 65 | func _init(): 66 | super._init() 67 | 68 | path_viewer.mouse_filter = MOUSE_FILTER_IGNORE 69 | path_viewer.set_script(PathViewer) 70 | path_viewer.dir_pressed.connect(_on_path_viewer_dir_pressed) 71 | top_bar.add_child(path_viewer) 72 | 73 | condition_visibility.tooltip_text = "Hide/Show Conditions on Transition Line" 74 | condition_visibility.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT_CENTERED 75 | condition_visibility.toggle_mode = true 76 | condition_visibility.size_flags_vertical = SIZE_SHRINK_CENTER 77 | condition_visibility.focus_mode = FOCUS_NONE 78 | condition_visibility.pressed.connect(_on_condition_visibility_pressed) 79 | condition_visibility.button_pressed = true 80 | gadget.add_child(condition_visibility) 81 | 82 | unsaved_indicator.size_flags_vertical = SIZE_SHRINK_CENTER 83 | unsaved_indicator.focus_mode = FOCUS_NONE 84 | gadget.add_child(unsaved_indicator) 85 | 86 | message_box.set_anchors_and_offsets_preset(PRESET_BOTTOM_WIDE) 87 | message_box.grow_vertical = GROW_DIRECTION_BEGIN 88 | add_child(message_box) 89 | 90 | content.get_child(0).name = "root" 91 | 92 | set_process(false) 93 | 94 | func _ready(): 95 | create_new_state_machine_container.visible = false 96 | create_new_state_machine.pressed.connect(_on_create_new_state_machine_pressed) 97 | context_menu.index_pressed.connect(_on_context_menu_index_pressed) 98 | state_node_context_menu.index_pressed.connect(_on_state_node_context_menu_index_pressed) 99 | convert_to_state_confirmation.confirmed.connect(_on_convert_to_state_confirmation_confirmed) 100 | save_dialog.confirmed.connect(_on_save_dialog_confirmed) 101 | 102 | func _process(delta): 103 | if not debug_mode: 104 | set_process(false) 105 | return 106 | if not is_instance_valid(state_machine_player): 107 | set_process(false) 108 | set_debug_mode(false) 109 | return 110 | var stack = state_machine_player.get("Members/StackPlayer.gd/stack") 111 | if ((stack == []) or (stack==null)): 112 | set_process(false) 113 | set_debug_mode(false) 114 | return 115 | 116 | if stack.size() == 1: 117 | set_current_state(state_machine_player.get("Members/StackPlayer.gd/current")) 118 | else: 119 | var stack_max_index = stack.size() - 1 120 | var prev_index = stack.find(_current_state) 121 | if prev_index == -1: 122 | if _last_stack.size() < stack.size(): 123 | # Reproduce transition, for example: 124 | # [Entry, Idle, Walk] 125 | # [Entry, Idle, Jump, Fall] 126 | # Walk -> Idle 127 | # Idle -> Jump 128 | # Jump -> Fall 129 | var common_index = -1 130 | for i in _last_stack.size(): 131 | if _last_stack[i] == stack[i]: 132 | common_index = i 133 | break 134 | if common_index > -1: 135 | var count_from_last_stack = _last_stack.size()-1 - common_index -1 136 | _last_stack.reverse() 137 | # Transit back to common state 138 | for i in count_from_last_stack: 139 | set_current_state(_last_stack[i + 1]) 140 | # Transit to all missing state in current stack 141 | for i in range(common_index + 1, stack.size()): 142 | set_current_state(stack[i]) 143 | else: 144 | set_current_state(stack.back()) 145 | else: 146 | set_current_state(stack.back()) 147 | else: 148 | # Set every skipped state 149 | var missing_count = stack_max_index - prev_index 150 | for i in range(1, missing_count + 1): 151 | set_current_state(stack[prev_index + i]) 152 | _last_stack = stack 153 | var params = state_machine_player.get("Members/_parameters") 154 | var local_params = state_machine_player.get("Members/_local_parameters") 155 | if params == null: 156 | params = state_machine_player.get("Members/StateMachinePlayer.gd/_parameters") 157 | local_params = state_machine_player.get("Members/StateMachinePlayer.gd/_local_parameters") 158 | param_panel.update_params(params, local_params) 159 | get_focused_layer(_current_state).debug_update(_current_state, params, local_params) 160 | 161 | func _on_path_viewer_dir_pressed(dir, index): 162 | var path = path_viewer.select_dir(dir) 163 | select_layer(get_layer(path)) 164 | 165 | if _last_index > index: 166 | # Going backward 167 | var end_state_parent_path = StateMachinePlayer.path_backward(_last_path) 168 | var end_state_name = StateMachinePlayer.path_end_dir(_last_path) 169 | var layer = content.get_node_or_null(NodePath(end_state_parent_path)) 170 | if layer: 171 | var node = layer.content_nodes.get_node_or_null(NodePath(end_state_name)) 172 | if node: 173 | var cond_1 = (not ("states" in node.state)) or (node.state.states=={}) # states property not defined or empty 174 | # Now check if, for some reason, there are an Entry and/or an Exit node inside this node 175 | # not registered in the states variable above. 176 | var nested_layer = content.get_node_or_null(NodePath(_last_path)) 177 | var cond_2 = (nested_layer.content_nodes.get_node_or_null(NodePath(State.ENTRY_STATE)) == null) # there is no entry state in the node 178 | var cond_3 = (nested_layer.content_nodes.get_node_or_null(NodePath(State.EXIT_STATE)) == null) # there is no exit state in the node 179 | if (cond_1 and cond_2 and cond_3): 180 | # Convert state machine node back to state node 181 | convert_to_state(layer, node) 182 | 183 | _last_index = index 184 | _last_path = path 185 | 186 | func _on_context_menu_index_pressed(index): 187 | var new_node = StateNode.instantiate() 188 | new_node.theme.get_stylebox("focus", "FlowChartNode").border_color = editor_accent_color 189 | match index: 190 | 0: # Add State 191 | ## Handle state name duplication (4.x changed how duplicates are 192 | ## automatically handled and gave a random index instead of 193 | ## a progressive one) 194 | var default_new_state_name = "State" 195 | var state_dup_index = 0 196 | var new_name = default_new_state_name 197 | for state_name in current_layer.state_machine.states: 198 | if (state_name == new_name): 199 | state_dup_index += 1 200 | new_name = "%s%s" % [default_new_state_name, state_dup_index] 201 | new_node.name = new_name 202 | 1: # Add Entry 203 | if State.ENTRY_STATE in current_layer.state_machine.states: 204 | push_warning("Entry node already exist") 205 | return 206 | new_node.name = State.ENTRY_STATE 207 | 2: # Add Exit 208 | if State.EXIT_STATE in current_layer.state_machine.states: 209 | push_warning("Exit node already exist") 210 | return 211 | new_node.name = State.EXIT_STATE 212 | new_node.position = content_position(get_local_mouse_position()) 213 | add_node(current_layer, new_node) 214 | 215 | func _on_state_node_context_menu_index_pressed(index): 216 | if not _context_node: 217 | return 218 | 219 | match index: 220 | 0: # Copy 221 | _copying_nodes = [_context_node] 222 | _context_node = null 223 | 1: # Duplicate 224 | duplicate_nodes(current_layer, [_context_node]) 225 | _context_node = null 226 | 2: # Delete 227 | remove_node(current_layer, _context_node.name) 228 | for connection_pair in current_layer.get_connection_list(): 229 | if connection_pair.from == _context_node.name or connection_pair.to == _context_node.name: 230 | disconnect_node(current_layer, connection_pair.from, connection_pair.to).queue_free() 231 | _context_node = null 232 | 3: # Separator 233 | _context_node = null 234 | 4: # Convert 235 | convert_to_state_confirmation.popup_centered() 236 | 237 | func _on_convert_to_state_confirmation_confirmed(): 238 | convert_to_state(current_layer, _context_node) 239 | _context_node.queue_redraw() # Update outlook of node 240 | # Remove layer 241 | var path = str(path_viewer.get_cwd(), "/", _context_node.name) 242 | var layer = get_layer(path) 243 | if layer: 244 | layer.queue_free() 245 | _context_node = null 246 | 247 | func _on_save_dialog_confirmed(): 248 | save() 249 | 250 | func _on_create_new_state_machine_pressed(): 251 | var new_state_machine = StateMachine.new() 252 | 253 | undo_redo.create_action("Create New StateMachine") 254 | 255 | undo_redo.add_do_reference(new_state_machine) 256 | undo_redo.add_do_property(state_machine_player, "state_machine", new_state_machine) 257 | undo_redo.add_do_method(self, "set_state_machine", new_state_machine) 258 | undo_redo.add_do_property(create_new_state_machine_container, "visible", false) 259 | undo_redo.add_do_method(self, "check_has_entry") 260 | undo_redo.add_do_method(self, "emit_signal", "inspector_changed", "state_machine") 261 | 262 | undo_redo.add_undo_property(state_machine_player, "state_machine", null) 263 | undo_redo.add_undo_method(self, "set_state_machine", null) 264 | undo_redo.add_undo_property(create_new_state_machine_container, "visible", true) 265 | undo_redo.add_undo_method(self, "check_has_entry") 266 | undo_redo.add_undo_method(self, "emit_signal", "inspector_changed", "state_machine") 267 | 268 | undo_redo.commit_action() 269 | 270 | func _on_condition_visibility_pressed(): 271 | for line in current_layer.content_lines.get_children(): 272 | line.vbox.visible = condition_visibility.button_pressed 273 | 274 | func _on_debug_mode_changed(new_debug_mode): 275 | if new_debug_mode: 276 | param_panel.show() 277 | add_message(DEBUG_MODE_MSG.key, DEBUG_MODE_MSG.text) 278 | set_process(true) 279 | # mouse_filter = MOUSE_FILTER_IGNORE 280 | can_gui_select_node = false 281 | can_gui_delete_node = false 282 | can_gui_connect_node = false 283 | can_gui_name_edit = false 284 | can_gui_context_menu = false 285 | else: 286 | param_panel.clear_params() 287 | param_panel.hide() 288 | remove_message(DEBUG_MODE_MSG.key) 289 | set_process(false) 290 | can_gui_select_node = true 291 | can_gui_delete_node = true 292 | can_gui_connect_node = true 293 | can_gui_name_edit = true 294 | can_gui_context_menu = true 295 | 296 | func _on_state_machine_player_changed(new_state_machine_player): 297 | if not state_machine_player: 298 | return 299 | if new_state_machine_player.get_class() == "EditorDebuggerRemoteObjects": 300 | return 301 | 302 | if new_state_machine_player: 303 | create_new_state_machine_container.visible = !new_state_machine_player.state_machine 304 | else: 305 | create_new_state_machine_container.visible = false 306 | 307 | func _on_state_machine_changed(new_state_machine): 308 | var root_layer = get_layer("root") 309 | path_viewer.select_dir("root") # Before select_layer, so path_viewer will be updated in _on_layer_selected 310 | select_layer(root_layer) 311 | clear_graph(root_layer) 312 | # Reset layers & path viewer 313 | for child in root_layer.get_children(): 314 | if child is FlowChartLayer: 315 | root_layer.remove_child(child) 316 | child.queue_free() 317 | if new_state_machine: 318 | root_layer.state_machine = state_machine 319 | var validated = StateMachine.validate(new_state_machine) 320 | if validated: 321 | print_debug("gd-YAFSM: Corrupted StateMachine Resource fixed, save to apply the fix.") 322 | draw_graph(root_layer) 323 | check_has_entry() 324 | 325 | func _gui_input(event): 326 | super._gui_input(event) 327 | 328 | if event is InputEventMouseButton: 329 | match event.button_index: 330 | MOUSE_BUTTON_RIGHT: 331 | if event.pressed and can_gui_context_menu: 332 | context_menu.set_item_disabled(1, current_layer.state_machine.has_entry()) 333 | context_menu.set_item_disabled(2, current_layer.state_machine.has_exit()) 334 | context_menu.position = get_window().position + Vector2i(get_viewport().get_mouse_position()) 335 | context_menu.popup() 336 | 337 | func _input(event): 338 | # Intercept save action 339 | if visible: 340 | if event is InputEventKey: 341 | match event.keycode: 342 | KEY_S: 343 | if event.ctrl_pressed and event.pressed: 344 | save_request() 345 | 346 | func create_layer(node): 347 | # Create/Move to new layer 348 | var new_state_machine = convert_to_state_machine(current_layer, node) 349 | # Determine current layer path 350 | var parent_path = path_viewer.get_cwd() 351 | var path = str(parent_path, "/", node.name) 352 | var layer = get_layer(path) 353 | path_viewer.add_dir(node.state.name) # Before select_layer, so path_viewer will be updated in _on_layer_selected 354 | if not layer: 355 | # New layer to spawn 356 | layer = add_layer_to(get_layer(parent_path)) 357 | layer.name = node.state.name 358 | layer.state_machine = new_state_machine 359 | draw_graph(layer) 360 | _last_index = path_viewer.get_child_count()-1 361 | _last_path = path 362 | return layer 363 | 364 | func open_layer(path): 365 | var dir = StateDirectory.new(path) 366 | dir.goto(dir.get_end_index()) 367 | dir.back() 368 | var next_layer = get_next_layer(dir, get_layer("root")) 369 | select_layer(next_layer) 370 | return next_layer 371 | 372 | # Recursively get next layer 373 | func get_next_layer(dir, base_layer): 374 | var next_layer = base_layer 375 | var np = dir.next() 376 | if np: 377 | next_layer = base_layer.get_node_or_null(NodePath(np)) 378 | if next_layer: 379 | next_layer = get_next_layer(dir, next_layer) 380 | else: 381 | var to_dir = StateDirectory.new(dir.get_current()) 382 | to_dir.goto(to_dir.get_end_index()) 383 | to_dir.back() 384 | var node = base_layer.content_nodes.get_node_or_null(NodePath(to_dir.get_current_end())) 385 | next_layer = get_next_layer(dir, create_layer(node)) 386 | return next_layer 387 | 388 | func get_focused_layer(state): 389 | var current_dir = StateDirectory.new(state) 390 | current_dir.goto(current_dir.get_end_index()) 391 | current_dir.back() 392 | return get_layer(str("root/", current_dir.get_current())) 393 | 394 | func _on_state_node_gui_input(event, node): 395 | if node.state.is_entry() or node.state.is_exit(): 396 | return 397 | 398 | if event is InputEventMouseButton: 399 | match event.button_index: 400 | MOUSE_BUTTON_LEFT: 401 | if event.pressed: 402 | if event.double_click: 403 | if node.name_edit.get_rect().has_point(event.position) and can_gui_name_edit: 404 | # Edit State name if within LineEdit 405 | node.enable_name_edit(true) 406 | accept_event() 407 | else: 408 | var layer = create_layer(node) 409 | select_layer(layer) 410 | accept_event() 411 | MOUSE_BUTTON_RIGHT: 412 | if event.pressed: 413 | # State node context menu 414 | _context_node = node 415 | state_node_context_menu.position = get_window().position + Vector2i(get_viewport().get_mouse_position()) 416 | state_node_context_menu.popup() 417 | state_node_context_menu.set_item_disabled(4, not (node.state is StateMachine)) 418 | accept_event() 419 | 420 | func convert_to_state_machine(layer, node): 421 | # Convert State to StateMachine 422 | var new_state_machine 423 | if node.state is StateMachine: 424 | new_state_machine = node.state 425 | else: 426 | new_state_machine = StateMachine.new() 427 | new_state_machine.name = node.state.name 428 | new_state_machine.graph_offset = node.state.graph_offset 429 | layer.state_machine.remove_state(node.state.name) 430 | layer.state_machine.add_state(new_state_machine) 431 | node.state = new_state_machine 432 | return new_state_machine 433 | 434 | func convert_to_state(layer, node): 435 | # Convert StateMachine to State 436 | var new_state 437 | if node.state is StateMachine: 438 | new_state = State.new() 439 | new_state.name = node.state.name 440 | new_state.graph_offset = node.state.graph_offset 441 | layer.state_machine.remove_state(node.state.name) 442 | layer.state_machine.add_state(new_state) 443 | node.state = new_state 444 | else: 445 | new_state = node.state 446 | return new_state 447 | 448 | func create_layer_instance(): 449 | var layer = Control.new() 450 | layer.set_script(StateMachineEditorLayer) 451 | layer.editor_accent_color = editor_accent_color 452 | return layer 453 | 454 | func create_line_instance(): 455 | var line = TransitionLine.instantiate() 456 | line.theme.get_stylebox("focus", "FlowChartLine").shadow_color = editor_accent_color 457 | line.theme.set_icon("arrow", "FlowChartLine", transition_arrow_icon) 458 | return line 459 | 460 | # Request to save current editing StateMachine 461 | func save_request(): 462 | if not can_save(): 463 | return 464 | 465 | save_dialog.dialog_text = "Saving StateMachine to %s" % state_machine.resource_path 466 | save_dialog.popup_centered() 467 | 468 | # Save current editing StateMachine 469 | func save(): 470 | if not can_save(): 471 | return 472 | 473 | unsaved_indicator.text = "" 474 | ResourceSaver.save(state_machine, state_machine.resource_path) 475 | 476 | # Clear editor 477 | func clear_graph(layer): 478 | clear_connections() 479 | 480 | for child in layer.content_nodes.get_children(): 481 | if child is StateNodeScript: 482 | layer.content_nodes.remove_child(child) 483 | child.queue_free() 484 | 485 | queue_redraw() 486 | unsaved_indicator.text = "" # Clear graph is not action by user 487 | 488 | # Intialize editor with current editing StateMachine 489 | func draw_graph(layer): 490 | for state_key in layer.state_machine.states.keys(): 491 | var state = layer.state_machine.states[state_key] 492 | var new_node = StateNode.instantiate() 493 | new_node.theme.get_stylebox("focus", "FlowChartNode").border_color = editor_accent_color 494 | new_node.name = state_key # Set before add_node to let engine handle duplicate name 495 | add_node(layer, new_node) 496 | # Set after add_node to make sure UIs are initialized 497 | new_node.state = state 498 | new_node.state.name = state_key 499 | new_node.position = state.graph_offset 500 | for state_key in layer.state_machine.states.keys(): 501 | var from_transitions = layer.state_machine.transitions.get(state_key) 502 | if from_transitions: 503 | for transition in from_transitions.values(): 504 | connect_node(layer, transition.from, transition.to) 505 | layer._connections[transition.from][transition.to].line.transition = transition 506 | queue_redraw() 507 | unsaved_indicator.text = "" # Draw graph is not action by user 508 | 509 | # Add message to message_box(overlay text at bottom of editor) 510 | func add_message(key, text): 511 | var label = Label.new() 512 | label.text = text 513 | _message_box_dict[key] = label 514 | message_box.add_child(label) 515 | return label 516 | 517 | # Remove message from message_box 518 | func remove_message(key): 519 | var control = _message_box_dict.get(key) 520 | if control: 521 | _message_box_dict.erase(key) 522 | message_box.remove_child(control) 523 | # Weird behavior of VBoxContainer, only sort children properly after changing grow_direction 524 | message_box.grow_vertical = GROW_DIRECTION_END 525 | message_box.grow_vertical = GROW_DIRECTION_BEGIN 526 | return true 527 | return false 528 | 529 | # Check if current editing StateMachine has entry, warns user if entry state missing 530 | func check_has_entry(): 531 | if not current_layer.state_machine: 532 | return 533 | if not current_layer.state_machine.has_entry(): 534 | if not (ENTRY_STATE_MISSING_MSG.key in _message_box_dict): 535 | add_message(ENTRY_STATE_MISSING_MSG.key, ENTRY_STATE_MISSING_MSG.text) 536 | else: 537 | if ENTRY_STATE_MISSING_MSG.key in _message_box_dict: 538 | remove_message(ENTRY_STATE_MISSING_MSG.key) 539 | 540 | # Check if current editing StateMachine is nested and has exit, warns user if exit state missing 541 | func check_has_exit(): 542 | if not current_layer.state_machine: 543 | return 544 | if not path_viewer.get_cwd() == "root": # Nested state 545 | if not current_layer.state_machine.has_exit(): 546 | if not (EXIT_STATE_MISSING_MSG.key in _message_box_dict): 547 | add_message(EXIT_STATE_MISSING_MSG.key, EXIT_STATE_MISSING_MSG.text) 548 | return 549 | if EXIT_STATE_MISSING_MSG.key in _message_box_dict: 550 | remove_message(EXIT_STATE_MISSING_MSG.key) 551 | 552 | func _on_layer_selected(layer): 553 | if layer: 554 | layer.show_content() 555 | check_has_entry() 556 | check_has_exit() 557 | 558 | func _on_layer_deselected(layer): 559 | if layer: 560 | layer.hide_content() 561 | 562 | func _on_node_dragged(layer, node, dragged): 563 | node.state.graph_offset = node.position 564 | _on_edited() 565 | 566 | func _on_node_added(layer, new_node): 567 | # Godot 4 duplicates node with an internal @ name, which breaks everything 568 | while String(new_node.name).begins_with("@"): 569 | new_node.name = String(new_node.name).lstrip("@") 570 | 571 | new_node.undo_redo = undo_redo 572 | new_node.state.name = new_node.name 573 | new_node.state.graph_offset = new_node.position 574 | new_node.name_edit_entered.connect(_on_node_name_edit_entered.bind(new_node)) 575 | new_node.gui_input.connect(_on_state_node_gui_input.bind(new_node)) 576 | layer.state_machine.add_state(new_node.state) 577 | check_has_entry() 578 | check_has_exit() 579 | _on_edited() 580 | 581 | func _on_node_removed(layer, node_name): 582 | var path = str(path_viewer.get_cwd(), "/", node_name) 583 | var layer_to_remove = get_layer(path) 584 | if layer_to_remove: 585 | layer_to_remove.get_parent().remove_child(layer_to_remove) 586 | layer_to_remove.queue_free() 587 | var result = layer.state_machine.remove_state(node_name) 588 | check_has_entry() 589 | check_has_exit() 590 | _on_edited() 591 | return result 592 | 593 | func _on_node_connected(layer, from, to): 594 | if _reconnecting_connection: 595 | # Reconnection will trigger _on_node_connected after _on_node_reconnect_end/_on_node_reconnect_failed 596 | if is_instance_valid(_reconnecting_connection.from_node) and \ 597 | _reconnecting_connection.from_node.name == from and \ 598 | is_instance_valid(_reconnecting_connection.to_node) and \ 599 | _reconnecting_connection.to_node.name == to: 600 | _reconnecting_connection = null 601 | return 602 | if layer.state_machine.transitions.has(from): 603 | if layer.state_machine.transitions[from].has(to): 604 | return # Already existed as it is loaded from file 605 | 606 | var line = layer._connections[from][to].line 607 | var new_transition = Transition.new(from, to) 608 | line.transition = new_transition 609 | layer.state_machine.add_transition(new_transition) 610 | clear_selection() 611 | select(line) 612 | _on_edited() 613 | 614 | func _on_node_disconnected(layer, from, to): 615 | layer.state_machine.remove_transition(from, to) 616 | _on_edited() 617 | 618 | func _on_node_reconnect_begin(layer, from, to): 619 | _reconnecting_connection = layer._connections[from][to] 620 | layer.state_machine.remove_transition(from, to) 621 | 622 | func _on_node_reconnect_end(layer, from, to): 623 | var transition = _reconnecting_connection.line.transition 624 | transition.to = to 625 | layer.state_machine.add_transition(transition) 626 | clear_selection() 627 | select(_reconnecting_connection.line) 628 | 629 | func _on_node_reconnect_failed(layer, from, to): 630 | var transition = _reconnecting_connection.line.transition 631 | layer.state_machine.add_transition(transition) 632 | clear_selection() 633 | select(_reconnecting_connection.line) 634 | 635 | func _request_connect_from(layer, from): 636 | if from == State.EXIT_STATE: 637 | return false 638 | return true 639 | 640 | func _request_connect_to(layer, to): 641 | if to == State.ENTRY_STATE: 642 | return false 643 | return true 644 | 645 | func _on_duplicated(layer, old_nodes, new_nodes): 646 | # Duplicate condition as well 647 | for i in old_nodes.size(): 648 | var from_node = old_nodes[i] 649 | for connection_pair in get_connection_list(): 650 | if from_node.name == connection_pair.from: 651 | for j in old_nodes.size(): 652 | var to_node = old_nodes[j] 653 | if to_node.name == connection_pair.to: 654 | var old_connection = layer._connections[connection_pair.from][connection_pair.to] 655 | var new_connection = layer._connections[new_nodes[i].name][new_nodes[j].name] 656 | for condition in old_connection.line.transition.conditions.values(): 657 | new_connection.line.transition.add_condition(condition.duplicate()) 658 | _on_edited() 659 | 660 | func _on_node_name_edit_entered(new_name, node): 661 | var old = node.state.name 662 | var new = new_name 663 | if old == new: 664 | return 665 | if "/" in new or "\\" in new: # No back/forward-slash 666 | push_warning("Illegal State Name: / and \\ are not allowed in State name(%s)" % new) 667 | node.name_edit.text = old 668 | return 669 | 670 | if current_layer.state_machine.change_state_name(old, new): 671 | rename_node(current_layer, node.name, new) 672 | node.name = new 673 | # Rename layer as well 674 | var path = str(path_viewer.get_cwd(), "/", node.name) 675 | var layer = get_layer(path) 676 | if layer: 677 | layer.name = new 678 | for child in path_viewer.get_children(): 679 | if child.text == old: 680 | child.text = new 681 | break 682 | _on_edited() 683 | else: 684 | node.name_edit.text = old 685 | 686 | func _on_edited(): 687 | unsaved_indicator.text = "*" 688 | 689 | func _on_remote_transited(from, to): 690 | var from_dir = StateDirectory.new(from) 691 | var to_dir = StateDirectory.new(to) 692 | var focused_layer = get_focused_layer(from) 693 | if from: 694 | if focused_layer: 695 | focused_layer.debug_transit_out(from, to) 696 | if to: 697 | if from_dir.is_nested() and from_dir.is_exit(): 698 | if focused_layer: 699 | var path = path_viewer.back() 700 | select_layer(get_layer(path)) 701 | elif to_dir.is_nested(): 702 | if to_dir.is_entry() and focused_layer: 703 | # Open into next layer 704 | to_dir.goto(to_dir.get_end_index()) 705 | to_dir.back() 706 | var node = focused_layer.content_nodes.get_node_or_null(NodePath(to_dir.get_current_end())) 707 | if node: 708 | var layer = create_layer(node) 709 | select_layer(layer) 710 | # In case where, "from" state is nested yet not an exit state, 711 | # while "to" state is on different level, then jump to destination layer directly. 712 | # This happens when StateMachinePlayer transit to state that existing in the stack, 713 | # which trigger StackPlayer.reset() and cause multiple states removed from stack within one frame 714 | elif from_dir.is_nested() and not from_dir.is_exit(): 715 | if to_dir._dirs.size() != from_dir._dirs.size(): 716 | to_dir.goto(to_dir.get_end_index()) 717 | var n = to_dir.back() 718 | if not n: 719 | n = "root" 720 | var layer = get_layer(n) 721 | path_viewer.select_dir(layer.name) 722 | select_layer(layer) 723 | 724 | focused_layer = get_focused_layer(to) 725 | if not focused_layer: 726 | focused_layer = open_layer(to) 727 | focused_layer.debug_transit_in(from, to) 728 | 729 | # Return if current editing StateMachine can be saved, ignore built-in resource 730 | func can_save(): 731 | if not state_machine: 732 | return false 733 | var resource_path = state_machine.resource_path 734 | if resource_path.is_empty(): 735 | return false 736 | if ".scn" in resource_path or ".tscn" in resource_path: # Built-in resource will be saved by scene 737 | return false 738 | return true 739 | 740 | func set_debug_mode(v): 741 | if debug_mode != v: 742 | debug_mode = v 743 | _on_debug_mode_changed(v) 744 | emit_signal("debug_mode_changed", debug_mode) 745 | 746 | func set_state_machine_player(smp): 747 | if state_machine_player != smp: 748 | state_machine_player = smp 749 | _on_state_machine_player_changed(smp) 750 | 751 | func set_state_machine(sm): 752 | if state_machine != sm: 753 | state_machine = sm 754 | _on_state_machine_changed(sm) 755 | 756 | func set_current_state(v): 757 | if _current_state != v: 758 | var from = _current_state 759 | var to = v 760 | _current_state = v 761 | _on_remote_transited(from, to) 762 | --------------------------------------------------------------------------------