citylimits/addons/simple-state/classes/debugger.gd
Tony Bark c980445340 Major clean up and reorganization
- Upgraded to Godot 4
- Just remembered the basic principles are based on a tile editor, and dramatically simplified from there. Derp.
- New state machine and license display add-ons.
- Re-licensed under the GPL because Micropolis' assets aren't under a separate one.
2023-03-14 06:17:27 -04:00

139 lines
3.6 KiB
GDScript

@icon("../icons/state_machine_debugger.png")
class_name StateMachineDebugger
extends Tree
## Displays an interactive state tree.
# This source code is a mess, I'm trying to make it less so.
@export
## Root state machine to reference.
var state_machine_root : State:
set(value):
state_machine_root = value
_setup_tree()
@export
## What color to make the item when a state is active.
var active_color := Color.FOREST_GREEN
@export
## Forcefully switch states by double-clicking them.
## Due to its nature, it has the potential to be destructive
## and/ or not behave completely how one might expect.
var allow_state_switching := false:
set(value):
allow_state_switching = value
if allow_state_switching:
item_activated.connect(_on_item_activated)
else:
item_activated.disconnect(_on_item_activated)
@export_group("Signals", "signal_")
@export
## Show when a state emits a relevant signal.
var signal_show := false:
set(value):
signal_show = value
if state_machine_root == null:
return
if signal_show:
connect_signals()
else:
disconnect_signals()
@export
## Which signals to connect to on each state, as long as they exist.
var signal_connections : Array[StringName] = [
&"entered",
&"exited",
&"choose_new_substate_requested",
&"animation_finished",
]
@export
## Delay before hiding signal.
var signal_hide_delay := 1.0
func _init() -> void:
columns = 2
func change_state_by_path(path: NodePath) -> void:
if not state_machine_root.has_node(path):
return
var state := state_machine_root
for i in path.get_name_count():
var part := path.get_name(i)
state = await state.change_state_name(part)
func connect_signals(state := state_machine_root) -> void:
if not state.has_meta(&"tree_item"):
return
for signal_name in signal_connections:
if state.has_signal(signal_name) and not \
state.is_connected(signal_name, _on_state_signal):
state.connect(signal_name, _on_state_signal.bind(signal_name, state.get_meta(&"tree_item")))
for child in(state.get_children() as Array[State]):
connect_signals(child)
func disconnect_signals(state := state_machine_root) -> void:
for signal_name in signal_connections:
if state.has_signal(signal_name) and \
state.is_connected(signal_name, _on_state_signal):
state.disconnect(signal_name, _on_state_signal)
for child in (state.get_children() as Array[State]):
disconnect_signals(child)
func _setup_tree(state := state_machine_root, parent_item: TreeItem = null) -> void:
if state == state_machine_root:
if get_root() != null:
disconnect_signals()
clear()
if state_machine_root == null:
return
# state.print_tree_pretty()
# TODO: add icons
var item := create_item(parent_item)
item.set_text(0, state.name)
item.set_metadata(0, state)
state.set_meta(&"tree_item", item)
connect_signals(state)
for child in (state.get_children() as Array[State]):
_setup_tree(child, item)
func _on_item_activated() -> void:
change_state_by_path(state_machine_root.get_path_to(
get_selected().get_metadata(0) as State))
func _on_state_signal(signal_name: StringName, state_item: TreeItem) -> void:
match signal_name:
&"entered":
for i in columns:
state_item.set_custom_color(i, active_color)
&"exited":
for i in columns:
state_item.clear_custom_color(i)
if not signal_show:
return
state_item.set_text(1, signal_name)
var timer := state_item.get_metadata(1) as SceneTreeTimer
if timer != null:
timer.timeout.disconnect(state_item.set_text)
timer = get_tree().create_timer(signal_hide_delay)
timer.timeout.connect(state_item.set_text.bind(1, ""))
state_item.set_metadata(1, timer)