└── 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) 
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) 
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`) 
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 |
--------------------------------------------------------------------------------