Add cap sprite to child scene and update project configuration
104
addons/guide/debugger/guide_debugger.gd
Normal file
@@ -0,0 +1,104 @@
|
||||
extends MarginContainer
|
||||
|
||||
@onready var _actions:Container = %Actions
|
||||
@onready var _inputs:Container = %Inputs
|
||||
@onready var _priorities:Container = %Priorities
|
||||
@onready var _formatter:GUIDEInputFormatter = GUIDEInputFormatter.for_active_contexts()
|
||||
|
||||
|
||||
func _ready():
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
GUIDE.input_mappings_changed.connect(_update_priorities)
|
||||
_update_priorities()
|
||||
|
||||
func _process(delta):
|
||||
if not is_visible_in_tree():
|
||||
return
|
||||
|
||||
var index:int = 0
|
||||
for mapping in GUIDE._active_action_mappings:
|
||||
var action:GUIDEAction = mapping.action
|
||||
|
||||
var action_name:String = action.name
|
||||
if action_name == "":
|
||||
action_name = action._editor_name()
|
||||
|
||||
var action_state:String = ""
|
||||
match(action._last_state):
|
||||
GUIDEAction.GUIDEActionState.COMPLETED:
|
||||
action_state = "Completed"
|
||||
GUIDEAction.GUIDEActionState.ONGOING:
|
||||
action_state = "Ongoing"
|
||||
GUIDEAction.GUIDEActionState.TRIGGERED:
|
||||
action_state = "Triggered"
|
||||
|
||||
var action_value:String = ""
|
||||
match(action.action_value_type):
|
||||
GUIDEAction.GUIDEActionValueType.BOOL:
|
||||
action_value = str(action.value_bool)
|
||||
GUIDEAction.GUIDEActionValueType.AXIS_1D:
|
||||
action_value = str(action.value_axis_1d)
|
||||
GUIDEAction.GUIDEActionValueType.AXIS_2D:
|
||||
action_value = str(action.value_axis_2d)
|
||||
GUIDEAction.GUIDEActionValueType.AXIS_3D:
|
||||
action_value = str(action.value_axis_3d)
|
||||
|
||||
|
||||
|
||||
|
||||
var label := _get_label(_actions, index)
|
||||
label.text = "[%s] %s - %s" % [action_name, action_state, action_value]
|
||||
|
||||
index += 1
|
||||
|
||||
# Clean out all labels we don't need anymore
|
||||
_cleanup(_actions, index)
|
||||
|
||||
index = 0
|
||||
for input in GUIDE._active_inputs:
|
||||
var input_label = _formatter.input_as_text(input, false)
|
||||
var input_value:String = str(input._value)
|
||||
|
||||
var label := _get_label(_inputs, index)
|
||||
label.text = "%s - %s" % [input_label, input_value]
|
||||
index += 1
|
||||
|
||||
_cleanup(_inputs, index)
|
||||
|
||||
|
||||
func _get_label(container:Container, index:int) -> Label:
|
||||
var label:Label = null
|
||||
if container.get_child_count() > index:
|
||||
# reuse existing label
|
||||
label = container.get_child(index)
|
||||
else:
|
||||
# make a new one
|
||||
label = Label.new()
|
||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
container.add_child(label)
|
||||
return label
|
||||
|
||||
func _cleanup(container:Container, index:int) -> void:
|
||||
while container.get_child_count() > index:
|
||||
var to_free = container.get_child(index)
|
||||
container.remove_child(to_free)
|
||||
to_free.queue_free()
|
||||
|
||||
func _update_priorities():
|
||||
# since we don't update these per frame, we can just clear them out and
|
||||
# rebuild them when mapping contexts change
|
||||
_cleanup(_priorities, 0)
|
||||
|
||||
for mapping:GUIDEActionMapping in GUIDE._active_action_mappings:
|
||||
var action := mapping.action
|
||||
if GUIDE._actions_sharing_input.has(action):
|
||||
var label := Label.new()
|
||||
var names = ", ".join(GUIDE._actions_sharing_input[action].map(func(it): return it._editor_name()))
|
||||
label.text = "[%s] > [%s]" % [action._editor_name(), names]
|
||||
_priorities.add_child(label)
|
||||
|
||||
|
||||
if _priorities.get_child_count() == 0:
|
||||
var label := Label.new()
|
||||
label.text = "<no overlapping input>"
|
||||
_priorities.add_child(label)
|
50
addons/guide/debugger/guide_debugger.tscn
Normal file
@@ -0,0 +1,50 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dkr80d2pi0d41"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/guide/debugger/guide_debugger.gd" id="1_ckdvj"]
|
||||
|
||||
[node name="GuideDebugger" type="MarginContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1_ckdvj")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "G.U.I.D.E - Debugger"
|
||||
|
||||
[node name="Label2" type="Label" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "Actions"
|
||||
|
||||
[node name="Actions" type="VFlowContainer" parent="VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Label3" type="Label" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "Inputs"
|
||||
|
||||
[node name="Inputs" type="VFlowContainer" parent="VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Label4" type="Label" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "Action Priority"
|
||||
|
||||
[node name="Priorities" type="VFlowContainer" parent="VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
mouse_filter = 2
|
@@ -0,0 +1,138 @@
|
||||
@tool
|
||||
extends MarginContainer
|
||||
|
||||
const ActionSlot = preload("../action_slot/action_slot.gd")
|
||||
const Utils = preload("../utils.gd")
|
||||
const ArrayEdit = preload("../array_edit/array_edit.gd")
|
||||
|
||||
signal delete_requested()
|
||||
signal duplicate_requested()
|
||||
|
||||
@export var input_mapping_editor_scene:PackedScene
|
||||
@onready var _action_slot:ActionSlot = %ActionSlot
|
||||
@onready var _input_mappings:ArrayEdit = %InputMappings
|
||||
|
||||
const ClassScanner = preload("../class_scanner.gd")
|
||||
|
||||
var _plugin:EditorPlugin
|
||||
var _scanner:ClassScanner
|
||||
var _undo_redo:EditorUndoRedoManager
|
||||
|
||||
var _mapping:GUIDEActionMapping
|
||||
|
||||
func _ready():
|
||||
_action_slot.action_changed.connect(_on_action_changed)
|
||||
_input_mappings.delete_requested.connect(_on_input_mapping_delete_requested)
|
||||
_input_mappings.add_requested.connect(_on_input_mappings_add_requested)
|
||||
_input_mappings.move_requested.connect(_on_input_mappings_move_requested)
|
||||
_input_mappings.clear_requested.connect(_on_input_mappings_clear_requested)
|
||||
_input_mappings.duplicate_requested.connect(_on_input_mappings_duplicate_requested)
|
||||
_input_mappings.collapse_state_changed.connect(_on_input_mappings_collapse_state_changed)
|
||||
|
||||
func initialize(plugin:EditorPlugin, scanner:ClassScanner):
|
||||
_plugin = plugin
|
||||
_scanner = scanner
|
||||
_undo_redo = _plugin.get_undo_redo()
|
||||
|
||||
|
||||
func edit(mapping:GUIDEActionMapping):
|
||||
assert(_mapping == null)
|
||||
_mapping = mapping
|
||||
|
||||
_mapping.changed.connect(_update)
|
||||
|
||||
_update()
|
||||
|
||||
|
||||
func _update():
|
||||
_input_mappings.clear()
|
||||
|
||||
_action_slot.action = _mapping.action
|
||||
|
||||
for i in _mapping.input_mappings.size():
|
||||
var input_mapping = _mapping.input_mappings[i]
|
||||
var input_mapping_editor = input_mapping_editor_scene.instantiate()
|
||||
_input_mappings.add_item(input_mapping_editor)
|
||||
|
||||
input_mapping_editor.initialize(_plugin, _scanner)
|
||||
input_mapping_editor.edit(input_mapping)
|
||||
|
||||
_input_mappings.collapsed = _mapping.get_meta("_guide_input_mappings_collapsed", false)
|
||||
|
||||
func _on_action_changed():
|
||||
_undo_redo.create_action("Change action")
|
||||
_undo_redo.add_do_property(_mapping, "action", _action_slot.action)
|
||||
_undo_redo.add_undo_property(_mapping, "action", _mapping.action)
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
func _on_input_mappings_add_requested():
|
||||
var values = _mapping.input_mappings.duplicate()
|
||||
var new_mapping = GUIDEInputMapping.new()
|
||||
values.append(new_mapping)
|
||||
|
||||
_undo_redo.create_action("Add input mapping")
|
||||
|
||||
_undo_redo.add_do_property(_mapping, "input_mappings", values)
|
||||
_undo_redo.add_undo_property(_mapping, "input_mappings", _mapping.input_mappings)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
func _on_input_mapping_delete_requested(index:int):
|
||||
var values = _mapping.input_mappings.duplicate()
|
||||
values.remove_at(index)
|
||||
|
||||
_undo_redo.create_action("Delete input mapping")
|
||||
_undo_redo.add_do_property(_mapping, "input_mappings", values)
|
||||
_undo_redo.add_undo_property(_mapping, "input_mappings", _mapping.input_mappings)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
func _on_input_mappings_move_requested(from:int, to:int):
|
||||
var values = _mapping.input_mappings.duplicate()
|
||||
var mapping = values[from]
|
||||
values.remove_at(from)
|
||||
if from < to:
|
||||
to -= 1
|
||||
values.insert(to, mapping)
|
||||
|
||||
_undo_redo.create_action("Move input mapping")
|
||||
_undo_redo.add_do_property(_mapping, "input_mappings", values)
|
||||
_undo_redo.add_undo_property(_mapping, "input_mappings", _mapping.input_mappings)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
func _on_input_mappings_clear_requested():
|
||||
var values:Array[GUIDEInputMapping] = []
|
||||
_undo_redo.create_action("Clear input mappings")
|
||||
_undo_redo.add_do_property(_mapping, "input_mappings", values)
|
||||
_undo_redo.add_undo_property(_mapping, "input_mappings", _mapping.input_mappings)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
func _on_input_mappings_duplicate_requested(index:int):
|
||||
var values = _mapping.input_mappings.duplicate()
|
||||
var copy:GUIDEInputMapping = values[index].duplicate()
|
||||
copy.input = Utils.duplicate_if_inline(copy.input)
|
||||
|
||||
for i in copy.modifiers.size():
|
||||
copy.modifiers[i] = Utils.duplicate_if_inline(copy.modifiers[i])
|
||||
|
||||
for i in copy.triggers.size():
|
||||
copy.triggers[i] = Utils.duplicate_if_inline(copy.triggers[i])
|
||||
|
||||
# insert copy after original
|
||||
values.insert(index+1, copy)
|
||||
|
||||
_undo_redo.create_action("Duplicate input mapping")
|
||||
_undo_redo.add_do_property(_mapping, "input_mappings", values)
|
||||
_undo_redo.add_undo_property(_mapping, "input_mappings", _mapping.input_mappings)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
func _on_input_mappings_collapse_state_changed(new_state:bool):
|
||||
_mapping.set_meta("_guide_input_mappings_collapsed", new_state)
|
@@ -0,0 +1,43 @@
|
||||
[gd_scene load_steps=5 format=3 uid="uid://361aipcef24h"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/guide/editor/action_mapping_editor/action_mapping_editor.gd" id="1_2k0pi"]
|
||||
[ext_resource type="PackedScene" uid="uid://du4x7ng6ntuk4" path="res://addons/guide/editor/action_slot/action_slot.tscn" id="1_hguf2"]
|
||||
[ext_resource type="PackedScene" uid="uid://c323mdijdhktg" path="res://addons/guide/editor/input_mapping_editor/input_mapping_editor.tscn" id="2_a8nbp"]
|
||||
[ext_resource type="PackedScene" uid="uid://cly0ff32fvpb2" path="res://addons/guide/editor/array_edit/array_edit.tscn" id="4_ehr5j"]
|
||||
|
||||
[node name="ActionMappingEditor" type="MarginContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_vertical = 0
|
||||
theme_override_constants/margin_bottom = 5
|
||||
script = ExtResource("1_2k0pi")
|
||||
input_mapping_editor_scene = ExtResource("2_a8nbp")
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 0
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 0
|
||||
|
||||
[node name="ActionSlot" parent="HBoxContainer/HBoxContainer" instance=ExtResource("1_hguf2")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 0
|
||||
size_flags_stretch_ratio = 4.0
|
||||
|
||||
[node name="InputMappings" parent="HBoxContainer/VBoxContainer" instance=ExtResource("4_ehr5j")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
title = "Input mappings"
|
||||
add_tooltip = "Add input mapping"
|
||||
clear_tooltip = "Clear input mappings"
|
57
addons/guide/editor/action_slot/action_slot.gd
Normal file
@@ -0,0 +1,57 @@
|
||||
@tool
|
||||
extends LineEdit
|
||||
|
||||
signal action_changed()
|
||||
|
||||
var index:int
|
||||
|
||||
var action:GUIDEAction:
|
||||
set(value):
|
||||
if is_instance_valid(action):
|
||||
action.changed.disconnect(_refresh)
|
||||
|
||||
action = value
|
||||
|
||||
if is_instance_valid(action):
|
||||
action.changed.connect(_refresh)
|
||||
|
||||
# action_changed can only be emitted by
|
||||
# dragging an action into this, not when setting
|
||||
# the property
|
||||
_refresh()
|
||||
|
||||
|
||||
func _refresh():
|
||||
if not is_instance_valid(action):
|
||||
text = "<none>"
|
||||
tooltip_text = ""
|
||||
else:
|
||||
text = action._editor_name()
|
||||
tooltip_text = action.resource_path
|
||||
|
||||
func _can_drop_data(at_position, data) -> bool:
|
||||
if not data is Dictionary:
|
||||
return false
|
||||
|
||||
if data.has("files"):
|
||||
for file in data["files"]:
|
||||
if ResourceLoader.load(file) is GUIDEAction:
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
|
||||
func _drop_data(at_position, data) -> void:
|
||||
|
||||
for file in data["files"]:
|
||||
var item = ResourceLoader.load(file)
|
||||
if item is GUIDEAction:
|
||||
action = item
|
||||
action_changed.emit()
|
||||
|
||||
func _gui_input(event):
|
||||
if event is InputEventMouseButton:
|
||||
if event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
if is_instance_valid(action):
|
||||
EditorInterface.edit_resource(action)
|
||||
|
16
addons/guide/editor/action_slot/action_slot.tscn
Normal file
@@ -0,0 +1,16 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://du4x7ng6ntuk4"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/guide/editor/action_slot/action_slot.gd" id="1_w5nxd"]
|
||||
|
||||
[node name="ActionSlot" type="LineEdit"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 0
|
||||
text = "Name"
|
||||
editable = false
|
||||
selecting_enabled = false
|
||||
script = ExtResource("1_w5nxd")
|
113
addons/guide/editor/array_edit/array_edit.gd
Normal file
@@ -0,0 +1,113 @@
|
||||
@tool
|
||||
extends Container
|
||||
const Utils = preload("../utils.gd")
|
||||
|
||||
@export var item_scene:PackedScene
|
||||
|
||||
@export var title:String = "":
|
||||
set(value):
|
||||
title = value
|
||||
_refresh()
|
||||
|
||||
@export var add_tooltip:String:
|
||||
set(value):
|
||||
add_tooltip = value
|
||||
_refresh()
|
||||
|
||||
@export var clear_tooltip:String:
|
||||
set(value):
|
||||
clear_tooltip = value
|
||||
_refresh()
|
||||
|
||||
@export var item_separation:int = 8:
|
||||
set(value):
|
||||
item_separation = value
|
||||
_refresh()
|
||||
|
||||
|
||||
@export var collapsed:bool = false:
|
||||
set(value):
|
||||
collapsed = value
|
||||
_refresh()
|
||||
|
||||
signal add_requested()
|
||||
signal delete_requested(index:int)
|
||||
signal move_requested(from:int, to:int)
|
||||
signal insert_requested(index:int)
|
||||
signal duplicate_requested(index:int)
|
||||
signal clear_requested()
|
||||
signal collapse_state_changed(collapsed:bool)
|
||||
|
||||
@onready var _add_button:Button = %AddButton
|
||||
@onready var _clear_button:Button = %ClearButton
|
||||
@onready var _contents:Container = %Contents
|
||||
@onready var _title_label:Label = %TitleLabel
|
||||
@onready var _collapse_button:Button = %CollapseButton
|
||||
@onready var _expand_button:Button = %ExpandButton
|
||||
@onready var _count_label:Label = %CountLabel
|
||||
|
||||
func _ready():
|
||||
_add_button.icon = get_theme_icon("Add", "EditorIcons")
|
||||
_add_button.pressed.connect(func(): add_requested.emit())
|
||||
|
||||
_clear_button.icon = get_theme_icon("Clear", "EditorIcons")
|
||||
_clear_button.pressed.connect(func(): clear_requested.emit())
|
||||
|
||||
_collapse_button.icon = get_theme_icon("Collapse", "EditorIcons")
|
||||
_collapse_button.pressed.connect(_on_collapse_pressed)
|
||||
|
||||
_expand_button.icon = get_theme_icon("Forward", "EditorIcons")
|
||||
_expand_button.pressed.connect(_on_expand_pressed)
|
||||
|
||||
|
||||
_refresh()
|
||||
|
||||
|
||||
func _refresh():
|
||||
if is_instance_valid(_add_button):
|
||||
_add_button.tooltip_text = add_tooltip
|
||||
if is_instance_valid(_clear_button):
|
||||
_clear_button.tooltip_text = clear_tooltip
|
||||
_clear_button.visible = _contents.get_child_count() > 0
|
||||
|
||||
if is_instance_valid(_contents):
|
||||
_contents.add_theme_constant_override("separation", item_separation)
|
||||
_contents.visible = not collapsed
|
||||
|
||||
if is_instance_valid(_collapse_button):
|
||||
_collapse_button.visible = not collapsed
|
||||
|
||||
if is_instance_valid(_expand_button):
|
||||
_expand_button.visible = collapsed
|
||||
|
||||
if is_instance_valid(_title_label):
|
||||
_title_label.text = title
|
||||
|
||||
if is_instance_valid(_count_label):
|
||||
_count_label.text = "(%s)" % [_contents.get_child_count()]
|
||||
|
||||
|
||||
func clear():
|
||||
Utils.clear(_contents)
|
||||
_refresh()
|
||||
|
||||
|
||||
func add_item(new_item:Control):
|
||||
var item_wrapper = item_scene.instantiate()
|
||||
_contents.add_child(item_wrapper)
|
||||
item_wrapper.initialize(new_item)
|
||||
item_wrapper.move_requested.connect(func(from:int, to:int): move_requested.emit(from, to))
|
||||
item_wrapper.delete_requested.connect(func(idx:int): delete_requested.emit(idx) )
|
||||
item_wrapper.duplicate_requested.connect(func(idx:int): duplicate_requested.emit(idx) )
|
||||
_refresh()
|
||||
|
||||
|
||||
func _on_collapse_pressed():
|
||||
collapsed = true
|
||||
collapse_state_changed.emit(true)
|
||||
|
||||
|
||||
func _on_expand_pressed():
|
||||
collapsed = false
|
||||
collapse_state_changed.emit(false)
|
||||
|
88
addons/guide/editor/array_edit/array_edit.tscn
Normal file
@@ -0,0 +1,88 @@
|
||||
[gd_scene load_steps=5 format=3 uid="uid://cly0ff32fvpb2"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/guide/editor/array_edit/array_edit.gd" id="1_y3qyt"]
|
||||
[ext_resource type="PackedScene" uid="uid://cjabwsa4gmlpp" path="res://addons/guide/editor/array_edit/array_edit_item.tscn" id="2_n3ncl"]
|
||||
|
||||
[sub_resource type="Image" id="Image_efj5n"]
|
||||
data = {
|
||||
"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, 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, 94, 94, 127, 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, 94, 94, 127, 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, 94, 94, 127, 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, 255, 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, 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, 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, 94, 94, 54, 255, 94, 94, 57, 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, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 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, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 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, 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, 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),
|
||||
"format": "RGBA8",
|
||||
"height": 16,
|
||||
"mipmaps": false,
|
||||
"width": 16
|
||||
}
|
||||
|
||||
[sub_resource type="ImageTexture" id="ImageTexture_uapko"]
|
||||
image = SubResource("Image_efj5n")
|
||||
|
||||
[node name="Array" type="MarginContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_y3qyt")
|
||||
item_scene = ExtResource("2_n3ncl")
|
||||
item_separation = 10
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Panel" type="Panel" parent="VBoxContainer/MarginContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/MarginContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="CollapseButton" type="Button" parent="VBoxContainer/MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(32, 0)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
tooltip_text = "Collapse"
|
||||
icon = SubResource("ImageTexture_uapko")
|
||||
|
||||
[node name="ExpandButton" type="Button" parent="VBoxContainer/MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(48, 0)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
tooltip_text = "Expand"
|
||||
icon = SubResource("ImageTexture_uapko")
|
||||
|
||||
[node name="AddButton" type="Button" parent="VBoxContainer/MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
icon = SubResource("ImageTexture_uapko")
|
||||
|
||||
[node name="ClearButton" type="Button" parent="VBoxContainer/MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
icon = SubResource("ImageTexture_uapko")
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/MarginContainer/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/MarginContainer/HBoxContainer/MarginContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="VBoxContainer/MarginContainer/HBoxContainer/MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="CountLabel" type="Label" parent="VBoxContainer/MarginContainer/HBoxContainer/MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "(0)"
|
||||
|
||||
[node name="Contents" type="VBoxContainer" parent="VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
84
addons/guide/editor/array_edit/array_edit_item.gd
Normal file
@@ -0,0 +1,84 @@
|
||||
@tool
|
||||
extends Container
|
||||
const Utils = preload("../utils.gd")
|
||||
const Dragger = preload("dragger.gd")
|
||||
|
||||
signal move_requested(from:int, to:int)
|
||||
signal delete_requested(index:int)
|
||||
signal duplicate_requested(index:int)
|
||||
|
||||
@onready var _dragger:Dragger = %Dragger
|
||||
@onready var _content:Container = %Content
|
||||
@onready var _before_indicator:ColorRect = %BeforeIndicator
|
||||
@onready var _after_indicator:ColorRect = %AfterIndicator
|
||||
@onready var _popup_menu:PopupMenu = %PopupMenu
|
||||
|
||||
|
||||
const ID_DELETE = 2
|
||||
const ID_DUPLICATE = 3
|
||||
|
||||
func _ready():
|
||||
_dragger.icon = get_theme_icon("GuiSpinboxUpdown", "EditorIcons")
|
||||
_before_indicator.color = get_theme_color("box_selection_stroke_color", "Editor")
|
||||
_after_indicator.color = get_theme_color("box_selection_stroke_color", "Editor")
|
||||
_before_indicator.visible = false
|
||||
_after_indicator.visible = false
|
||||
_dragger._parent_array = get_parent()
|
||||
_dragger._index = get_index()
|
||||
_dragger.pressed.connect(_show_popup_menu)
|
||||
|
||||
_popup_menu.clear()
|
||||
_popup_menu.add_icon_item(get_theme_icon("Duplicate", "EditorIcons"), "Duplicate", ID_DUPLICATE)
|
||||
_popup_menu.add_icon_item(get_theme_icon("Remove", "EditorIcons"), "Delete", ID_DELETE)
|
||||
_popup_menu.id_pressed.connect(_on_popup_menu_id_pressed)
|
||||
|
||||
func initialize(content:Control):
|
||||
Utils.clear(_content)
|
||||
_content.add_child(content)
|
||||
|
||||
|
||||
func _can_drop_data(at_position:Vector2, data) -> bool:
|
||||
if data is Dictionary and data.has("parent_array") and data.parent_array == get_parent() and data.index != get_index():
|
||||
var height = size.y
|
||||
|
||||
var is_before = not _is_last_child() or (at_position.y < height/2.0)
|
||||
if is_before and data.index == get_index() - 1:
|
||||
# don't allow the previous child to be inserted at its
|
||||
# own position
|
||||
return false
|
||||
|
||||
_before_indicator.visible = is_before
|
||||
_after_indicator.visible = not is_before
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
|
||||
func _drop_data(at_position, data):
|
||||
var height = size.y
|
||||
var is_before = not _is_last_child() or (at_position.y < height/2.0)
|
||||
var from = data.index
|
||||
var to = get_index() if is_before else get_index() + 1
|
||||
move_requested.emit(data.index, to)
|
||||
_before_indicator.visible = false
|
||||
_after_indicator.visible = false
|
||||
|
||||
func _is_last_child() -> bool:
|
||||
return get_index() == get_parent().get_child_count() - 1
|
||||
|
||||
|
||||
func _on_mouse_exited():
|
||||
_before_indicator.visible = false
|
||||
_after_indicator.visible = false
|
||||
|
||||
|
||||
func _show_popup_menu():
|
||||
_popup_menu.popup(Rect2(get_global_mouse_position(), Vector2.ZERO))
|
||||
|
||||
|
||||
func _on_popup_menu_id_pressed(id:int):
|
||||
match id:
|
||||
ID_DELETE:
|
||||
delete_requested.emit(get_index())
|
||||
ID_DUPLICATE:
|
||||
duplicate_requested.emit(get_index())
|
83
addons/guide/editor/array_edit/array_edit_item.tscn
Normal file
@@ -0,0 +1,83 @@
|
||||
[gd_scene load_steps=5 format=3 uid="uid://cjabwsa4gmlpp"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/guide/editor/array_edit/array_edit_item.gd" id="1_ujx05"]
|
||||
[ext_resource type="Script" path="res://addons/guide/editor/array_edit/dragger.gd" id="2_53e2r"]
|
||||
|
||||
[sub_resource type="Image" id="Image_efj5n"]
|
||||
data = {
|
||||
"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, 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, 94, 94, 127, 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, 94, 94, 127, 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, 94, 94, 127, 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, 255, 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, 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, 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, 94, 94, 54, 255, 94, 94, 57, 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, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 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, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 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, 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, 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),
|
||||
"format": "RGBA8",
|
||||
"height": 16,
|
||||
"mipmaps": false,
|
||||
"width": 16
|
||||
}
|
||||
|
||||
[sub_resource type="ImageTexture" id="ImageTexture_uapko"]
|
||||
image = SubResource("Image_efj5n")
|
||||
|
||||
[node name="ArrayEditItem" type="MarginContainer"]
|
||||
anchors_preset = 10
|
||||
anchor_right = 1.0
|
||||
offset_bottom = 8.0
|
||||
grow_horizontal = 2
|
||||
script = ExtResource("1_ujx05")
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="."]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_top = 2
|
||||
theme_override_constants/margin_bottom = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Dragger" type="Button" parent="MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
tooltip_text = "Drag to reorder, click for options."
|
||||
focus_mode = 0
|
||||
mouse_filter = 1
|
||||
icon = SubResource("ImageTexture_uapko")
|
||||
script = ExtResource("2_53e2r")
|
||||
|
||||
[node name="Content" type="MarginContainer" parent="MarginContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="BeforeIndicator" type="ColorRect" parent="VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(0, 2)
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
color = Color(0, 0, 0, 1)
|
||||
|
||||
[node name="Control" type="Control" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="AfterIndicator" type="ColorRect" parent="VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(0, 2)
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
color = Color(0, 0, 0, 1)
|
||||
|
||||
[node name="PopupMenu" type="PopupMenu" parent="."]
|
||||
unique_name_in_owner = true
|
||||
item_count = 2
|
||||
item_0/text = "Duplicate"
|
||||
item_0/icon = SubResource("ImageTexture_uapko")
|
||||
item_0/id = 3
|
||||
item_1/text = "Delete"
|
||||
item_1/icon = SubResource("ImageTexture_uapko")
|
||||
item_1/id = 2
|
||||
|
||||
[connection signal="mouse_exited" from="." to="." method="_on_mouse_exited"]
|
8
addons/guide/editor/array_edit/dragger.gd
Normal file
@@ -0,0 +1,8 @@
|
||||
@tool
|
||||
extends Button
|
||||
|
||||
var _parent_array:Variant
|
||||
var _index:int
|
||||
|
||||
func _get_drag_data(at_position):
|
||||
return { "parent_array" : _parent_array, "index" : _index }
|
148
addons/guide/editor/binding_dialog/binding_dialog.gd
Normal file
@@ -0,0 +1,148 @@
|
||||
@tool
|
||||
extends Window
|
||||
|
||||
const ClassScanner = preload("../class_scanner.gd")
|
||||
const Utils = preload("../utils.gd")
|
||||
|
||||
signal input_selected(input:GUIDEInput)
|
||||
|
||||
@onready var _input_display = %InputDisplay
|
||||
@onready var _available_types:Container = %AvailableTypes
|
||||
@onready var _none_available:Control = %NoneAvailable
|
||||
@onready var _some_available:Control = %SomeAvailable
|
||||
@onready var _select_bool_button:Button = %SelectBoolButton
|
||||
@onready var _select_1d_button:Button = %Select1DButton
|
||||
@onready var _select_2d_button:Button = %Select2DButton
|
||||
@onready var _select_3d_button:Button = %Select3DButton
|
||||
@onready var _instructions_label:Label = %InstructionsLabel
|
||||
@onready var _accept_detection_button:Button = %AcceptDetectionButton
|
||||
@onready var _input_detector:GUIDEInputDetector = %InputDetector
|
||||
@onready var _detect_bool_button:Button = %DetectBoolButton
|
||||
@onready var _detect_1d_button:Button = %Detect1DButton
|
||||
@onready var _detect_2d_button:Button = %Detect2DButton
|
||||
@onready var _detect_3d_button:Button = %Detect3DButton
|
||||
|
||||
var _scanner:ClassScanner
|
||||
var _last_detected_input:GUIDEInput
|
||||
|
||||
|
||||
func initialize(scanner:ClassScanner):
|
||||
_scanner = scanner
|
||||
_setup_dialog()
|
||||
|
||||
func _setup_dialog():
|
||||
# we need to bind this here. if we bind it in the editor, the editor
|
||||
# will crash when opening the scene because it will delete the node it
|
||||
# just tries to edit.
|
||||
focus_exited.connect(_on_close_requested)
|
||||
|
||||
_show_inputs_of_value_type(GUIDEAction.GUIDEActionValueType.BOOL)
|
||||
_instructions_label.text = tr("Press one of the buttons above to detect an input.")
|
||||
_accept_detection_button.visible = false
|
||||
|
||||
|
||||
func _on_close_requested():
|
||||
hide()
|
||||
queue_free()
|
||||
|
||||
|
||||
func _show_inputs_of_value_type(type:GUIDEAction.GUIDEActionValueType) -> void:
|
||||
var items:Array[GUIDEInput] = []
|
||||
|
||||
_select_bool_button.set_pressed_no_signal(type == GUIDEAction.GUIDEActionValueType.BOOL)
|
||||
_select_1d_button.set_pressed_no_signal(type == GUIDEAction.GUIDEActionValueType.AXIS_1D)
|
||||
_select_2d_button.set_pressed_no_signal(type == GUIDEAction.GUIDEActionValueType.AXIS_2D)
|
||||
_select_3d_button.set_pressed_no_signal(type == GUIDEAction.GUIDEActionValueType.AXIS_3D)
|
||||
|
||||
var all_inputs = _scanner.find_inheritors("GUIDEInput")
|
||||
for script in all_inputs.values():
|
||||
var dummy:GUIDEInput = script.new()
|
||||
if dummy._native_value_type() == type:
|
||||
items.append(dummy)
|
||||
|
||||
_some_available.visible = not items.is_empty()
|
||||
_none_available.visible = items.is_empty()
|
||||
|
||||
if items.is_empty():
|
||||
return
|
||||
|
||||
items.sort_custom(func(a,b): return a._editor_name().nocasecmp_to(b._editor_name()) < 0)
|
||||
Utils.clear(_available_types)
|
||||
|
||||
for item in items:
|
||||
var button = Button.new()
|
||||
button.text = item._editor_name()
|
||||
button.tooltip_text = item._editor_description()
|
||||
button.pressed.connect(_deliver.bind(item))
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
||||
_available_types.add_child(button)
|
||||
|
||||
|
||||
func _deliver(input:GUIDEInput):
|
||||
input_selected.emit(input)
|
||||
hide()
|
||||
queue_free()
|
||||
|
||||
|
||||
func _on_select_bool_button_pressed():
|
||||
_show_inputs_of_value_type(GUIDEAction.GUIDEActionValueType.BOOL)
|
||||
|
||||
|
||||
func _on_select_1d_button_pressed():
|
||||
_show_inputs_of_value_type(GUIDEAction.GUIDEActionValueType.AXIS_1D)
|
||||
|
||||
|
||||
func _on_select_2d_button_pressed():
|
||||
_show_inputs_of_value_type(GUIDEAction.GUIDEActionValueType.AXIS_2D)
|
||||
|
||||
|
||||
func _on_select_3d_button_pressed():
|
||||
_show_inputs_of_value_type(GUIDEAction.GUIDEActionValueType.AXIS_3D)
|
||||
|
||||
|
||||
func _on_input_detector_detection_started():
|
||||
_instructions_label.text = tr("Actuate the input now...")
|
||||
|
||||
|
||||
func _on_input_detector_input_detected(input:GUIDEInput):
|
||||
_instructions_label.visible = false
|
||||
_input_display.visible = true
|
||||
_input_display.input = input
|
||||
_accept_detection_button.visible = true
|
||||
_last_detected_input = input
|
||||
|
||||
|
||||
func _begin_detect_input(type:GUIDEAction.GUIDEActionValueType):
|
||||
_last_detected_input = null
|
||||
_instructions_label.visible = true
|
||||
_instructions_label.text = tr("Get ready...")
|
||||
_accept_detection_button.visible = false
|
||||
_input_display.visible = false
|
||||
_input_detector.detect(type)
|
||||
|
||||
|
||||
func _on_detect_bool_button_pressed():
|
||||
_detect_bool_button.release_focus()
|
||||
_begin_detect_input(GUIDEAction.GUIDEActionValueType.BOOL)
|
||||
|
||||
|
||||
func _on_detect_1d_button_pressed():
|
||||
_detect_1d_button.release_focus()
|
||||
_begin_detect_input(GUIDEAction.GUIDEActionValueType.AXIS_1D)
|
||||
|
||||
|
||||
func _on_detect_2d_button_pressed():
|
||||
_detect_2d_button.release_focus()
|
||||
_begin_detect_input(GUIDEAction.GUIDEActionValueType.AXIS_2D)
|
||||
|
||||
|
||||
func _on_detect_3d_button_pressed():
|
||||
_detect_3d_button.release_focus()
|
||||
_begin_detect_input(GUIDEAction.GUIDEActionValueType.AXIS_3D)
|
||||
|
||||
|
||||
func _on_accept_detection_button_pressed():
|
||||
input_selected.emit(_last_detected_input)
|
||||
hide()
|
||||
queue_free
|
216
addons/guide/editor/binding_dialog/binding_dialog.tscn
Normal file
@@ -0,0 +1,216 @@
|
||||
[gd_scene load_steps=5 format=3 uid="uid://dic27bm4pfw3q"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/guide/editor/binding_dialog/binding_dialog.gd" id="1_tknjd"]
|
||||
[ext_resource type="PackedScene" uid="uid://dsv7s6tfmnsrs" path="res://addons/guide/editor/input_display/input_display.tscn" id="2_83ieu"]
|
||||
[ext_resource type="Script" path="res://addons/guide/remapping/guide_input_detector.gd" id="3_c6q6r"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_3e874"]
|
||||
content_margin_left = 4.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_right = 4.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color(1, 0.365, 0.365, 1)
|
||||
draw_center = false
|
||||
border_width_left = 2
|
||||
border_width_top = 2
|
||||
border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
corner_detail = 1
|
||||
|
||||
[node name="BindingDialog" type="Window"]
|
||||
title = "Input Configuration"
|
||||
initial_position = 4
|
||||
size = Vector2i(1200, 600)
|
||||
popup_window = true
|
||||
min_size = Vector2i(1200, 600)
|
||||
script = ExtResource("1_tknjd")
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="."]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/margin_bottom = 5
|
||||
|
||||
[node name="BGPanel" type="Panel" parent="MarginContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_3e874")
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="MarginContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 5
|
||||
theme_override_constants/margin_top = 5
|
||||
theme_override_constants/margin_right = 5
|
||||
theme_override_constants/margin_bottom = 5
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/MarginContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/MarginContainer/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="LeftPanel" type="Panel" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 5
|
||||
theme_override_constants/margin_top = 5
|
||||
theme_override_constants/margin_right = 5
|
||||
theme_override_constants/margin_bottom = 5
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="Label" type="Label" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "Detect Input"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="DetectBoolButton" type="Button" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "Boolean"
|
||||
|
||||
[node name="Detect1DButton" type="Button" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "1D"
|
||||
|
||||
[node name="Detect2DButton" type="Button" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "2D"
|
||||
|
||||
[node name="Detect3DButton" type="Button" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "3D"
|
||||
|
||||
[node name="InstructionsLabel" type="Label" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 6
|
||||
text = "3..2..1.."
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="InputDisplay" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer" instance=ExtResource("2_83ieu")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 6
|
||||
|
||||
[node name="AcceptDetectionButton" type="Button" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
text = "Accept"
|
||||
|
||||
[node name="MarginContainer2" type="MarginContainer" parent="MarginContainer/MarginContainer/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="RightPanel" type="Panel" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 5
|
||||
theme_override_constants/margin_top = 5
|
||||
theme_override_constants/margin_right = 5
|
||||
theme_override_constants/margin_bottom = 5
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="Label" type="Label" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "Select Input"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="SelectBoolButton" type="Button" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
toggle_mode = true
|
||||
text = "Boolean"
|
||||
|
||||
[node name="Select1DButton" type="Button" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
toggle_mode = true
|
||||
text = "1D"
|
||||
|
||||
[node name="Select2DButton" type="Button" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
toggle_mode = true
|
||||
text = "2D"
|
||||
|
||||
[node name="Select3DButton" type="Button" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
toggle_mode = true
|
||||
text = "3D"
|
||||
|
||||
[node name="NoneAvailable" type="Label" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 6
|
||||
size_flags_vertical = 6
|
||||
text = "No matching inputs available."
|
||||
|
||||
[node name="SomeAvailable" type="ScrollContainer" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="AvailableTypes" type="VBoxContainer" parent="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer/SomeAvailable"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="InputDetector" type="Node" parent="."]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("3_c6q6r")
|
||||
|
||||
[connection signal="close_requested" from="." to="." method="_on_close_requested"]
|
||||
[connection signal="pressed" from="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer/HBoxContainer/DetectBoolButton" to="." method="_on_detect_bool_button_pressed"]
|
||||
[connection signal="pressed" from="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer/HBoxContainer/Detect1DButton" to="." method="_on_detect_1d_button_pressed"]
|
||||
[connection signal="pressed" from="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer/HBoxContainer/Detect2DButton" to="." method="_on_detect_2d_button_pressed"]
|
||||
[connection signal="pressed" from="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer/HBoxContainer/Detect3DButton" to="." method="_on_detect_3d_button_pressed"]
|
||||
[connection signal="pressed" from="MarginContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/VBoxContainer/AcceptDetectionButton" to="." method="_on_accept_detection_button_pressed"]
|
||||
[connection signal="pressed" from="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer/HBoxContainer/SelectBoolButton" to="." method="_on_select_bool_button_pressed"]
|
||||
[connection signal="pressed" from="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer/HBoxContainer/Select1DButton" to="." method="_on_select_1d_button_pressed"]
|
||||
[connection signal="pressed" from="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer/HBoxContainer/Select2DButton" to="." method="_on_select_2d_button_pressed"]
|
||||
[connection signal="pressed" from="MarginContainer/MarginContainer/HBoxContainer/MarginContainer2/MarginContainer/VBoxContainer/HBoxContainer/Select3DButton" to="." method="_on_select_3d_button_pressed"]
|
||||
[connection signal="detection_started" from="InputDetector" to="." method="_on_input_detector_detection_started"]
|
||||
[connection signal="input_detected" from="InputDetector" to="." method="_on_input_detector_input_detected"]
|
91
addons/guide/editor/class_scanner.gd
Normal file
@@ -0,0 +1,91 @@
|
||||
## Scanner to find inheriting classes. Used to detect inheritors of
|
||||
## modifiers and triggers. Ideally this would be built into the editor
|
||||
## but sometimes one has to hack their way around the limitations.
|
||||
## This only scans to the extent needed to drive the UI, it's not a general
|
||||
## purpose implementation.
|
||||
@tool
|
||||
|
||||
const GUIDESet = preload("../guide_set.gd")
|
||||
|
||||
var _dirty:bool = true
|
||||
|
||||
# looks like we only get very limited access to the script's inheritance tree,
|
||||
# so we need to do a little caching ourselves
|
||||
var _script_lut:Dictionary = {}
|
||||
|
||||
func _init():
|
||||
EditorInterface.get_resource_filesystem().script_classes_updated.connect(_mark_dirty)
|
||||
|
||||
|
||||
func _mark_dirty():
|
||||
_dirty = true
|
||||
|
||||
## Returns all classes that directly or indirectly inherit from the
|
||||
## given class. Only works for scripts in the project, e.g. doesn't
|
||||
## scan the whole class_db. Key is class name, value is the Script instance
|
||||
func find_inheritors(clazz_name:StringName) -> Dictionary:
|
||||
var result:Dictionary = {}
|
||||
|
||||
var root := EditorInterface.get_resource_filesystem().get_filesystem()
|
||||
|
||||
# rebuild the LUT when needed
|
||||
if _dirty:
|
||||
_script_lut.clear()
|
||||
_scan(root)
|
||||
_dirty = false
|
||||
|
||||
|
||||
var open_set:GUIDESet = GUIDESet.new()
|
||||
# a closed set just to avoid infinite loops, we'll never
|
||||
# look at the same class more than once.
|
||||
var closed_set:GUIDESet = GUIDESet.new()
|
||||
|
||||
open_set.add(clazz_name)
|
||||
|
||||
while not open_set.is_empty():
|
||||
var next = open_set.pull()
|
||||
closed_set.add(next)
|
||||
if not _script_lut.has(next):
|
||||
# we don't know this script, ignore, move on
|
||||
continue
|
||||
|
||||
# now find all scripts that extend the one we
|
||||
# are looking at
|
||||
for item:ScriptInfo in _script_lut.values():
|
||||
if item.extendz == next:
|
||||
# put them into the result
|
||||
result[item.clazz_name] = item.clazz_script
|
||||
# and put their class in the open set
|
||||
# unless we already looked at it.
|
||||
if not closed_set.has(item.clazz_name):
|
||||
open_set.add(item.clazz_name)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
func _scan(folder:EditorFileSystemDirectory):
|
||||
for i in folder.get_file_count():
|
||||
var script_clazz = folder.get_file_script_class_name(i)
|
||||
if script_clazz != "":
|
||||
var info := _script_lut.get(script_clazz)
|
||||
if info == null:
|
||||
info = ScriptInfo.new()
|
||||
info.clazz_name = script_clazz
|
||||
info.clazz_script = ResourceLoader.load(folder.get_file_path(i))
|
||||
_script_lut[script_clazz] = info
|
||||
|
||||
var script_extendz = folder.get_file_script_class_extends(i)
|
||||
info.extendz = script_extendz
|
||||
|
||||
for i in folder.get_subdir_count():
|
||||
_scan(folder.get_subdir(i))
|
||||
|
||||
|
||||
class ScriptInfo:
|
||||
var clazz_name:StringName
|
||||
var extendz:StringName
|
||||
var clazz_script:Script
|
||||
|
||||
func _to_string() -> String:
|
||||
return clazz_name + ":" + extendz
|
||||
|
39
addons/guide/editor/input_display/input_display.gd
Normal file
@@ -0,0 +1,39 @@
|
||||
@tool
|
||||
extends RichTextLabel
|
||||
signal clicked()
|
||||
|
||||
var _formatter:GUIDEInputFormatter = GUIDEInputFormatter.new(64)
|
||||
|
||||
var input:GUIDEInput:
|
||||
set(value):
|
||||
if value == input:
|
||||
return
|
||||
|
||||
if is_instance_valid(input):
|
||||
input.changed.disconnect(_refresh)
|
||||
|
||||
input = value
|
||||
|
||||
if is_instance_valid(input):
|
||||
input.changed.connect(_refresh)
|
||||
|
||||
_refresh()
|
||||
|
||||
func _refresh():
|
||||
if not is_instance_valid(input):
|
||||
parse_bbcode("[center][i]<not bound>[/i][/center]")
|
||||
tooltip_text = ""
|
||||
return
|
||||
|
||||
var text := await _formatter.input_as_richtext_async(input, false)
|
||||
parse_bbcode("[center]" + text + "[/center]")
|
||||
tooltip_text = _formatter.input_as_text(input)
|
||||
|
||||
|
||||
func _gui_input(event):
|
||||
if event is InputEventMouseButton:
|
||||
if event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
clicked.emit()
|
||||
|
||||
|
||||
|
18
addons/guide/editor/input_display/input_display.tscn
Normal file
@@ -0,0 +1,18 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://dsv7s6tfmnsrs"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/guide/editor/input_display/input_display.gd" id="1_ne6sd"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_0bp65"]
|
||||
|
||||
[node name="InputDisplay" type="RichTextLabel"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_styles/normal = SubResource("StyleBoxEmpty_0bp65")
|
||||
bbcode_enabled = true
|
||||
fit_content = true
|
||||
script = ExtResource("1_ne6sd")
|
299
addons/guide/editor/input_mapping_editor/input_mapping_editor.gd
Normal file
@@ -0,0 +1,299 @@
|
||||
@tool
|
||||
extends MarginContainer
|
||||
|
||||
const ArrayEdit = preload("../array_edit/array_edit.gd")
|
||||
const ClassScanner = preload("../class_scanner.gd")
|
||||
const Utils = preload("../utils.gd")
|
||||
|
||||
@export var modifier_slot_scene:PackedScene
|
||||
@export var trigger_slot_scene:PackedScene
|
||||
@export var binding_dialog_scene:PackedScene
|
||||
|
||||
@onready var _edit_input_mapping_button:Button = %EditInputMappingButton
|
||||
@onready var _input_display = %InputDisplay
|
||||
@onready var _edit_input_button:Button = %EditInputButton
|
||||
@onready var _clear_input_button:Button = %ClearInputButton
|
||||
|
||||
@onready var _modifiers:ArrayEdit = %Modifiers
|
||||
@onready var _add_modifier_popup:PopupMenu = %AddModifierPopup
|
||||
|
||||
@onready var _triggers:ArrayEdit = %Triggers
|
||||
@onready var _add_trigger_popup:PopupMenu = %AddTriggerPopup
|
||||
|
||||
var _plugin:EditorPlugin
|
||||
var _scanner:ClassScanner
|
||||
var _undo_redo:EditorUndoRedoManager
|
||||
|
||||
var _mapping:GUIDEInputMapping
|
||||
|
||||
func _ready():
|
||||
_edit_input_button.icon = get_theme_icon("Edit", "EditorIcons")
|
||||
_clear_input_button.icon = get_theme_icon("Remove", "EditorIcons")
|
||||
_edit_input_mapping_button.icon = get_theme_icon("Tools", "EditorIcons")
|
||||
|
||||
_modifiers.add_requested.connect(_on_modifiers_add_requested)
|
||||
_modifiers.delete_requested.connect(_on_modifier_delete_requested)
|
||||
_modifiers.duplicate_requested.connect(_on_modifier_duplicate_requested)
|
||||
_modifiers.move_requested.connect(_on_modifier_move_requested)
|
||||
_modifiers.clear_requested.connect(_on_modifiers_clear_requested)
|
||||
_modifiers.collapse_state_changed.connect(_on_modifiers_collapse_state_changed)
|
||||
|
||||
_triggers.add_requested.connect(_on_triggers_add_requested)
|
||||
_triggers.delete_requested.connect(_on_trigger_delete_requested)
|
||||
_triggers.duplicate_requested.connect(_on_trigger_duplicate_requested)
|
||||
_triggers.move_requested.connect(_on_trigger_move_requested)
|
||||
_triggers.clear_requested.connect(_on_triggers_clear_requested)
|
||||
_triggers.collapse_state_changed.connect(_on_triggers_collapse_state_changed)
|
||||
|
||||
|
||||
func initialize(plugin:EditorPlugin, scanner:ClassScanner) -> void:
|
||||
_plugin = plugin
|
||||
_scanner = scanner
|
||||
_undo_redo = plugin.get_undo_redo()
|
||||
_input_display.clicked.connect(_on_input_display_clicked)
|
||||
|
||||
|
||||
func edit(mapping:GUIDEInputMapping) -> void:
|
||||
assert(_mapping == null)
|
||||
_mapping = mapping
|
||||
_mapping.changed.connect(_update)
|
||||
_update()
|
||||
|
||||
|
||||
func _update():
|
||||
_modifiers.clear()
|
||||
_triggers.clear()
|
||||
|
||||
_input_display.input = _mapping.input
|
||||
for i in _mapping.modifiers.size():
|
||||
var modifier_slot = modifier_slot_scene.instantiate()
|
||||
_modifiers.add_item(modifier_slot)
|
||||
|
||||
modifier_slot.modifier = _mapping.modifiers[i]
|
||||
modifier_slot.changed.connect(_on_modifier_changed.bind(i, modifier_slot))
|
||||
|
||||
for i in _mapping.triggers.size():
|
||||
var trigger_slot = trigger_slot_scene.instantiate()
|
||||
_triggers.add_item(trigger_slot)
|
||||
|
||||
trigger_slot.trigger = _mapping.triggers[i]
|
||||
trigger_slot.changed.connect(_on_trigger_changed.bind(i, trigger_slot))
|
||||
|
||||
_modifiers.collapsed = _mapping.get_meta("_guide_modifiers_collapsed", false)
|
||||
_triggers.collapsed = _mapping.get_meta("_guide_triggers_collapsed", false)
|
||||
|
||||
|
||||
func _on_modifiers_add_requested():
|
||||
_fill_popup(_add_modifier_popup, "GUIDEModifier")
|
||||
_add_modifier_popup.popup(Rect2(get_global_mouse_position(), Vector2.ZERO))
|
||||
|
||||
|
||||
func _on_triggers_add_requested():
|
||||
_fill_popup(_add_trigger_popup, "GUIDETrigger")
|
||||
_add_trigger_popup.popup(Rect2(get_global_mouse_position(), Vector2.ZERO))
|
||||
|
||||
|
||||
func _fill_popup(popup:PopupMenu, base_clazz:StringName):
|
||||
popup.clear(true)
|
||||
|
||||
var inheritors := _scanner.find_inheritors(base_clazz)
|
||||
for type in inheritors.keys():
|
||||
var class_script:Script = inheritors[type]
|
||||
var dummy = class_script.new()
|
||||
popup.add_item(dummy._editor_name())
|
||||
popup.set_item_tooltip(popup.item_count -1, dummy._editor_description())
|
||||
popup.set_item_metadata(popup.item_count - 1, class_script)
|
||||
|
||||
func _on_input_display_clicked():
|
||||
if is_instance_valid(_mapping.input):
|
||||
EditorInterface.edit_resource(_mapping.input)
|
||||
|
||||
|
||||
func _on_input_changed(input:GUIDEInput):
|
||||
_undo_redo.create_action("Change input")
|
||||
|
||||
_undo_redo.add_do_property(_mapping, "input", input)
|
||||
_undo_redo.add_undo_property(_mapping, "input", _mapping.input)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
if is_instance_valid(input):
|
||||
EditorInterface.edit_resource(input)
|
||||
|
||||
|
||||
func _on_edit_input_button_pressed():
|
||||
var dialog:Window = binding_dialog_scene.instantiate()
|
||||
EditorInterface.popup_dialog_centered(dialog)
|
||||
dialog.initialize(_scanner)
|
||||
dialog.input_selected.connect(_on_input_changed)
|
||||
|
||||
|
||||
func _on_clear_input_button_pressed():
|
||||
_undo_redo.create_action("Delete bound input")
|
||||
|
||||
_undo_redo.add_do_property(_mapping, "input", null)
|
||||
_undo_redo.add_undo_property(_mapping, "triggers", _mapping.input)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
func _on_add_modifier_popup_index_pressed(index:int) -> void:
|
||||
var script = _add_modifier_popup.get_item_metadata(index)
|
||||
var new_modifier = script.new()
|
||||
|
||||
_undo_redo.create_action("Add " + new_modifier._editor_name() + " modifier")
|
||||
var modifiers = _mapping.modifiers.duplicate()
|
||||
modifiers.append(new_modifier)
|
||||
|
||||
_undo_redo.add_do_property(_mapping, "modifiers", modifiers)
|
||||
_undo_redo.add_undo_property(_mapping, "modifiers", _mapping.modifiers)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
func _on_add_trigger_popup_index_pressed(index):
|
||||
var script = _add_trigger_popup.get_item_metadata(index)
|
||||
var new_trigger = script.new()
|
||||
|
||||
_undo_redo.create_action("Add " + new_trigger._editor_name() + " trigger")
|
||||
var triggers = _mapping.triggers.duplicate()
|
||||
triggers.append(new_trigger)
|
||||
|
||||
_undo_redo.add_do_property(_mapping, "triggers", triggers)
|
||||
_undo_redo.add_undo_property(_mapping, "triggers", _mapping.triggers)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
func _on_modifier_changed(index:int, slot) -> void:
|
||||
var new_modifier = slot.modifier
|
||||
|
||||
_undo_redo.create_action("Replace modifier")
|
||||
var modifiers = _mapping.modifiers.duplicate()
|
||||
modifiers[index] = new_modifier
|
||||
|
||||
_undo_redo.add_do_property(_mapping, "modifiers", modifiers)
|
||||
_undo_redo.add_undo_property(_mapping, "modifiers", _mapping.modifiers)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
func _on_trigger_changed(index:int, slot) -> void:
|
||||
var new_trigger = slot.trigger
|
||||
|
||||
_undo_redo.create_action("Replace trigger")
|
||||
var triggers = _mapping.triggers.duplicate()
|
||||
triggers[index] = new_trigger
|
||||
|
||||
_undo_redo.add_do_property(_mapping, "triggers", triggers)
|
||||
_undo_redo.add_undo_property(_mapping, "triggers", _mapping.triggers)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
func _on_modifier_move_requested(from:int, to:int) -> void:
|
||||
_undo_redo.create_action("Move modifier")
|
||||
var modifiers = _mapping.modifiers.duplicate()
|
||||
var modifier = modifiers[from]
|
||||
modifiers.remove_at(from)
|
||||
if from < to:
|
||||
to -= 1
|
||||
modifiers.insert(to, modifier)
|
||||
|
||||
_undo_redo.add_do_property(_mapping, "modifiers", modifiers)
|
||||
_undo_redo.add_undo_property(_mapping, "modifiers", _mapping.modifiers)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
func _on_trigger_move_requested(from:int, to:int) -> void:
|
||||
_undo_redo.create_action("Move trigger")
|
||||
var triggers = _mapping.triggers.duplicate()
|
||||
var trigger = triggers[from]
|
||||
triggers.remove_at(from)
|
||||
if from < to:
|
||||
to -= 1
|
||||
triggers.insert(to, trigger)
|
||||
|
||||
_undo_redo.add_do_property(_mapping, "triggers", triggers)
|
||||
_undo_redo.add_undo_property(_mapping, "triggers", _mapping.triggers)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
func _on_modifier_duplicate_requested(index:int) -> void:
|
||||
_undo_redo.create_action("Duplicate modifier")
|
||||
var modifiers = _mapping.modifiers.duplicate()
|
||||
var copy = Utils.duplicate_if_inline(modifiers[index])
|
||||
modifiers.insert(index+1, copy)
|
||||
|
||||
_undo_redo.add_do_property(_mapping, "modifiers", modifiers)
|
||||
_undo_redo.add_undo_property(_mapping, "modifiers", _mapping.modifiers)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
func _on_trigger_duplicate_requested(index:int) -> void:
|
||||
_undo_redo.create_action("Duplicate trigger")
|
||||
var triggers = _mapping.triggers.duplicate()
|
||||
var copy = Utils.duplicate_if_inline(triggers[index])
|
||||
triggers.insert(index+1, copy)
|
||||
|
||||
_undo_redo.add_do_property(_mapping, "triggers", triggers)
|
||||
_undo_redo.add_undo_property(_mapping, "triggers", _mapping.triggers)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
|
||||
func _on_modifier_delete_requested(index:int) -> void:
|
||||
_undo_redo.create_action("Delete modifier")
|
||||
var modifiers = _mapping.modifiers.duplicate()
|
||||
modifiers.remove_at(index)
|
||||
|
||||
_undo_redo.add_do_property(_mapping, "modifiers", modifiers)
|
||||
_undo_redo.add_undo_property(_mapping, "modifiers", _mapping.modifiers)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
func _on_trigger_delete_requested(index:int) -> void:
|
||||
_undo_redo.create_action("Delete trigger")
|
||||
var triggers = _mapping.triggers.duplicate()
|
||||
triggers.remove_at(index)
|
||||
|
||||
_undo_redo.add_do_property(_mapping, "triggers", triggers)
|
||||
_undo_redo.add_undo_property(_mapping, "triggers", _mapping.triggers)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
func _on_modifiers_clear_requested() -> void:
|
||||
_undo_redo.create_action("Clear modifiers")
|
||||
# if this is inlined into the do_property, then it doesn't work
|
||||
# so lets keep it a local variable
|
||||
var value:Array[GUIDEModifier] = []
|
||||
_undo_redo.add_do_property(_mapping, "modifiers", value)
|
||||
_undo_redo.add_undo_property(_mapping, "modifiers", _mapping.modifiers)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
func _on_triggers_clear_requested() -> void:
|
||||
_undo_redo.create_action("Clear triggers")
|
||||
# if this is inlined into the do_property, then it doesn't work
|
||||
# so lets keep it a local variable
|
||||
var value:Array[GUIDETrigger] = []
|
||||
_undo_redo.add_do_property(_mapping, "triggers", value)
|
||||
_undo_redo.add_undo_property(_mapping, "triggers", _mapping.triggers)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
func _on_modifiers_collapse_state_changed(new_state:bool):
|
||||
_mapping.set_meta("_guide_modifiers_collapsed", new_state)
|
||||
|
||||
func _on_triggers_collapse_state_changed(new_state:bool):
|
||||
_mapping.set_meta("_guide_triggers_collapsed", new_state)
|
||||
|
||||
|
||||
func _on_edit_input_mapping_button_pressed():
|
||||
EditorInterface.edit_resource(_mapping)
|
@@ -0,0 +1,140 @@
|
||||
[gd_scene load_steps=9 format=3 uid="uid://c323mdijdhktg"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://dsv7s6tfmnsrs" path="res://addons/guide/editor/input_display/input_display.tscn" id="1_pg8n3"]
|
||||
[ext_resource type="Script" path="res://addons/guide/editor/input_mapping_editor/input_mapping_editor.gd" id="1_xsluc"]
|
||||
[ext_resource type="PackedScene" uid="uid://ck5a30syo6bpo" path="res://addons/guide/editor/modifier_slot/modifier_slot.tscn" id="2_uhbrq"]
|
||||
[ext_resource type="PackedScene" uid="uid://tk30wnstb0ku" path="res://addons/guide/editor/trigger_slot/trigger_slot.tscn" id="3_e0jys"]
|
||||
[ext_resource type="PackedScene" uid="uid://dic27bm4pfw3q" path="res://addons/guide/editor/binding_dialog/binding_dialog.tscn" id="4_oepf3"]
|
||||
[ext_resource type="PackedScene" uid="uid://cly0ff32fvpb2" path="res://addons/guide/editor/array_edit/array_edit.tscn" id="6_jekhk"]
|
||||
|
||||
[sub_resource type="Image" id="Image_m1w1j"]
|
||||
data = {
|
||||
"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, 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, 94, 94, 127, 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, 94, 94, 127, 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, 94, 94, 127, 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, 255, 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, 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, 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, 94, 94, 54, 255, 94, 94, 57, 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, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 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, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 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, 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, 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),
|
||||
"format": "RGBA8",
|
||||
"height": 16,
|
||||
"mipmaps": false,
|
||||
"width": 16
|
||||
}
|
||||
|
||||
[sub_resource type="ImageTexture" id="ImageTexture_y0eyy"]
|
||||
image = SubResource("Image_m1w1j")
|
||||
|
||||
[node name="InputMappingEditor" type="MarginContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_vertical = 0
|
||||
script = ExtResource("1_xsluc")
|
||||
modifier_slot_scene = ExtResource("2_uhbrq")
|
||||
trigger_slot_scene = ExtResource("3_e0jys")
|
||||
binding_dialog_scene = ExtResource("4_oepf3")
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 0
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 0
|
||||
|
||||
[node name="Panel" type="Panel" parent="HBoxContainer/MarginContainer"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
||||
[node name="EditInputMappingButton" type="Button" parent="HBoxContainer/MarginContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
tooltip_text = "Open input mapping in inspector"
|
||||
icon = SubResource("ImageTexture_y0eyy")
|
||||
flat = true
|
||||
|
||||
[node name="MarginContainer1" type="MarginContainer" parent="HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="Panel" type="Panel" parent="HBoxContainer/MarginContainer1"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer/MarginContainer1"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="InputDisplay" parent="HBoxContainer/MarginContainer1/HBoxContainer" instance=ExtResource("1_pg8n3")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
scroll_active = false
|
||||
|
||||
[node name="EditInputButton" type="Button" parent="HBoxContainer/MarginContainer1/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 0
|
||||
tooltip_text = "Edit bound input..."
|
||||
icon = SubResource("ImageTexture_y0eyy")
|
||||
flat = true
|
||||
|
||||
[node name="ClearInputButton" type="Button" parent="HBoxContainer/MarginContainer1/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 0
|
||||
tooltip_text = "Delete bound input"
|
||||
icon = SubResource("ImageTexture_y0eyy")
|
||||
flat = true
|
||||
|
||||
[node name="MarginContainer2" type="MarginContainer" parent="HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 2.0
|
||||
|
||||
[node name="Panel" type="Panel" parent="HBoxContainer/MarginContainer2"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer/MarginContainer2"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 0
|
||||
size_flags_stretch_ratio = 2.0
|
||||
|
||||
[node name="Modifiers" parent="HBoxContainer/MarginContainer2/VBoxContainer" instance=ExtResource("6_jekhk")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
title = "Modifiers"
|
||||
add_tooltip = "Add modifier..."
|
||||
clear_tooltip = "Clear modifiers"
|
||||
|
||||
[node name="AddModifierPopup" type="PopupMenu" parent="HBoxContainer/MarginContainer2/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
|
||||
[node name="MarginContainer3" type="MarginContainer" parent="HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 2.0
|
||||
|
||||
[node name="Panel" type="Panel" parent="HBoxContainer/MarginContainer3"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
||||
[node name="VBoxContainer2" type="VBoxContainer" parent="HBoxContainer/MarginContainer3"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 0
|
||||
size_flags_stretch_ratio = 2.0
|
||||
|
||||
[node name="Triggers" parent="HBoxContainer/MarginContainer3/VBoxContainer2" instance=ExtResource("6_jekhk")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
title = "Triggers"
|
||||
add_tooltip = "Add trigger..."
|
||||
clear_tooltip = "Clear triggers"
|
||||
|
||||
[node name="AddTriggerPopup" type="PopupMenu" parent="HBoxContainer/MarginContainer3/VBoxContainer2"]
|
||||
unique_name_in_owner = true
|
||||
|
||||
[connection signal="pressed" from="HBoxContainer/MarginContainer/EditInputMappingButton" to="." method="_on_edit_input_mapping_button_pressed"]
|
||||
[connection signal="pressed" from="HBoxContainer/MarginContainer1/HBoxContainer/EditInputButton" to="." method="_on_edit_input_button_pressed"]
|
||||
[connection signal="pressed" from="HBoxContainer/MarginContainer1/HBoxContainer/ClearInputButton" to="." method="_on_clear_input_button_pressed"]
|
||||
[connection signal="index_pressed" from="HBoxContainer/MarginContainer2/VBoxContainer/AddModifierPopup" to="." method="_on_add_modifier_popup_index_pressed"]
|
||||
[connection signal="index_pressed" from="HBoxContainer/MarginContainer3/VBoxContainer2/AddTriggerPopup" to="." method="_on_add_trigger_popup_index_pressed"]
|
24
addons/guide/editor/logo_editor_small.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1.16508,0,-1.89607)">
|
||||
<path d="M11.289,19.641L0.424,19.641L0.424,11.08L11.289,11.08L11.289,1.973L21.263,1.973L21.263,11.08L31.576,11.08L31.576,19.641L21.263,19.641L21.263,28.711L11.289,28.711L11.289,19.641Z" style="fill:rgb(235,235,235);"/>
|
||||
<path d="M11.289,19.641L0.424,19.641L0.424,11.08L11.289,11.08L11.289,1.973L21.263,1.973L21.263,11.08L31.576,11.08L31.576,19.641L21.263,19.641L21.263,28.711L11.289,28.711L11.289,19.641ZM11.567,19.641L11.567,28.473L20.985,28.473L20.985,19.641C20.985,19.509 21.109,19.402 21.263,19.402L31.298,19.402L31.298,11.318L21.263,11.318C21.109,11.318 20.985,11.212 20.985,11.08L20.985,2.212L11.567,2.212L11.567,11.08C11.567,11.212 11.442,11.318 11.289,11.318L0.702,11.318L0.702,19.402L11.289,19.402C11.442,19.402 11.567,19.509 11.567,19.641Z" style="fill:rgb(102,102,102);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-1.63395,-1.35279)">
|
||||
<path d="M17.634,3.353L21.284,10.653L13.984,10.653L17.634,3.353Z" style="fill:rgb(102,102,102);"/>
|
||||
<path d="M17.634,3.353L21.284,10.653L13.984,10.653L17.634,3.353ZM17.634,3.973C17.634,3.973 14.433,10.375 14.433,10.375L20.835,10.375L17.634,3.973Z" style="fill:rgb(102,102,102);"/>
|
||||
</g>
|
||||
<g transform="matrix(6.12323e-17,1,-1,6.12323e-17,33.3528,-1.63395)">
|
||||
<path d="M17.634,3.353L21.284,10.653L13.984,10.653L17.634,3.353Z" style="fill:rgb(102,102,102);"/>
|
||||
<path d="M17.634,3.353L21.284,10.653L13.984,10.653L17.634,3.353ZM17.634,3.973C17.634,3.973 14.433,10.375 14.433,10.375L20.835,10.375L17.634,3.973Z" style="fill:rgb(102,102,102);"/>
|
||||
</g>
|
||||
<g transform="matrix(-1,1.22465e-16,-1.22465e-16,-1,33.634,33.3528)">
|
||||
<path d="M17.634,3.353L21.284,10.653L13.984,10.653L17.634,3.353Z" style="fill:rgb(102,102,102);"/>
|
||||
<path d="M17.634,3.353L21.284,10.653L13.984,10.653L17.634,3.353ZM17.634,3.973C17.634,3.973 14.433,10.375 14.433,10.375L20.835,10.375L17.634,3.973Z" style="fill:rgb(102,102,102);"/>
|
||||
</g>
|
||||
<g transform="matrix(-1.83697e-16,-1,1,-1.83697e-16,-1.33687,33.6127)">
|
||||
<path d="M17.634,3.353L21.284,10.653L13.984,10.653L17.634,3.353Z" style="fill:rgb(102,102,102);"/>
|
||||
<path d="M17.634,3.353L21.284,10.653L13.984,10.653L17.634,3.353ZM17.634,3.973C17.634,3.973 14.433,10.375 14.433,10.375L20.835,10.375L17.634,3.973Z" style="fill:rgb(102,102,102);"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
38
addons/guide/editor/logo_editor_small.svg.import
Normal file
@@ -0,0 +1,38 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cap7e0f05pj8j"
|
||||
path="res://.godot/imported/logo_editor_small.svg-a18f1eaff840dcdf5215ef26c289caf9.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/guide/editor/logo_editor_small.svg"
|
||||
dest_files=["res://.godot/imported/logo_editor_small.svg-a18f1eaff840dcdf5215ef26c289caf9.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=0.5
|
||||
editor/scale_with_editor_scale=true
|
||||
editor/convert_colors_with_editor_theme=false
|
@@ -0,0 +1,159 @@
|
||||
@tool
|
||||
extends MarginContainer
|
||||
|
||||
const ClassScanner = preload("../class_scanner.gd")
|
||||
const Utils = preload("../utils.gd")
|
||||
const ArrayEdit = preload("../array_edit/array_edit.gd")
|
||||
|
||||
@export var action_mapping_editor_scene:PackedScene
|
||||
|
||||
@onready var _title_label:Label = %TitleLabel
|
||||
@onready var _action_mappings:ArrayEdit = %ActionMappings
|
||||
@onready var _editing_view:Control = %EditingView
|
||||
@onready var _empty_view = %EmptyView
|
||||
|
||||
var _plugin:EditorPlugin
|
||||
var _current_context:GUIDEMappingContext
|
||||
var _undo_redo:EditorUndoRedoManager
|
||||
var _scanner:ClassScanner
|
||||
|
||||
|
||||
func _ready():
|
||||
_title_label.add_theme_font_override("font", get_theme_font("title", "EditorFonts"))
|
||||
_scanner = ClassScanner.new()
|
||||
|
||||
_editing_view.visible = false
|
||||
_empty_view.visible = true
|
||||
|
||||
_action_mappings.add_requested.connect(_on_action_mappings_add_requested)
|
||||
_action_mappings.move_requested.connect(_on_action_mappings_move_requested)
|
||||
_action_mappings.delete_requested.connect(_on_action_mapping_delete_requested)
|
||||
_action_mappings.clear_requested.connect(_on_action_mappings_clear_requested)
|
||||
_action_mappings.duplicate_requested.connect(_on_action_mapping_duplicate_requested)
|
||||
_action_mappings.collapse_state_changed.connect(_on_action_mappings_collapse_state_changed)
|
||||
|
||||
func initialize(plugin:EditorPlugin) -> void:
|
||||
_plugin = plugin
|
||||
_undo_redo = plugin.get_undo_redo()
|
||||
|
||||
|
||||
func edit(context:GUIDEMappingContext) -> void:
|
||||
if is_instance_valid(_current_context):
|
||||
_current_context.changed.disconnect(_refresh)
|
||||
|
||||
_current_context = context
|
||||
|
||||
if is_instance_valid(_current_context):
|
||||
_current_context.changed.connect(_refresh)
|
||||
|
||||
_refresh()
|
||||
|
||||
|
||||
func _refresh():
|
||||
_editing_view.visible = is_instance_valid(_current_context)
|
||||
_empty_view.visible = not is_instance_valid(_current_context)
|
||||
|
||||
if not is_instance_valid(_current_context):
|
||||
return
|
||||
|
||||
_title_label.text = _current_context._editor_name()
|
||||
_title_label.tooltip_text = _current_context.resource_path
|
||||
|
||||
_action_mappings.clear()
|
||||
|
||||
for i in _current_context.mappings.size():
|
||||
var mapping = _current_context.mappings[i]
|
||||
|
||||
var mapping_editor = action_mapping_editor_scene.instantiate()
|
||||
mapping_editor.initialize(_plugin, _scanner)
|
||||
|
||||
_action_mappings.add_item(mapping_editor)
|
||||
|
||||
mapping_editor.edit(mapping)
|
||||
|
||||
_action_mappings.collapsed = _current_context.get_meta("_guide_action_mappings_collapsed", false)
|
||||
|
||||
func _on_action_mappings_add_requested():
|
||||
var mappings = _current_context.mappings.duplicate()
|
||||
var new_mapping := GUIDEActionMapping.new()
|
||||
# don't set an action because they should come from the file system
|
||||
mappings.append(new_mapping)
|
||||
|
||||
_undo_redo.create_action("Add action mapping")
|
||||
|
||||
_undo_redo.add_do_property(_current_context, "mappings", mappings)
|
||||
_undo_redo.add_undo_property(_current_context, "mappings", _current_context.mappings)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
func _on_action_mappings_move_requested(from:int, to:int):
|
||||
var mappings = _current_context.mappings.duplicate()
|
||||
var mapping = mappings[from]
|
||||
mappings.remove_at(from)
|
||||
if from < to:
|
||||
to -= 1
|
||||
mappings.insert(to, mapping)
|
||||
|
||||
_undo_redo.create_action("Move action mapping")
|
||||
|
||||
_undo_redo.add_do_property(_current_context, "mappings", mappings)
|
||||
_undo_redo.add_undo_property(_current_context, "mappings", _current_context.mappings)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
func _on_action_mapping_delete_requested(index:int):
|
||||
var mappings = _current_context.mappings.duplicate()
|
||||
mappings.remove_at(index)
|
||||
|
||||
_undo_redo.create_action("Delete action mapping")
|
||||
|
||||
_undo_redo.add_do_property(_current_context, "mappings", mappings)
|
||||
_undo_redo.add_undo_property(_current_context, "mappings", _current_context.mappings)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
|
||||
func _on_action_mappings_clear_requested():
|
||||
var mappings:Array[GUIDEActionMapping] = []
|
||||
|
||||
_undo_redo.create_action("Clear action mappings")
|
||||
|
||||
_undo_redo.add_do_property(_current_context, "mappings", mappings)
|
||||
_undo_redo.add_undo_property(_current_context, "mappings", _current_context.mappings)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
func _on_action_mapping_duplicate_requested(index:int):
|
||||
var mappings = _current_context.mappings.duplicate()
|
||||
var to_duplicate:GUIDEActionMapping = mappings[index]
|
||||
|
||||
var copy = GUIDEActionMapping.new()
|
||||
# don't set the action, because each mapping should have a unique mapping
|
||||
for input_mapping:GUIDEInputMapping in to_duplicate.input_mappings:
|
||||
var copied_input_mapping := GUIDEInputMapping.new()
|
||||
copied_input_mapping.input = Utils.duplicate_if_inline(input_mapping.input)
|
||||
for modifier in input_mapping.modifiers:
|
||||
copied_input_mapping.modifiers.append(Utils.duplicate_if_inline(modifier))
|
||||
|
||||
for trigger in input_mapping.triggers:
|
||||
copied_input_mapping.triggers.append(Utils.duplicate_if_inline(trigger))
|
||||
|
||||
copy.input_mappings.append(copied_input_mapping)
|
||||
|
||||
# insert the copy after the copied mapping
|
||||
mappings.insert(index+1, copy)
|
||||
|
||||
|
||||
_undo_redo.create_action("Duplicate action mapping")
|
||||
|
||||
_undo_redo.add_do_property(_current_context, "mappings", mappings)
|
||||
_undo_redo.add_undo_property(_current_context, "mappings", _current_context.mappings)
|
||||
|
||||
_undo_redo.commit_action()
|
||||
|
||||
func _on_action_mappings_collapse_state_changed(new_state:bool):
|
||||
_current_context.set_meta("_guide_action_mappings_collapsed", new_state)
|
||||
|
||||
|
@@ -0,0 +1,58 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://dm3hott3tfvwe"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/guide/editor/mapping_context_editor/mapping_context_editor.gd" id="1_vytdu"]
|
||||
[ext_resource type="PackedScene" uid="uid://361aipcef24h" path="res://addons/guide/editor/action_mapping_editor/action_mapping_editor.tscn" id="2_qb3p8"]
|
||||
[ext_resource type="PackedScene" uid="uid://cly0ff32fvpb2" path="res://addons/guide/editor/array_edit/array_edit.tscn" id="3_x7h5x"]
|
||||
|
||||
[node name="MappingContextEditor" type="MarginContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/margin_left = 5
|
||||
theme_override_constants/margin_top = 5
|
||||
theme_override_constants/margin_right = 5
|
||||
theme_override_constants/margin_bottom = 5
|
||||
script = ExtResource("1_vytdu")
|
||||
action_mapping_editor_scene = ExtResource("2_qb3p8")
|
||||
|
||||
[node name="EditingView" type="VBoxContainer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="EditingView"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="EditingView/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 6
|
||||
text = "narf.tres"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="EditingView"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_bottom = 5
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="EditingView"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="ActionMappings" parent="EditingView/ScrollContainer" instance=ExtResource("3_x7h5x")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
title = "Action mappings"
|
||||
add_tooltip = "Add action mapping"
|
||||
clear_tooltip = "Clear action mappings"
|
||||
|
||||
[node name="EmptyView" type="CenterContainer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="EmptyView"]
|
||||
layout_mode = 2
|
||||
text = "Create and open a GUIDEMappingContext to get started."
|
14
addons/guide/editor/modifier_slot/modifier_slot.gd
Normal file
@@ -0,0 +1,14 @@
|
||||
@tool
|
||||
extends "../resource_slot/resource_slot.gd"
|
||||
|
||||
var modifier:GUIDEModifier:
|
||||
set(value):
|
||||
_value = value
|
||||
get:
|
||||
return _value
|
||||
|
||||
func _accepts_drop_data(data:Resource) -> bool:
|
||||
return data is GUIDEModifier
|
||||
|
||||
|
||||
|
18
addons/guide/editor/modifier_slot/modifier_slot.tscn
Normal file
@@ -0,0 +1,18 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://ck5a30syo6bpo"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/guide/editor/modifier_slot/modifier_slot.gd" id="1_273m5"]
|
||||
|
||||
[node name="LineEdit" type="LineEdit"]
|
||||
offset_right = 1920.0
|
||||
offset_bottom = 31.0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 0
|
||||
text = "Name"
|
||||
editable = false
|
||||
context_menu_enabled = false
|
||||
virtual_keyboard_enabled = false
|
||||
shortcut_keys_enabled = false
|
||||
middle_mouse_paste_enabled = false
|
||||
selecting_enabled = false
|
||||
drag_and_drop_selection_enabled = false
|
||||
script = ExtResource("1_273m5")
|
106
addons/guide/editor/resource_slot/resource_slot.gd
Normal file
@@ -0,0 +1,106 @@
|
||||
@tool
|
||||
extends LineEdit
|
||||
|
||||
signal changed()
|
||||
const Utils = preload("../utils.gd")
|
||||
|
||||
func _ready():
|
||||
editable = false
|
||||
context_menu_enabled = false
|
||||
virtual_keyboard_enabled = false
|
||||
shortcut_keys_enabled = false
|
||||
selecting_enabled = false
|
||||
drag_and_drop_selection_enabled = false
|
||||
middle_mouse_paste_enabled = false
|
||||
|
||||
## The underlying resource. This is opened for editing when the user clicks on the control. Its also
|
||||
## used when dragging from the control.
|
||||
var _value:Resource = null:
|
||||
set(value):
|
||||
if _value == value:
|
||||
return
|
||||
|
||||
# stop tracking changes to the old resource (if any)
|
||||
if is_instance_valid(_value):
|
||||
_value.changed.disconnect(_update_from_value)
|
||||
|
||||
_value = value
|
||||
|
||||
# track changes to the resource itself
|
||||
if is_instance_valid(_value):
|
||||
_value.changed.connect(_update_from_value)
|
||||
|
||||
_update_from_value()
|
||||
changed.emit()
|
||||
|
||||
func _update_from_value():
|
||||
if not is_instance_valid(_value):
|
||||
text = "<none>"
|
||||
tooltip_text = ""
|
||||
remove_theme_color_override("font_uneditable_color")
|
||||
else:
|
||||
text = _value._editor_name()
|
||||
tooltip_text = _value.resource_path
|
||||
# if the value is shared, we override the font color to indicate that
|
||||
if not Utils.is_inline(_value):
|
||||
add_theme_color_override("font_uneditable_color", get_theme_color("accent_color", "Editor"))
|
||||
queue_redraw()
|
||||
else:
|
||||
remove_theme_color_override("font_uneditable_color")
|
||||
|
||||
## Can be overridden to handle the drop data. This method is called when the user drops something on the control.
|
||||
## If the value should be updated ,this method should set the _value property.
|
||||
func _do_drop_data(data:Resource):
|
||||
_value = data
|
||||
|
||||
|
||||
## Whether this control can accept drop data. This method is called when the user drags something over the control.
|
||||
func _accepts_drop_data(data:Resource) -> bool:
|
||||
return false
|
||||
|
||||
func _can_drop_data(at_position, data) -> bool:
|
||||
if data is Resource:
|
||||
return _accepts_drop_data(data)
|
||||
|
||||
if not data is Dictionary:
|
||||
return false
|
||||
|
||||
if data.has("files"):
|
||||
for file in data["files"]:
|
||||
if _accepts_drop_data(ResourceLoader.load(file)):
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
|
||||
func _drop_data(at_position, data) -> void:
|
||||
if data is Resource:
|
||||
_do_drop_data(data)
|
||||
return
|
||||
|
||||
for file in data["files"]:
|
||||
var item := ResourceLoader.load(file)
|
||||
_do_drop_data(item)
|
||||
|
||||
|
||||
func _get_drag_data(at_position: Vector2) -> Variant:
|
||||
if is_instance_valid(_value):
|
||||
var _preview := TextureRect.new()
|
||||
_preview.texture = get_theme_icon("File", "EditorIcons")
|
||||
set_drag_preview(_preview)
|
||||
# if the value is shared, we just hand out the resource path
|
||||
if not Utils.is_inline(_value):
|
||||
return {"files": [_value.resource_path]}
|
||||
else:
|
||||
# otherwise we hand out a shallow copy
|
||||
return _value.duplicate()
|
||||
else:
|
||||
return null
|
||||
|
||||
func _gui_input(event):
|
||||
if event is InputEventMouseButton:
|
||||
if event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
if is_instance_valid(_value):
|
||||
EditorInterface.edit_resource(_value)
|
||||
|
||||
|
14
addons/guide/editor/trigger_slot/trigger_slot.gd
Normal file
@@ -0,0 +1,14 @@
|
||||
@tool
|
||||
extends "../resource_slot/resource_slot.gd"
|
||||
|
||||
var trigger:GUIDETrigger:
|
||||
set(value):
|
||||
_value = value
|
||||
get:
|
||||
return _value
|
||||
|
||||
func _accepts_drop_data(data:Resource) -> bool:
|
||||
return data is GUIDETrigger
|
||||
|
||||
|
||||
|
20
addons/guide/editor/trigger_slot/trigger_slot.tscn
Normal file
@@ -0,0 +1,20 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://tk30wnstb0ku"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/guide/editor/trigger_slot/trigger_slot.gd" id="1_wxafc"]
|
||||
|
||||
[node name="LineEdit" type="LineEdit"]
|
||||
unique_name_in_owner = true
|
||||
offset_right = 1920.0
|
||||
offset_bottom = 31.0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 0
|
||||
tooltip_text = "Delete trigger"
|
||||
text = "Name"
|
||||
editable = false
|
||||
context_menu_enabled = false
|
||||
virtual_keyboard_enabled = false
|
||||
shortcut_keys_enabled = false
|
||||
middle_mouse_paste_enabled = false
|
||||
selecting_enabled = false
|
||||
drag_and_drop_selection_enabled = false
|
||||
script = ExtResource("1_wxafc")
|
22
addons/guide/editor/utils.gd
Normal file
@@ -0,0 +1,22 @@
|
||||
## Removes and frees all children of a node.
|
||||
static func clear(node:Node):
|
||||
if not is_instance_valid(node):
|
||||
return
|
||||
for child in node.get_children():
|
||||
node.remove_child(child)
|
||||
child.queue_free()
|
||||
|
||||
|
||||
## Checks if the given resource is an inline resource. If so, returns a shallow copy,
|
||||
## otherwise returns the resource. If the resource is null, returns null.
|
||||
static func duplicate_if_inline(resource:Resource) -> Resource:
|
||||
if is_inline(resource):
|
||||
return resource.duplicate()
|
||||
return resource
|
||||
|
||||
|
||||
## Checks if the given resource is an inline resource.
|
||||
static func is_inline(resource:Resource) -> bool:
|
||||
if resource == null:
|
||||
return false
|
||||
return resource.resource_path.contains("::") or resource.resource_path == ""
|
365
addons/guide/guide.gd
Normal file
@@ -0,0 +1,365 @@
|
||||
extends Node
|
||||
|
||||
const GUIDESet = preload("guide_set.gd")
|
||||
const GUIDEReset = preload("guide_reset.gd")
|
||||
const GUIDEInputTracker = preload("guide_input_tracker.gd")
|
||||
|
||||
## This is emitted whenever input mappings change (either due to mapping
|
||||
## contexts being enabled/disabled or remapping configs being re-applied or
|
||||
## joystick devices being connected/disconnected).
|
||||
## This is useful for updating UI prompts.
|
||||
signal input_mappings_changed()
|
||||
|
||||
## The currently active contexts. Key is the context, value is the priority
|
||||
var _active_contexts:Dictionary = {}
|
||||
## The currently active action mappings.
|
||||
var _active_action_mappings:Array[GUIDEActionMapping] = []
|
||||
|
||||
## The currently active remapping config.
|
||||
var _active_remapping_config:GUIDERemappingConfig
|
||||
|
||||
## All currently active inputs as collected from the active input mappings
|
||||
var _active_inputs:Array[GUIDEInput] = []
|
||||
|
||||
## A dictionary of actions sharing input. Key is the action, value
|
||||
## is an array of lower-priority actions that share input with the
|
||||
## key action.
|
||||
var _actions_sharing_input:Dictionary = {}
|
||||
|
||||
## A reference to the reset node which resets inputs that need a reset per frame
|
||||
## This is an extra node because the reset should run at the end of the frame
|
||||
## before new input is processed at the beginning of the frame.
|
||||
var _reset_node:GUIDEReset
|
||||
|
||||
|
||||
func _ready():
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
_reset_node = GUIDEReset.new()
|
||||
add_child(_reset_node)
|
||||
# attach to the current viewport to get input events
|
||||
GUIDEInputTracker._instrument.call_deferred(get_viewport())
|
||||
|
||||
get_tree().node_added.connect(_on_node_added)
|
||||
|
||||
# Emit a change of input mappings whenever a joystick was connected
|
||||
# or disconnected.
|
||||
Input.joy_connection_changed.connect(func(ig, ig2): input_mappings_changed.emit())
|
||||
|
||||
|
||||
## Called when a node is added to the tree. If the node is a window
|
||||
## GUIDE will instrument it to get events when the window is focused.
|
||||
func _on_node_added(node:Node) -> void:
|
||||
if not node is Window:
|
||||
return
|
||||
|
||||
GUIDEInputTracker._instrument(node)
|
||||
|
||||
|
||||
## Injects input into GUIDE. GUIDE will call this automatically but
|
||||
## can also be used to manually inject input for GUIDE to handle
|
||||
func inject_input(event:InputEvent) -> void:
|
||||
if event is InputEventAction:
|
||||
return # we don't react to Godot's built-in events
|
||||
|
||||
for input:GUIDEInput in _active_inputs:
|
||||
input._input(event)
|
||||
|
||||
|
||||
## Applies an input remapping config. This will override all input bindings in the
|
||||
## currently loaded mapping contexts with the bindings from the configuration.
|
||||
## Note that GUIDE will not track changes to the remapping config. If your remapping
|
||||
## config changes, you will need to call this method again.
|
||||
func set_remapping_config(config:GUIDERemappingConfig) -> void:
|
||||
_active_remapping_config = config
|
||||
_update_caches()
|
||||
|
||||
|
||||
## Enables the given context with the given priority. Lower numbers have higher priority. If
|
||||
## disable_others is set to true, all other currently enabled mapping contexts will be disabled.
|
||||
func enable_mapping_context(context:GUIDEMappingContext, disable_others:bool = false, priority:int = 0):
|
||||
if not is_instance_valid(context):
|
||||
push_error("Null context given. Ignoring.")
|
||||
return
|
||||
|
||||
if disable_others:
|
||||
_active_contexts.clear()
|
||||
|
||||
_active_contexts[context] = priority
|
||||
_update_caches()
|
||||
|
||||
|
||||
## Disables the given mapping context.
|
||||
func disable_mapping_context(context:GUIDEMappingContext):
|
||||
if not is_instance_valid(context):
|
||||
push_error("Null context given. Ignoring.")
|
||||
return
|
||||
|
||||
_active_contexts.erase(context)
|
||||
_update_caches()
|
||||
|
||||
|
||||
## Checks whether the given mapping context is currently enabled.
|
||||
func is_mapping_context_enabled(context:GUIDEMappingContext) -> bool:
|
||||
return _active_contexts.has(context)
|
||||
|
||||
|
||||
## Returns the currently enabled mapping contexts
|
||||
func get_enabled_mapping_contexts() -> Array[GUIDEMappingContext]:
|
||||
var result:Array[GUIDEMappingContext] = []
|
||||
for key in _active_contexts.keys():
|
||||
result.append(key)
|
||||
return result
|
||||
|
||||
|
||||
## Processes all currently active actions
|
||||
func _process(delta:float) -> void:
|
||||
var blocked_actions:GUIDESet = GUIDESet.new()
|
||||
|
||||
for action_mapping:GUIDEActionMapping in _active_action_mappings:
|
||||
|
||||
var action:GUIDEAction = action_mapping.action
|
||||
|
||||
# Walk over all input mappings for this action and consolidate state
|
||||
# and result value.
|
||||
var consolidated_value:Vector3 = Vector3.ZERO
|
||||
var consolidated_trigger_state:GUIDETrigger.GUIDETriggerState
|
||||
|
||||
for input_mapping:GUIDEInputMapping in action_mapping.input_mappings:
|
||||
input_mapping._update_state(delta, action.action_value_type)
|
||||
consolidated_value += input_mapping._value
|
||||
consolidated_trigger_state = max(consolidated_trigger_state, input_mapping._state)
|
||||
|
||||
# we do the blocking check only here because triggers may need to run anyways
|
||||
# (e.g. to collect hold times).
|
||||
if blocked_actions.has(action):
|
||||
consolidated_trigger_state = GUIDETrigger.GUIDETriggerState.NONE
|
||||
|
||||
if action.block_lower_priority_actions and \
|
||||
consolidated_trigger_state == GUIDETrigger.GUIDETriggerState.TRIGGERED and \
|
||||
_actions_sharing_input.has(action):
|
||||
for blocked_action in _actions_sharing_input[action]:
|
||||
blocked_actions.add(blocked_action)
|
||||
|
||||
|
||||
# Now state change events.
|
||||
match(action._last_state):
|
||||
GUIDEAction.GUIDEActionState.TRIGGERED:
|
||||
match(consolidated_trigger_state):
|
||||
GUIDETrigger.GUIDETriggerState.NONE:
|
||||
action._completed(consolidated_value)
|
||||
GUIDETrigger.GUIDETriggerState.ONGOING:
|
||||
action._ongoing(consolidated_value, delta)
|
||||
GUIDETrigger.GUIDETriggerState.TRIGGERED:
|
||||
action._triggered(consolidated_value, delta)
|
||||
|
||||
GUIDEAction.GUIDEActionState.ONGOING:
|
||||
match(consolidated_trigger_state):
|
||||
GUIDETrigger.GUIDETriggerState.NONE:
|
||||
action._cancelled(consolidated_value)
|
||||
GUIDETrigger.GUIDETriggerState.ONGOING:
|
||||
action._ongoing(consolidated_value, delta)
|
||||
GUIDETrigger.GUIDETriggerState.TRIGGERED:
|
||||
action._triggered(consolidated_value, delta)
|
||||
|
||||
GUIDEAction.GUIDEActionState.COMPLETED:
|
||||
match(consolidated_trigger_state):
|
||||
GUIDETrigger.GUIDETriggerState.NONE:
|
||||
# make sure the value updated but don't emit any other events
|
||||
action._update_value(consolidated_value)
|
||||
GUIDETrigger.GUIDETriggerState.ONGOING:
|
||||
action._started(consolidated_value)
|
||||
GUIDETrigger.GUIDETriggerState.TRIGGERED:
|
||||
action._triggered(consolidated_value, delta)
|
||||
|
||||
func _update_caches():
|
||||
# Notify existing inputs that they aren no longer required
|
||||
for input:GUIDEInput in _active_inputs:
|
||||
input._reset()
|
||||
input._end_usage()
|
||||
|
||||
# Cancel all actions, so they don't remain in weird states.
|
||||
for mapping:GUIDEActionMapping in _active_action_mappings:
|
||||
match mapping.action._last_state:
|
||||
GUIDEAction.GUIDEActionState.ONGOING:
|
||||
mapping.action._cancelled(Vector3.ZERO)
|
||||
GUIDEAction.GUIDEActionState.TRIGGERED:
|
||||
mapping.action._completed(Vector3.ZERO)
|
||||
# notify all modifiers they are no longer in use
|
||||
for input_mapping in mapping.input_mappings:
|
||||
for modifier in input_mapping.modifiers:
|
||||
modifier._end_usage()
|
||||
|
||||
_active_inputs.clear()
|
||||
_active_action_mappings.clear()
|
||||
_actions_sharing_input.clear()
|
||||
|
||||
var sorted_contexts:Array[Dictionary] = []
|
||||
|
||||
for context:GUIDEMappingContext in _active_contexts.keys():
|
||||
sorted_contexts.append({"context": context, "priority": _active_contexts[context]})
|
||||
|
||||
sorted_contexts.sort_custom( func(a,b): return a.priority < b.priority )
|
||||
|
||||
# The actions we already have processed. Same action may appear in different
|
||||
# contexts, so if we find the same action twice, only the first instance wins.
|
||||
var processed_actions:GUIDESet = GUIDESet.new()
|
||||
var consolidated_inputs:GUIDESet = GUIDESet.new()
|
||||
|
||||
for entry:Dictionary in sorted_contexts:
|
||||
var context:GUIDEMappingContext = entry.context
|
||||
for action_mapping:GUIDEActionMapping in context.mappings:
|
||||
var action := action_mapping.action
|
||||
# If the action was already configured in a higher priority context,
|
||||
# we'll skip it.
|
||||
if processed_actions.has(action):
|
||||
# skip
|
||||
continue
|
||||
|
||||
processed_actions.add(action)
|
||||
|
||||
# We consolidate the inputs here, so we'll internally build a new
|
||||
# action mapping that uses consolidated inputs rather than the
|
||||
# original ones. This achieves multiple things:
|
||||
# - if two actions check for the same input, we only need to
|
||||
# process the input once instead of twice.
|
||||
# - it allows us to prioritize input, if two actions check for
|
||||
# the same input. This way the first action can consume the
|
||||
# input and not have it affect further actions.
|
||||
# - we make sure nobody shares triggers as they are stateful and
|
||||
# should not be shared.
|
||||
|
||||
var effective_mapping = GUIDEActionMapping.new()
|
||||
effective_mapping.action = action
|
||||
|
||||
# now update the input mappings
|
||||
for index in action_mapping.input_mappings.size():
|
||||
var bound_input:GUIDEInput = action_mapping.input_mappings[index].input
|
||||
|
||||
# if the mapping has an override for the input, apply it.
|
||||
if _active_remapping_config != null and \
|
||||
_active_remapping_config._has(context, action, index):
|
||||
bound_input = _active_remapping_config._get_bound_input_or_null(context, action, index)
|
||||
|
||||
# make a new input mapping
|
||||
var new_input_mapping := GUIDEInputMapping.new()
|
||||
|
||||
# can be null for combo mappings, so check that
|
||||
if bound_input != null:
|
||||
# check if we already have this kind of input
|
||||
var existing = consolidated_inputs.first_match(func(it:GUIDEInput): return it.is_same_as(bound_input))
|
||||
if existing != null:
|
||||
# if we have this already, use the instance we have
|
||||
bound_input = existing
|
||||
else:
|
||||
# otherwise register this input into the consolidated input
|
||||
consolidated_inputs.add(bound_input)
|
||||
|
||||
new_input_mapping.input = bound_input
|
||||
# modifiers cannot be re-bound so we can just use the one
|
||||
# from the original configuration. this is also needed for shared
|
||||
# modifiers to work.
|
||||
new_input_mapping.modifiers = action_mapping.input_mappings[index].modifiers
|
||||
# triggers also cannot be re-bound but we still make a copy
|
||||
# to ensure that no shared triggers exist.
|
||||
new_input_mapping.triggers = []
|
||||
|
||||
for trigger in action_mapping.input_mappings[index].triggers:
|
||||
new_input_mapping.triggers.append(trigger.duplicate())
|
||||
|
||||
new_input_mapping._initialize()
|
||||
|
||||
# and add it to the new mapping
|
||||
effective_mapping.input_mappings.append(new_input_mapping)
|
||||
|
||||
|
||||
# if any binding remains, add the mapping to the list of active
|
||||
# action mappings
|
||||
if not effective_mapping.input_mappings.is_empty():
|
||||
_active_action_mappings.append(effective_mapping)
|
||||
|
||||
# now we have a new set of active inputs
|
||||
for input:GUIDEInput in consolidated_inputs.values():
|
||||
_active_inputs.append(input)
|
||||
|
||||
# prepare the action input share lookup table
|
||||
for i:int in _active_action_mappings.size():
|
||||
|
||||
var mapping = _active_action_mappings[i]
|
||||
|
||||
if mapping.action.block_lower_priority_actions:
|
||||
# first find out if the action uses any chorded actions and
|
||||
# collect all inputs that this action uses
|
||||
var chorded_actions:GUIDESet = GUIDESet.new()
|
||||
var inputs:GUIDESet = GUIDESet.new()
|
||||
var blocked_actions:GUIDESet = GUIDESet.new()
|
||||
for input_mapping:GUIDEInputMapping in mapping.input_mappings:
|
||||
if input_mapping.input != null:
|
||||
inputs.add(input_mapping.input)
|
||||
|
||||
for trigger:GUIDETrigger in input_mapping.triggers:
|
||||
if trigger is GUIDETriggerChordedAction and trigger.action != null:
|
||||
chorded_actions.add(trigger.action)
|
||||
|
||||
# Now the action that has a chorded action (A) needs to make sure that
|
||||
# the chorded action it depends upon (B) is not blocked (otherwise A would
|
||||
# never trigger) and if that chorded action (B) in turn depends on chorded actions. So
|
||||
# if chorded actions build a chain, we need to keep the full
|
||||
# chain unblocked. In addition we need to add the inputs of all
|
||||
# these chorded actions to the list of blocked inputs.
|
||||
for j:int in range(i+1, _active_action_mappings.size()):
|
||||
var inner_mapping = _active_action_mappings[j]
|
||||
# this is a chorded action that is used by one other action
|
||||
# in the chain.
|
||||
if chorded_actions.has(inner_mapping.action):
|
||||
for input_mapping:GUIDEInputMapping in inner_mapping.input_mappings:
|
||||
# put all of its inputs into the list of blocked inputs
|
||||
if input_mapping.input != null:
|
||||
inputs.add(input_mapping.input)
|
||||
|
||||
# also if this mapping in turn again depends on a chorded
|
||||
# action, ad this one to the list of chorded actions
|
||||
for trigger:GUIDETrigger in input_mapping.triggers:
|
||||
if trigger is GUIDETriggerChordedAction and trigger.action != null:
|
||||
chorded_actions.add(trigger.action)
|
||||
|
||||
# now find lower priority actions that share input
|
||||
for j:int in range(i+1, _active_action_mappings.size()):
|
||||
var inner_mapping = _active_action_mappings[j]
|
||||
if chorded_actions.has(inner_mapping.action):
|
||||
continue
|
||||
|
||||
for input_mapping:GUIDEInputMapping in inner_mapping.input_mappings:
|
||||
if input_mapping.input == null:
|
||||
continue
|
||||
|
||||
# because we consolidated input, we can now do an == comparison
|
||||
# to find equal input.
|
||||
if inputs.has(input_mapping.input):
|
||||
blocked_actions.add(inner_mapping.action)
|
||||
# we can continue to the next action
|
||||
break
|
||||
|
||||
if not blocked_actions.is_empty():
|
||||
_actions_sharing_input[mapping.action] = blocked_actions.values()
|
||||
|
||||
# finally collect which inputs we need to reset per frame
|
||||
_reset_node._inputs_to_reset.clear()
|
||||
for input:GUIDEInput in _active_inputs:
|
||||
if input._needs_reset():
|
||||
_reset_node._inputs_to_reset.append(input)
|
||||
# Notify inputs that GUIDE is about to use them
|
||||
input._begin_usage()
|
||||
|
||||
for mapping in _active_action_mappings:
|
||||
for input_mapping in mapping.input_mappings:
|
||||
# notify modifiers they will be used.
|
||||
for modifier in input_mapping.modifiers:
|
||||
modifier._begin_usage()
|
||||
|
||||
# and copy over the hold time threshold from the mapping
|
||||
mapping.action._trigger_hold_threshold = input_mapping._trigger_hold_threshold
|
||||
|
||||
# and notify interested parties that the input mappings have changed
|
||||
input_mappings_changed.emit()
|
||||
|
||||
|
254
addons/guide/guide_action.gd
Normal file
@@ -0,0 +1,254 @@
|
||||
@tool
|
||||
@icon("res://addons/guide/guide_action.svg")
|
||||
class_name GUIDEAction
|
||||
extends Resource
|
||||
|
||||
enum GUIDEActionValueType {
|
||||
BOOL = 0,
|
||||
AXIS_1D = 1,
|
||||
AXIS_2D = 2,
|
||||
AXIS_3D = 3
|
||||
}
|
||||
|
||||
enum GUIDEActionState {
|
||||
TRIGGERED,
|
||||
ONGOING,
|
||||
COMPLETED
|
||||
}
|
||||
|
||||
## The name of this action. Required when this action should be used as
|
||||
## Godot action. Also displayed in the debugger.
|
||||
@export var name:StringName:
|
||||
set(value):
|
||||
if name == value:
|
||||
return
|
||||
name = value
|
||||
emit_changed()
|
||||
|
||||
|
||||
## The action value type.
|
||||
@export var action_value_type: GUIDEActionValueType = GUIDEActionValueType.BOOL:
|
||||
set(value):
|
||||
if action_value_type == value:
|
||||
return
|
||||
action_value_type = value
|
||||
emit_changed()
|
||||
|
||||
## If this action triggers, lower-priority actions cannot trigger
|
||||
## if they share input with this action unless these actions are
|
||||
## chorded with this action.
|
||||
@export var block_lower_priority_actions:bool = true:
|
||||
set(value):
|
||||
if block_lower_priority_actions == value:
|
||||
return
|
||||
block_lower_priority_actions = value
|
||||
emit_changed()
|
||||
|
||||
|
||||
@export_category("Godot Actions")
|
||||
## If true, then this action will be emitted into Godot's
|
||||
## built-in action system. This can be helpful to interact with
|
||||
## code using this system, like Godot's UI system. Actions
|
||||
## will be emitted on trigger and completion (e.g. button down
|
||||
## and button up).
|
||||
@export var emit_as_godot_actions:bool = false:
|
||||
set(value):
|
||||
if emit_as_godot_actions == value:
|
||||
return
|
||||
emit_as_godot_actions = value
|
||||
emit_changed()
|
||||
|
||||
|
||||
@export_category("Action Remapping")
|
||||
|
||||
## If true, players can remap this action. To be remappable, make sure
|
||||
## that a name and the action type are properly set.
|
||||
@export var is_remappable:bool:
|
||||
set(value):
|
||||
if is_remappable == value:
|
||||
return
|
||||
is_remappable = value
|
||||
emit_changed()
|
||||
|
||||
## The display name of the action shown to the player.
|
||||
@export var display_name:String:
|
||||
set(value):
|
||||
if display_name == value:
|
||||
return
|
||||
display_name = value
|
||||
emit_changed()
|
||||
|
||||
## The display category of the action shown to the player.
|
||||
@export var display_category:String:
|
||||
set(value):
|
||||
if display_category == value:
|
||||
return
|
||||
display_category = value
|
||||
emit_changed()
|
||||
|
||||
## Emitted every frame while the action is triggered.
|
||||
signal triggered()
|
||||
|
||||
## Emitted when the action started evaluating.
|
||||
signal started()
|
||||
|
||||
## Emitted every frame while the action is still evaluating.
|
||||
signal ongoing()
|
||||
|
||||
## Emitted when the action finished evaluating.
|
||||
signal completed()
|
||||
|
||||
## Emitted when the action was cancelled.
|
||||
signal cancelled()
|
||||
|
||||
var _last_state:GUIDEActionState = GUIDEActionState.COMPLETED
|
||||
|
||||
var _value_bool:bool
|
||||
## Returns the value of this action as bool.
|
||||
var value_bool:bool:
|
||||
get: return _value_bool
|
||||
|
||||
## Returns the value of this action as float.
|
||||
var value_axis_1d:float:
|
||||
get: return _value.x
|
||||
|
||||
var _value_axis_2d:Vector2 = Vector2.ZERO
|
||||
## Returns the value of this action as Vector2.
|
||||
var value_axis_2d:Vector2:
|
||||
get: return _value_axis_2d
|
||||
|
||||
var _value:Vector3 = Vector3.ZERO
|
||||
## Returns the value of this action as Vector3.
|
||||
var value_axis_3d:Vector3:
|
||||
get: return _value
|
||||
|
||||
|
||||
var _elapsed_seconds:float
|
||||
## The amount of seconds elapsed since the action started evaluating.
|
||||
var elapsed_seconds:float:
|
||||
get: return _elapsed_seconds
|
||||
|
||||
var _elapsed_ratio:float
|
||||
## The ratio of the elapsed time to the hold time. This is a percentage
|
||||
## of the hold time that has passed. If the action has no hold time, this will
|
||||
## be 0 when the action is not triggered and 1 when the action is triggered.
|
||||
## Otherwise, this will be a value between 0 and 1.
|
||||
var elapsed_ratio:float:
|
||||
get: return _elapsed_ratio
|
||||
|
||||
var _triggered_seconds:float
|
||||
## The amount of seconds elapsed since the action triggered.
|
||||
var triggered_seconds:float:
|
||||
get: return _triggered_seconds
|
||||
|
||||
|
||||
## This is a hint for how long the input must remain actuated (in seconds) before the action triggers.
|
||||
## It depends on the mapping in which this action is used. If the mapping has no hold trigger it will be -1.
|
||||
## In general, you should not access this variable directly, but rather the `elapsed_ratio` property of the action
|
||||
## which is a percentage of the hold time that has passed.
|
||||
var _trigger_hold_threshold:float = -1.0
|
||||
|
||||
func _triggered(value:Vector3, delta:float) -> void:
|
||||
_triggered_seconds += delta
|
||||
_elapsed_ratio = 1.0
|
||||
_update_value(value)
|
||||
_last_state = GUIDEActionState.TRIGGERED
|
||||
triggered.emit()
|
||||
_emit_godot_action_maybe(true)
|
||||
|
||||
func _started(value:Vector3) -> void:
|
||||
_elapsed_ratio = 0.0
|
||||
_update_value(value)
|
||||
_last_state = GUIDEActionState.ONGOING
|
||||
started.emit()
|
||||
ongoing.emit()
|
||||
|
||||
func _ongoing(value:Vector3, delta:float) -> void:
|
||||
_elapsed_seconds += delta
|
||||
if _trigger_hold_threshold > 0:
|
||||
_elapsed_ratio = _elapsed_seconds / _trigger_hold_threshold
|
||||
_update_value(value)
|
||||
var was_triggered:bool = _last_state == GUIDEActionState.TRIGGERED
|
||||
_last_state = GUIDEActionState.ONGOING
|
||||
ongoing.emit()
|
||||
# if the action reverts from triggered to ongoing, this counts as
|
||||
# releasing the action for the godot action system.
|
||||
if was_triggered:
|
||||
_emit_godot_action_maybe(false)
|
||||
|
||||
|
||||
func _cancelled(value:Vector3) -> void:
|
||||
_elapsed_seconds = 0
|
||||
_elapsed_ratio = 0
|
||||
_update_value(value)
|
||||
_last_state = GUIDEActionState.COMPLETED
|
||||
cancelled.emit()
|
||||
completed.emit()
|
||||
|
||||
func _completed(value:Vector3) -> void:
|
||||
_elapsed_seconds = 0
|
||||
_elapsed_ratio = 0
|
||||
_triggered_seconds = 0
|
||||
_update_value(value)
|
||||
_last_state = GUIDEActionState.COMPLETED
|
||||
completed.emit()
|
||||
_emit_godot_action_maybe(false)
|
||||
|
||||
func _emit_godot_action_maybe(pressed:bool) -> void:
|
||||
if not emit_as_godot_actions:
|
||||
return
|
||||
|
||||
if name.is_empty():
|
||||
push_error("Cannot emit action into Godot's system because name is empty.")
|
||||
return
|
||||
|
||||
var godot_action = InputEventAction.new()
|
||||
godot_action.action = name
|
||||
godot_action.strength = _value.x
|
||||
godot_action.pressed = pressed
|
||||
Input.parse_input_event(godot_action)
|
||||
|
||||
func _update_value(value:Vector3):
|
||||
match action_value_type:
|
||||
GUIDEActionValueType.BOOL, GUIDEActionValueType.AXIS_1D:
|
||||
_value_bool = abs(value.x) > 0
|
||||
_value_axis_2d = Vector2(abs(value.x), 0)
|
||||
_value = Vector3(value.x, 0, 0)
|
||||
GUIDEActionValueType.AXIS_2D:
|
||||
_value_bool = abs(value.x) > 0
|
||||
_value_axis_2d = Vector2(value.x, value.y)
|
||||
_value = Vector3(value.x, value.y, 0)
|
||||
GUIDEActionValueType.AXIS_3D:
|
||||
_value_bool = abs(value.x) > 0
|
||||
_value_axis_2d = Vector2(value.x, value.y)
|
||||
_value = value
|
||||
|
||||
## Returns whether the action is currently triggered. Can be used for a
|
||||
## polling style input.
|
||||
func is_triggered() -> bool:
|
||||
return _last_state == GUIDEActionState.TRIGGERED
|
||||
|
||||
|
||||
## Returns whether the action is currently completed. Can be used for a
|
||||
## polling style input.
|
||||
func is_completed() -> bool:
|
||||
return _last_state == GUIDEActionState.COMPLETED
|
||||
|
||||
|
||||
## Returns whether the action is currently completed. Can be used for a
|
||||
## polling style input.
|
||||
func is_ongoing() -> bool:
|
||||
return _last_state == GUIDEActionState.ONGOING
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
# Try to give the most user friendly name
|
||||
if display_name != "":
|
||||
return display_name
|
||||
|
||||
if name != "":
|
||||
return name
|
||||
|
||||
return resource_path.get_file().replace(".tres", "")
|
||||
|
||||
|
7
addons/guide/guide_action.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1.52323,0,0,1.5171,-6.78788,-8.07906)">
|
||||
<path d="M15.976,5.375L16.311,7.01C16.958,7.11 17.593,7.281 18.203,7.52L19.308,6.271C19.926,6.553 20.515,6.895 21.067,7.291L20.543,8.876C21.054,9.287 21.519,9.753 21.928,10.267L23.506,9.74C23.901,10.294 24.241,10.886 24.522,11.507L23.279,12.615C23.517,13.228 23.687,13.866 23.786,14.516L25.415,14.852C25.481,15.53 25.481,16.213 25.415,16.892L23.786,17.228C23.687,17.878 23.517,18.515 23.279,19.128L24.522,20.237C24.241,20.857 23.901,21.449 23.506,22.004L21.928,21.477C21.519,21.99 21.054,22.457 20.543,22.868L21.067,24.452C20.515,24.849 19.926,25.19 19.308,25.472L18.203,24.224C17.593,24.463 16.958,24.634 16.311,24.733L15.976,26.369C15.3,26.435 14.62,26.435 13.944,26.369L13.61,24.733C12.962,24.634 12.327,24.463 11.717,24.224L10.613,25.472C9.995,25.19 9.405,24.849 8.853,24.452L9.378,22.868C8.867,22.457 8.402,21.99 7.992,21.477L6.414,22.004C6.019,21.449 5.679,20.857 5.398,20.237L6.642,19.128C6.404,18.515 6.234,17.878 6.135,17.228L4.505,16.892C4.44,16.213 4.44,15.53 4.505,14.852L6.135,14.516C6.234,13.866 6.404,13.228 6.642,12.615L5.398,11.507C5.679,10.886 6.019,10.294 6.414,9.74L7.992,10.267C8.402,9.753 8.867,9.287 9.378,8.876L8.853,7.291C9.405,6.895 9.995,6.553 10.613,6.271L11.717,7.52C12.327,7.281 12.962,7.11 13.61,7.01L13.944,5.375C14.62,5.309 15.3,5.309 15.976,5.375ZM20.25,20.88L15.736,9.545L14.184,9.545L9.67,20.88L10.852,20.88C10.983,20.88 11.093,20.843 11.183,20.769C11.272,20.695 11.335,20.611 11.372,20.516L12.427,17.779L17.493,17.779L18.549,20.516C18.591,20.622 18.654,20.709 18.738,20.777C18.822,20.846 18.932,20.88 19.069,20.88L20.25,20.88ZM12.853,16.672L14.625,12.068C14.678,11.931 14.73,11.773 14.783,11.594C14.841,11.409 14.899,11.209 14.956,10.993C15.067,11.42 15.179,11.776 15.295,12.061L17.068,16.672L12.853,16.672Z" style="fill:rgb(253,150,0);"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
38
addons/guide/guide_action.svg.import
Normal file
@@ -0,0 +1,38 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bei7cw115tks0"
|
||||
path="res://.godot/imported/guide_action.svg-4d1dfb47183d95c4796078798ce2d0ab.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/guide/guide_action.svg"
|
||||
dest_files=["res://.godot/imported/guide_action.svg-4d1dfb47183d95c4796078798ce2d0ab.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=0.5
|
||||
editor/scale_with_editor_scale=true
|
||||
editor/convert_colors_with_editor_theme=false
|
21
addons/guide/guide_action_mapping.gd
Normal file
@@ -0,0 +1,21 @@
|
||||
@icon("res://addons/guide/guide_internal.svg")
|
||||
@tool
|
||||
## An action to input mapping
|
||||
class_name GUIDEActionMapping
|
||||
extends Resource
|
||||
|
||||
## The action to be mapped
|
||||
@export var action:GUIDEAction:
|
||||
set(value):
|
||||
if value == action:
|
||||
return
|
||||
action = value
|
||||
emit_changed()
|
||||
|
||||
## A set of input mappings that can trigger the action
|
||||
@export var input_mappings:Array[GUIDEInputMapping] = []:
|
||||
set(value):
|
||||
if value == input_mappings:
|
||||
return
|
||||
input_mappings = value
|
||||
emit_changed()
|
177
addons/guide/guide_input_mapping.gd
Normal file
@@ -0,0 +1,177 @@
|
||||
@icon("res://addons/guide/guide_internal.svg")
|
||||
@tool
|
||||
## A mapping from actuated input to a trigger result
|
||||
class_name GUIDEInputMapping
|
||||
extends Resource
|
||||
|
||||
## Whether the remapping configuration in this input mapping
|
||||
## should override the configuration of the bound action. Enable
|
||||
## this, to give a key a custom name or category for remapping.
|
||||
@export var override_action_settings:bool = false:
|
||||
set(value):
|
||||
if override_action_settings == value:
|
||||
return
|
||||
override_action_settings = value
|
||||
emit_changed()
|
||||
|
||||
## If true, players can remap this input mapping. Note that the
|
||||
## action to which this input is bound also needs to be remappable
|
||||
## for this setting to have an effect.
|
||||
@export var is_remappable:bool = false:
|
||||
set(value):
|
||||
if is_remappable == value:
|
||||
return
|
||||
is_remappable = value
|
||||
emit_changed()
|
||||
|
||||
## The display name of the input mapping shown to the player. If empty,
|
||||
## the display name of the action is used.
|
||||
@export var display_name:String = "":
|
||||
set(value):
|
||||
if display_name == value:
|
||||
return
|
||||
display_name = value
|
||||
emit_changed()
|
||||
|
||||
## The display category of the input mapping. If empty, the display name of the
|
||||
## action is used.
|
||||
@export var display_category:String = "":
|
||||
set(value):
|
||||
if display_category == value:
|
||||
return
|
||||
display_category = value
|
||||
emit_changed()
|
||||
|
||||
|
||||
@export_group("Mappings")
|
||||
## The input to be actuated
|
||||
@export var input:GUIDEInput:
|
||||
set(value):
|
||||
if value == input:
|
||||
return
|
||||
input = value
|
||||
emit_changed()
|
||||
|
||||
|
||||
## A list of modifiers that preprocess the actuated input before
|
||||
## it is fed to the triggers.
|
||||
@export var modifiers:Array[GUIDEModifier] = []:
|
||||
set(value):
|
||||
if value == modifiers:
|
||||
return
|
||||
modifiers = value
|
||||
emit_changed()
|
||||
|
||||
|
||||
## A list of triggers that could trigger the mapped action.
|
||||
@export var triggers:Array[GUIDETrigger] = []:
|
||||
set(value):
|
||||
if value == triggers:
|
||||
return
|
||||
triggers = value
|
||||
emit_changed()
|
||||
|
||||
## Hint for how long the input must remain actuated (in seconds) before the mapping triggers.
|
||||
## If the mapping has no hold trigger it will be -1. If it has multiple hold triggers
|
||||
## the shortest hold time will be used.
|
||||
var _trigger_hold_threshold:float = -1.0
|
||||
|
||||
var _state:GUIDETrigger.GUIDETriggerState = GUIDETrigger.GUIDETriggerState.NONE
|
||||
var _value:Vector3 = Vector3.ZERO
|
||||
|
||||
var _trigger_list:Array[GUIDETrigger] = []
|
||||
var _implicit_count:int = 0
|
||||
var _explicit_count:int = 0
|
||||
|
||||
## Called when the mapping is started to be used by GUIDE. Calculates
|
||||
## the number of implicit and explicit triggers so we don't need to do this
|
||||
## per frame. Also creates a default trigger when none is set.
|
||||
func _initialize() -> void :
|
||||
_trigger_list.clear()
|
||||
|
||||
_implicit_count = 0
|
||||
_explicit_count = 0
|
||||
_trigger_hold_threshold = -1.0
|
||||
|
||||
if triggers.is_empty():
|
||||
# make a default trigger and use that
|
||||
var default_trigger = GUIDETriggerDown.new()
|
||||
default_trigger.actuation_threshold = 0
|
||||
_explicit_count = 1
|
||||
_trigger_list.append(default_trigger)
|
||||
return
|
||||
|
||||
for trigger in triggers:
|
||||
match trigger._get_trigger_type():
|
||||
GUIDETrigger.GUIDETriggerType.EXPLICIT:
|
||||
_explicit_count += 1
|
||||
GUIDETrigger.GUIDETriggerType.IMPLICIT:
|
||||
_implicit_count += 1
|
||||
_trigger_list.append(trigger)
|
||||
|
||||
# collect the hold threshold for hinting the UI about how long
|
||||
# the input must be held down. This is only relevant for hold triggers
|
||||
if trigger is GUIDETriggerHold:
|
||||
if _trigger_hold_threshold == -1:
|
||||
_trigger_hold_threshold = trigger.hold_treshold
|
||||
else:
|
||||
_trigger_hold_threshold = min(_trigger_hold_threshold, trigger.hold_treshold)
|
||||
|
||||
|
||||
|
||||
func _update_state(delta:float, value_type:GUIDEAction.GUIDEActionValueType):
|
||||
# Collect the current input value
|
||||
var input_value:Vector3 = input._value if input != null else Vector3.ZERO
|
||||
|
||||
# Run it through all modifiers
|
||||
for modifier:GUIDEModifier in modifiers:
|
||||
input_value = modifier._modify_input(input_value, delta, value_type)
|
||||
|
||||
_value = input_value
|
||||
|
||||
var triggered_implicits:int = 0
|
||||
var triggered_explicits:int = 0
|
||||
var triggered_blocked:int = 0
|
||||
|
||||
# Run over all triggers
|
||||
var result:int = GUIDETrigger.GUIDETriggerState.NONE
|
||||
for trigger:GUIDETrigger in _trigger_list:
|
||||
var trigger_result:GUIDETrigger.GUIDETriggerState = trigger._update_state(_value, delta, value_type)
|
||||
trigger._last_value = _value
|
||||
|
||||
var trigger_type = trigger._get_trigger_type()
|
||||
if trigger_result == GUIDETrigger.GUIDETriggerState.TRIGGERED:
|
||||
match trigger_type:
|
||||
GUIDETrigger.GUIDETriggerType.EXPLICIT:
|
||||
triggered_explicits += 1
|
||||
GUIDETrigger.GUIDETriggerType.IMPLICIT:
|
||||
triggered_implicits += 1
|
||||
GUIDETrigger.GUIDETriggerType.BLOCKING:
|
||||
triggered_blocked += 1
|
||||
|
||||
# we only care about the nuances of explicit triggers. implicits and blocking
|
||||
# can only really return yes or no, so they have no nuance
|
||||
if trigger_type == GUIDETrigger.GUIDETriggerType.EXPLICIT:
|
||||
# Higher value results take precedence over lower value results
|
||||
result = max(result, trigger_result)
|
||||
|
||||
# final collection
|
||||
if triggered_blocked > 0:
|
||||
# some blocker triggered which means that this cannot succeed
|
||||
_state = GUIDETrigger.GUIDETriggerState.NONE
|
||||
return
|
||||
|
||||
if triggered_implicits < _implicit_count:
|
||||
# not all implicits triggered, which also fails this binding
|
||||
_state = GUIDETrigger.GUIDETriggerState.NONE
|
||||
return
|
||||
|
||||
if _explicit_count == 0 and _implicit_count > 0:
|
||||
# if no explicits exist, its enough when all implicits trigger
|
||||
_state = GUIDETrigger.GUIDETriggerState.TRIGGERED
|
||||
return
|
||||
|
||||
# return the best result
|
||||
_state = result
|
||||
|
||||
|
26
addons/guide/guide_input_tracker.gd
Normal file
@@ -0,0 +1,26 @@
|
||||
## Tracker that tracks input for a window and injects it into GUIDE.
|
||||
## Will automatically keep track of sub-windows.
|
||||
extends Node
|
||||
|
||||
## Instruments a sub-window so it forwards input events to GUIDE.
|
||||
static func _instrument(viewport:Viewport):
|
||||
if viewport.has_meta("x-guide-instrumented"):
|
||||
return
|
||||
|
||||
var tracker = preload("guide_input_tracker.gd").new()
|
||||
tracker.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
viewport.add_child(tracker, false, Node.INTERNAL_MODE_BACK)
|
||||
viewport.gui_focus_changed.connect(tracker._control_focused)
|
||||
|
||||
## Catches unhandled input and forwards it to GUIDE
|
||||
func _unhandled_input(event:InputEvent):
|
||||
GUIDE.inject_input(event)
|
||||
|
||||
## Some ... creative code ... to catch events from popup windows
|
||||
## that are spawned by Godot's control nodes.
|
||||
func _control_focused(control:Control):
|
||||
if control is OptionButton or control is ColorPickerButton \
|
||||
or control is MenuButton or control is TabContainer:
|
||||
_instrument(control.get_popup())
|
||||
|
||||
|
12
addons/guide/guide_internal.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1.07241,0,0,1.07396,-3.11767,-2.34767)">
|
||||
<path d="M17.827,2.164C26.061,2.164 32.747,8.85 32.747,17.084C32.747,25.319 26.061,32.004 17.827,32.004C9.592,32.004 2.907,25.319 2.907,17.084C2.907,8.85 9.592,2.164 17.827,2.164ZM17.827,4.857C11.08,4.857 5.604,10.337 5.604,17.084C5.604,23.831 11.08,29.311 17.827,29.311C24.574,29.311 30.05,23.831 30.05,17.084C30.05,10.337 24.574,4.857 17.827,4.857Z" style="fill:rgb(253,150,0);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-5.23265,-2.69876)">
|
||||
<g transform="matrix(24,0,0,24,11.6286,27.2968)">
|
||||
<path d="M0.407,-0.071C0.426,-0.071 0.444,-0.071 0.46,-0.073C0.476,-0.075 0.491,-0.078 0.506,-0.082C0.52,-0.085 0.533,-0.09 0.546,-0.095C0.559,-0.1 0.571,-0.106 0.584,-0.113L0.584,-0.271L0.473,-0.271C0.467,-0.271 0.462,-0.272 0.458,-0.276C0.454,-0.28 0.452,-0.284 0.452,-0.29L0.452,-0.345L0.672,-0.345L0.672,-0.07C0.654,-0.057 0.635,-0.045 0.616,-0.036C0.596,-0.026 0.575,-0.018 0.553,-0.011C0.531,-0.005 0.507,0 0.482,0.003C0.457,0.006 0.429,0.008 0.4,0.008C0.347,0.008 0.3,-0.001 0.257,-0.019C0.213,-0.037 0.176,-0.062 0.145,-0.094C0.113,-0.126 0.089,-0.165 0.071,-0.21C0.054,-0.255 0.045,-0.304 0.045,-0.358C0.045,-0.413 0.054,-0.463 0.071,-0.508C0.088,-0.553 0.112,-0.591 0.144,-0.623C0.176,-0.655 0.215,-0.68 0.26,-0.698C0.305,-0.716 0.356,-0.725 0.412,-0.725C0.44,-0.725 0.466,-0.722 0.491,-0.718C0.515,-0.714 0.537,-0.708 0.558,-0.7C0.579,-0.692 0.598,-0.683 0.616,-0.672C0.634,-0.661 0.65,-0.648 0.666,-0.634L0.638,-0.59C0.632,-0.581 0.625,-0.577 0.616,-0.577C0.611,-0.577 0.605,-0.578 0.599,-0.582C0.59,-0.587 0.581,-0.592 0.571,-0.599C0.56,-0.606 0.548,-0.612 0.533,-0.618C0.518,-0.624 0.5,-0.63 0.48,-0.634C0.46,-0.638 0.436,-0.641 0.409,-0.641C0.368,-0.641 0.332,-0.634 0.299,-0.621C0.266,-0.608 0.239,-0.589 0.216,-0.564C0.193,-0.54 0.175,-0.51 0.163,-0.475C0.15,-0.44 0.144,-0.401 0.144,-0.358C0.144,-0.313 0.15,-0.272 0.163,-0.237C0.176,-0.201 0.194,-0.171 0.218,-0.147C0.241,-0.122 0.269,-0.103 0.301,-0.09C0.333,-0.077 0.368,-0.071 0.407,-0.071Z" style="fill:rgb(253,150,0);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
38
addons/guide/guide_internal.svg.import
Normal file
@@ -0,0 +1,38 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://ddkj7kntb4fit"
|
||||
path="res://.godot/imported/guide_internal.svg-560a143a1e289215e72d8844f5173844.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/guide/guide_internal.svg"
|
||||
dest_files=["res://.godot/imported/guide_internal.svg-560a143a1e289215e72d8844f5173844.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=0.5
|
||||
editor/scale_with_editor_scale=true
|
||||
editor/convert_colors_with_editor_theme=false
|
30
addons/guide/guide_mapping_context.gd
Normal file
@@ -0,0 +1,30 @@
|
||||
@tool
|
||||
@icon("res://addons/guide/guide_mapping_context.svg")
|
||||
class_name GUIDEMappingContext
|
||||
extends Resource
|
||||
|
||||
const GUIDESet = preload("guide_set.gd")
|
||||
|
||||
## The display name for this mapping context during action remapping
|
||||
@export var display_name:String:
|
||||
set(value):
|
||||
if value == display_name:
|
||||
return
|
||||
display_name = value
|
||||
emit_changed()
|
||||
|
||||
## The mappings. Do yourself a favour and use the G.U.I.D.E panel
|
||||
## to edit these.
|
||||
@export var mappings:Array[GUIDEActionMapping] = []:
|
||||
set(value):
|
||||
if value == mappings:
|
||||
return
|
||||
mappings = value
|
||||
emit_changed()
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
if display_name.is_empty():
|
||||
return resource_path.get_file()
|
||||
else:
|
||||
return display_name
|
9
addons/guide/guide_mapping_context.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1.16508,0,-1.89607)">
|
||||
<g id="MappingContext">
|
||||
<path d="M11.289,19.641L0.424,19.641L0.424,11.08L11.289,11.08L11.289,1.973L21.263,1.973L21.263,11.08L31.576,11.08L31.576,19.641L21.263,19.641L21.263,28.711L11.289,28.711L11.289,19.641ZM16,3.344L12.35,9.609L19.65,9.609L16,3.344ZM2.016,15.342L9.316,18.475L9.316,12.209L2.016,15.342ZM30,15.36L22.7,12.228L22.7,18.493L30,15.36ZM16,27.377L19.65,21.111L12.35,21.111L16,27.377Z" style="fill:rgb(253,150,0);"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 972 B |
38
addons/guide/guide_mapping_context.svg.import
Normal file
@@ -0,0 +1,38 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bcwpqc8016n7b"
|
||||
path="res://.godot/imported/guide_mapping_context.svg-025f10fbbdb2bb11a96754ab9b725bea.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/guide/guide_mapping_context.svg"
|
||||
dest_files=["res://.godot/imported/guide_mapping_context.svg-025f10fbbdb2bb11a96754ab9b725bea.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=0.5
|
||||
editor/scale_with_editor_scale=true
|
||||
editor/convert_colors_with_editor_theme=false
|
13
addons/guide/guide_reset.gd
Normal file
@@ -0,0 +1,13 @@
|
||||
extends Node
|
||||
|
||||
|
||||
var _inputs_to_reset:Array[GUIDEInput] = []
|
||||
|
||||
func _enter_tree() -> void:
|
||||
# this should run at the end of the frame, so we put in a low priority (= high number)
|
||||
process_priority = 10000000
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
func _process(delta: float) -> void:
|
||||
for input:GUIDEInput in _inputs_to_reset:
|
||||
input._reset()
|
40
addons/guide/guide_set.gd
Normal file
@@ -0,0 +1,40 @@
|
||||
## Helper class for modelling sets
|
||||
var _values:Dictionary = {}
|
||||
|
||||
func add(value:Variant) -> void:
|
||||
_values[value] = value
|
||||
|
||||
|
||||
func remove(value:Variant) -> void:
|
||||
_values.erase(value)
|
||||
|
||||
|
||||
func clear() -> void:
|
||||
_values.clear()
|
||||
|
||||
func is_empty() -> bool:
|
||||
return _values.is_empty()
|
||||
|
||||
|
||||
func pull() -> Variant:
|
||||
if is_empty():
|
||||
return null
|
||||
|
||||
var key = _values.keys()[0]
|
||||
remove(key)
|
||||
return key
|
||||
|
||||
|
||||
func has(value:Variant) -> bool:
|
||||
return _values.has(value)
|
||||
|
||||
## Returns the first item for which the given matcher function returns
|
||||
## a true value.
|
||||
func first_match(matcher:Callable) -> Variant:
|
||||
for key in _values.keys():
|
||||
if matcher.call(key):
|
||||
return key
|
||||
return null
|
||||
|
||||
func values() -> Array:
|
||||
return _values.keys()
|
50
addons/guide/inputs/guide_input.gd
Normal file
@@ -0,0 +1,50 @@
|
||||
@tool
|
||||
@icon("res://addons/guide/inputs/guide_input.svg")
|
||||
## A class representing some actuated input.
|
||||
class_name GUIDEInput
|
||||
extends Resource
|
||||
|
||||
## The current valueo f this input. Depending on the input type only parts of the
|
||||
## returned vector may be relevant.
|
||||
var _value:Vector3 = Vector3.ZERO
|
||||
|
||||
## Whether this input needs a reset per frame. _input is only called when
|
||||
## there is input happening, but some GUIDE inputs may need to be reset
|
||||
## in the absence of input.
|
||||
func _needs_reset() -> bool:
|
||||
return false
|
||||
|
||||
## Resets the input value to the default value. Is called once per frame if
|
||||
## _needs_reset returns true.
|
||||
func _reset() -> void:
|
||||
_value = Vector3.ZERO
|
||||
|
||||
## Called when an input event happens. Should update the
|
||||
## the input value of this input.
|
||||
func _input(event:InputEvent):
|
||||
pass
|
||||
|
||||
## Returns whether this input is the same input as the other input.
|
||||
func is_same_as(other:GUIDEInput) -> bool:
|
||||
return false
|
||||
|
||||
## Called when the input is started to be used by GUIDE. Can be used to perform
|
||||
## initializations.
|
||||
func _begin_usage() -> void :
|
||||
pass
|
||||
|
||||
## Called, when the input is no longer used by GUIDE. Can be used to perform
|
||||
## cleanup.
|
||||
func _end_usage() -> void:
|
||||
pass
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return ""
|
||||
|
||||
func _editor_description() -> String:
|
||||
return ""
|
||||
|
||||
|
||||
func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
|
||||
return -1
|
12
addons/guide/inputs/guide_input.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1.07241,0,0,1.07396,-3.11767,-2.34767)">
|
||||
<path d="M17.827,2.164C26.061,2.164 32.747,8.85 32.747,17.084C32.747,25.319 26.061,32.004 17.827,32.004C9.592,32.004 2.907,25.319 2.907,17.084C2.907,8.85 9.592,2.164 17.827,2.164ZM17.827,4.857C11.08,4.857 5.604,10.337 5.604,17.084C5.604,23.831 11.08,29.311 17.827,29.311C24.574,29.311 30.05,23.831 30.05,17.084C30.05,10.337 24.574,4.857 17.827,4.857Z" style="fill:rgb(253,150,0);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0.687353,-2.69876)">
|
||||
<g transform="matrix(24,0,0,24,11.6286,27.2968)">
|
||||
<rect x="0.105" y="-0.717" width="0.097" height="0.717" style="fill:rgb(253,150,0);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
38
addons/guide/inputs/guide_input.svg.import
Normal file
@@ -0,0 +1,38 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://oku7f5t0ox3r"
|
||||
path="res://.godot/imported/guide_input.svg-d7e8ae255db039e6a02cccc3f844cc0e.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/guide/inputs/guide_input.svg"
|
||||
dest_files=["res://.godot/imported/guide_input.svg-d7e8ae255db039e6a02cccc3f844cc0e.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=0.5
|
||||
editor/scale_with_editor_scale=true
|
||||
editor/convert_colors_with_editor_theme=false
|
59
addons/guide/inputs/guide_input_action.gd
Normal file
@@ -0,0 +1,59 @@
|
||||
## An input that mirrors the action's value while the action is triggered.
|
||||
@tool
|
||||
class_name GUIDEInputAction
|
||||
extends GUIDEInput
|
||||
|
||||
## The action that this input should mirror. This is live tracked, so any change in
|
||||
## the action will update the input.
|
||||
@export var action:GUIDEAction:
|
||||
set(value):
|
||||
if value == action:
|
||||
return
|
||||
action = value
|
||||
emit_changed()
|
||||
|
||||
func _begin_usage():
|
||||
if is_instance_valid(action):
|
||||
action.triggered.connect(_on)
|
||||
action.completed.connect(_off)
|
||||
action.ongoing.connect(_off)
|
||||
if action.is_triggered():
|
||||
_on()
|
||||
return
|
||||
# not triggered or no action.
|
||||
_off()
|
||||
|
||||
|
||||
func _end_usage():
|
||||
if is_instance_valid(action):
|
||||
action.triggered.disconnect(_on)
|
||||
action.completed.disconnect(_off)
|
||||
action.ongoing.disconnect(_off)
|
||||
|
||||
|
||||
func _on() -> void:
|
||||
# on is only called when the action is actually existing, so this is
|
||||
# always not-null here
|
||||
_value = action.value_axis_3d
|
||||
|
||||
func _off() -> void:
|
||||
_value = Vector3.ZERO
|
||||
|
||||
|
||||
func is_same_as(other:GUIDEInput) -> bool:
|
||||
return other is GUIDEInputAction and other.action == action
|
||||
|
||||
|
||||
func _to_string():
|
||||
return "(GUIDEInputAction: " + str(action) + ")"
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Action"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "An input that mirrors the action's value while the action is triggered."
|
||||
|
||||
|
||||
func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
|
||||
return GUIDEAction.GUIDEActionValueType.AXIS_3D
|
115
addons/guide/inputs/guide_input_any.gd
Normal file
@@ -0,0 +1,115 @@
|
||||
## Input that triggers if any input from the given device class
|
||||
## is given. Only looks for button inputs, not axis inputs as axes
|
||||
## have a tendency to accidentally trigger.
|
||||
@tool
|
||||
class_name GUIDEInputAny
|
||||
extends GUIDEInput
|
||||
|
||||
|
||||
## Should input from mouse buttons be considered? Deprecated, use
|
||||
## mouse_buttons instead.
|
||||
## @deprecated
|
||||
var mouse:bool:
|
||||
get: return mouse_buttons
|
||||
set(value): mouse_buttons = value
|
||||
|
||||
## Should input from joy buttons be considered. Deprecated, use
|
||||
## joy_buttons instead.
|
||||
## @deprecated
|
||||
var joy:bool:
|
||||
get: return joy_buttons
|
||||
set(value): joy_buttons = value
|
||||
|
||||
## Should input from mouse buttons be considered?
|
||||
@export var mouse_buttons:bool = false
|
||||
|
||||
## Should input from mouse movement be considered?
|
||||
@export var mouse_movement:bool = false
|
||||
|
||||
## Minimum movement distance of the mouse before it is considered
|
||||
## moving.
|
||||
@export var minimum_mouse_movement_distance:float = 1.0
|
||||
|
||||
## Should input from gamepad/joystick buttons be considered?
|
||||
@export var joy_buttons:bool = false
|
||||
|
||||
## Should input from gamepad/joystick axes be considered?
|
||||
@export var joy_axes:bool = false
|
||||
|
||||
## Minimum strength of a single joy axis actuation before it is considered
|
||||
## as actuated.
|
||||
@export var minimum_joy_axis_actuation_strength:float = 0.2
|
||||
|
||||
## Should input from the keyboard be considered?
|
||||
@export var keyboard:bool = false
|
||||
|
||||
## Should input from touch be considered?
|
||||
@export var touch:bool = false
|
||||
|
||||
|
||||
func _needs_reset() -> bool:
|
||||
# Needs reset because we cannot detect the absence of input.
|
||||
return true
|
||||
|
||||
func _input(event:InputEvent):
|
||||
if mouse_buttons and event is InputEventMouseButton:
|
||||
_value = Vector3.RIGHT
|
||||
return
|
||||
|
||||
if mouse_movement and event is InputEventMouseMotion \
|
||||
and event.relative.length() >= minimum_mouse_movement_distance:
|
||||
_value = Vector3.RIGHT
|
||||
return
|
||||
|
||||
if joy_buttons and event is InputEventJoypadButton:
|
||||
_value = Vector3.RIGHT
|
||||
return
|
||||
|
||||
if joy_axes and event is InputEventJoypadMotion \
|
||||
and abs(event.axis_value) >= minimum_joy_axis_actuation_strength:
|
||||
_value = Vector3.RIGHT
|
||||
return
|
||||
|
||||
if keyboard and event is InputEventKey:
|
||||
_value = Vector3.RIGHT
|
||||
return
|
||||
|
||||
if touch and (event is InputEventScreenTouch or event is InputEventScreenDrag):
|
||||
_value = Vector3.RIGHT
|
||||
return
|
||||
|
||||
_value = Vector3.ZERO
|
||||
|
||||
|
||||
func is_same_as(other:GUIDEInput) -> bool:
|
||||
return other is GUIDEInputAny and \
|
||||
other.mouse == mouse and \
|
||||
other.joy == joy and \
|
||||
other.keyboard == keyboard
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Any Input"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Input that triggers if any input from the given device class is given."
|
||||
|
||||
|
||||
func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
|
||||
return GUIDEAction.GUIDEActionValueType.BOOL
|
||||
|
||||
# support for legacy properties
|
||||
func _get_property_list():
|
||||
return [
|
||||
{
|
||||
"name": "mouse",
|
||||
"type": TYPE_BOOL,
|
||||
"usage": PROPERTY_USAGE_NO_EDITOR
|
||||
},
|
||||
{
|
||||
"name": "joy",
|
||||
"type": TYPE_BOOL,
|
||||
"usage": PROPERTY_USAGE_NO_EDITOR
|
||||
}
|
||||
]
|
||||
|
43
addons/guide/inputs/guide_input_joy_axis_1d.gd
Normal file
@@ -0,0 +1,43 @@
|
||||
## Input from a single joy axis.
|
||||
@tool
|
||||
class_name GUIDEInputJoyAxis1D
|
||||
extends GUIDEInputJoyBase
|
||||
|
||||
## The joy axis to sample
|
||||
@export var axis:JoyAxis = JOY_AXIS_LEFT_X:
|
||||
set(value):
|
||||
if value == axis:
|
||||
return
|
||||
axis = value
|
||||
emit_changed()
|
||||
|
||||
func _input(event:InputEvent):
|
||||
if not event is InputEventJoypadMotion:
|
||||
return
|
||||
|
||||
if event.axis != axis:
|
||||
return
|
||||
|
||||
if joy_index > -1 and event.device != _joy_id:
|
||||
return
|
||||
|
||||
_value.x = event.axis_value
|
||||
|
||||
|
||||
func is_same_as(other:GUIDEInput) -> bool:
|
||||
return other is GUIDEInputJoyAxis1D and \
|
||||
other.axis == axis and \
|
||||
other.joy_index == joy_index
|
||||
|
||||
func _to_string():
|
||||
return "(GUIDEInputJoyAxis1D: axis=" + str(axis) + ", joy_index=" + str(joy_index) + ")"
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Joy Axis 1D"
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "The input from a single joy axis."
|
||||
|
||||
|
||||
func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
|
||||
return GUIDEAction.GUIDEActionValueType.AXIS_1D
|
58
addons/guide/inputs/guide_input_joy_axis_2d.gd
Normal file
@@ -0,0 +1,58 @@
|
||||
## Input from two joy axes.
|
||||
class_name GUIDEInputJoyAxis2D
|
||||
extends GUIDEInputJoyBase
|
||||
|
||||
## The joy axis to sample for x input.
|
||||
@export var x:JoyAxis = JOY_AXIS_LEFT_X:
|
||||
set(value):
|
||||
if value == x:
|
||||
return
|
||||
x = value
|
||||
emit_changed()
|
||||
|
||||
|
||||
## The joy axis to sample for y input.
|
||||
@export var y:JoyAxis = JOY_AXIS_LEFT_Y:
|
||||
set(value):
|
||||
if value == y:
|
||||
return
|
||||
y = value
|
||||
emit_changed()
|
||||
|
||||
|
||||
func _input(event:InputEvent):
|
||||
if not event is InputEventJoypadMotion:
|
||||
return
|
||||
|
||||
if event.axis != x and event.axis != y:
|
||||
return
|
||||
|
||||
if joy_index > -1 and event.device != _joy_id:
|
||||
return
|
||||
|
||||
if event.axis == x:
|
||||
_value.x = event.axis_value
|
||||
return
|
||||
|
||||
if event.axis == y:
|
||||
_value.y = event.axis_value
|
||||
|
||||
func is_same_as(other:GUIDEInput) -> bool:
|
||||
return other is GUIDEInputJoyAxis2D and \
|
||||
other.x == x and \
|
||||
other.y == y and \
|
||||
other.joy_index == joy_index
|
||||
|
||||
func _to_string():
|
||||
return "(GUIDEInputJoyAxis2D: x=" + str(x) + ", y=" + str(y) + ", joy_index=" + str(joy_index) + ")"
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Joy Axis 2D"
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "The input from two Joy axes. Usually from a stick."
|
||||
|
||||
|
||||
func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
|
||||
return GUIDEAction.GUIDEActionValueType.AXIS_2D
|
35
addons/guide/inputs/guide_input_joy_base.gd
Normal file
@@ -0,0 +1,35 @@
|
||||
## Base class for joystick inputs.
|
||||
@tool
|
||||
class_name GUIDEInputJoyBase
|
||||
extends GUIDEInput
|
||||
|
||||
## The index of the connected joy pad to check. If -1 checks all joypads.
|
||||
@export var joy_index:int = -1:
|
||||
set(value):
|
||||
if value == joy_index:
|
||||
return
|
||||
joy_index = value
|
||||
emit_changed()
|
||||
|
||||
## Cached joystick ID if we use a joy index.
|
||||
var _joy_id:int = -2
|
||||
|
||||
func _begin_usage():
|
||||
Input.joy_connection_changed.connect(_update_joy_id)
|
||||
_update_joy_id(null, null)
|
||||
|
||||
func _end_usage():
|
||||
Input.joy_connection_changed.disconnect(_update_joy_id)
|
||||
|
||||
func _update_joy_id(_ignore, _ignore2):
|
||||
if joy_index < 0:
|
||||
return
|
||||
|
||||
var joypads:Array[int] = Input.get_connected_joypads()
|
||||
if joy_index < joypads.size():
|
||||
_joy_id = joypads[joy_index]
|
||||
else:
|
||||
push_warning("Only ", joypads.size(), " joy pads/sticks connected. Cannot sample in put from index ", joy_index, ".")
|
||||
_joy_id = -2
|
||||
|
||||
|
44
addons/guide/inputs/guide_input_joy_button.gd
Normal file
@@ -0,0 +1,44 @@
|
||||
@tool
|
||||
class_name GUIDEInputJoyButton
|
||||
extends GUIDEInputJoyBase
|
||||
|
||||
@export var button:JoyButton = JOY_BUTTON_A:
|
||||
set(value):
|
||||
if value == button:
|
||||
return
|
||||
button = value
|
||||
emit_changed()
|
||||
|
||||
func _input(event:InputEvent):
|
||||
if not event is InputEventJoypadButton:
|
||||
return
|
||||
|
||||
if event.button_index != button:
|
||||
return
|
||||
|
||||
|
||||
if joy_index > -1 and event.device != _joy_id:
|
||||
return
|
||||
|
||||
_value.x = 1.0 if event.pressed else 0.0
|
||||
|
||||
|
||||
func is_same_as(other:GUIDEInput) -> bool:
|
||||
return other is GUIDEInputJoyButton and \
|
||||
other.button == button and \
|
||||
other.joy_index == joy_index
|
||||
|
||||
|
||||
func _to_string():
|
||||
return "(GUIDEInputJoyButton: button=" + str(button) + ", joy_index=" + str(joy_index) + ")"
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Joy Button"
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "A button press from a joy button."
|
||||
|
||||
|
||||
func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
|
||||
return GUIDEAction.GUIDEActionValueType.BOOL
|
127
addons/guide/inputs/guide_input_key.gd
Normal file
@@ -0,0 +1,127 @@
|
||||
@tool
|
||||
class_name GUIDEInputKey
|
||||
extends GUIDEInput
|
||||
|
||||
## The physical keycode of the key.
|
||||
@export var key:Key:
|
||||
set(value):
|
||||
if value == key:
|
||||
return
|
||||
key = value
|
||||
emit_changed()
|
||||
|
||||
|
||||
@export_group("Modifiers")
|
||||
## Whether shift must be pressed.
|
||||
@export var shift:bool = false:
|
||||
set(value):
|
||||
if value == shift:
|
||||
return
|
||||
shift = value
|
||||
emit_changed()
|
||||
|
||||
## Whether control must be pressed.
|
||||
@export var control:bool = false:
|
||||
set(value):
|
||||
if value == control:
|
||||
return
|
||||
control = value
|
||||
emit_changed()
|
||||
|
||||
## Whether alt must be pressed.
|
||||
@export var alt:bool = false:
|
||||
set(value):
|
||||
if value == alt:
|
||||
return
|
||||
alt = value
|
||||
emit_changed()
|
||||
|
||||
|
||||
## Whether meta/win/cmd must be pressed.
|
||||
@export var meta:bool = false:
|
||||
set(value):
|
||||
if value == meta:
|
||||
return
|
||||
meta = value
|
||||
emit_changed()
|
||||
|
||||
## Whether this input should fire if additional
|
||||
## modifier keys are currently pressed.
|
||||
@export var allow_additional_modifiers:bool = true:
|
||||
set(value):
|
||||
if value == allow_additional_modifiers:
|
||||
return
|
||||
allow_additional_modifiers = value
|
||||
emit_changed()
|
||||
|
||||
|
||||
|
||||
func _input(event:InputEvent):
|
||||
if not event is InputEventKey:
|
||||
return
|
||||
|
||||
# we start assuming the key is not pressed right now
|
||||
_value.x = 0.0
|
||||
|
||||
# the key itself must be pressed
|
||||
if not Input.is_physical_key_pressed(key):
|
||||
return
|
||||
|
||||
# every required modifier must be pressed
|
||||
if shift and not Input.is_physical_key_pressed(KEY_SHIFT):
|
||||
return
|
||||
|
||||
if control and not Input.is_physical_key_pressed(KEY_CTRL):
|
||||
return
|
||||
|
||||
if alt and not Input.is_physical_key_pressed(KEY_ALT):
|
||||
return
|
||||
|
||||
if meta and not Input.is_physical_key_pressed(KEY_META):
|
||||
return
|
||||
|
||||
# unless additional modifiers are allowed, every
|
||||
# unselected modifier must not be pressed (except if the
|
||||
# bound key is actually the modifier itself)
|
||||
|
||||
if not allow_additional_modifiers:
|
||||
if not shift and key != KEY_SHIFT and Input.is_physical_key_pressed(KEY_SHIFT):
|
||||
return
|
||||
|
||||
if not control and key != KEY_CTRL and Input.is_physical_key_pressed(KEY_CTRL):
|
||||
return
|
||||
|
||||
if not alt and key != KEY_ALT and Input.is_physical_key_pressed(KEY_ALT):
|
||||
return
|
||||
|
||||
if not meta and key != KEY_META and Input.is_physical_key_pressed(KEY_META):
|
||||
return
|
||||
|
||||
# we're still here, so all required keys are pressed and
|
||||
# no extra keys are pressed
|
||||
|
||||
_value.x = 1.0
|
||||
|
||||
|
||||
func is_same_as(other:GUIDEInput) -> bool:
|
||||
return other is GUIDEInputKey \
|
||||
and other.key == key \
|
||||
and other.shift == shift \
|
||||
and other.control == control \
|
||||
and other.alt == alt \
|
||||
and other.meta == meta \
|
||||
and other.allow_additional_modifiers == allow_additional_modifiers
|
||||
|
||||
func _to_string():
|
||||
return "(GUIDEInputKey: key=" + str(key) + ", shift=" + str(shift) + ", alt=" + str(alt) + ", control=" + str(control) + ", meta="+ str(meta) + ")"
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Key"
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "A button press on the keyboard."
|
||||
|
||||
|
||||
func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
|
||||
return GUIDEAction.GUIDEActionValueType.BOOL
|
47
addons/guide/inputs/guide_input_mouse_axis_1d.gd
Normal file
@@ -0,0 +1,47 @@
|
||||
@tool
|
||||
class_name GUIDEInputMouseAxis1D
|
||||
extends GUIDEInput
|
||||
|
||||
enum GUIDEInputMouseAxis {
|
||||
X,
|
||||
Y
|
||||
}
|
||||
|
||||
@export var axis:GUIDEInputMouseAxis:
|
||||
set(value):
|
||||
if value == axis:
|
||||
return
|
||||
axis = value
|
||||
emit_changed()
|
||||
|
||||
# we don't get mouse updates when the mouse is not moving, so this needs to be
|
||||
# reset every frame
|
||||
func _needs_reset() -> bool:
|
||||
return true
|
||||
|
||||
func _input(event:InputEvent) -> void:
|
||||
if event is InputEventMouseMotion:
|
||||
match axis:
|
||||
GUIDEInputMouseAxis.X:
|
||||
_value.x = event.relative.x
|
||||
GUIDEInputMouseAxis.Y:
|
||||
_value.x = event.relative.y
|
||||
|
||||
|
||||
func is_same_as(other:GUIDEInput):
|
||||
return other is GUIDEInputMouseAxis1D and other.axis == axis
|
||||
|
||||
func _to_string():
|
||||
return "(GUIDEInputMouseAxis1D: axis=" + str(axis) + ")"
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Mouse Axis 1D"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Relative mouse movement on a single axis."
|
||||
|
||||
|
||||
func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
|
||||
return GUIDEAction.GUIDEActionValueType.AXIS_1D
|
35
addons/guide/inputs/guide_input_mouse_axis_2d.gd
Normal file
@@ -0,0 +1,35 @@
|
||||
@tool
|
||||
class_name GUIDEInputMouseAxis2D
|
||||
extends GUIDEInput
|
||||
|
||||
|
||||
# we don't get mouse updates when the mouse is not moving, so this needs to be
|
||||
# reset every frame
|
||||
func _needs_reset() -> bool:
|
||||
return true
|
||||
|
||||
func _input(event:InputEvent) -> void:
|
||||
if not event is InputEventMouseMotion:
|
||||
return
|
||||
|
||||
_value.x = event.relative.x
|
||||
_value.y = event.relative.y
|
||||
|
||||
func is_same_as(other:GUIDEInput):
|
||||
return other is GUIDEInputMouseAxis2D
|
||||
|
||||
|
||||
func _to_string():
|
||||
return "(GUIDEInputMouseAxis2D)"
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Mouse Axis 2D"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Relative mouse movement on 2 axes."
|
||||
|
||||
|
||||
func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
|
||||
return GUIDEAction.GUIDEActionValueType.AXIS_2D
|
65
addons/guide/inputs/guide_input_mouse_button.gd
Normal file
@@ -0,0 +1,65 @@
|
||||
@tool
|
||||
class_name GUIDEInputMouseButton
|
||||
extends GUIDEInput
|
||||
|
||||
|
||||
@export var button:MouseButton = MOUSE_BUTTON_LEFT:
|
||||
set(value):
|
||||
if value == button:
|
||||
return
|
||||
button = value
|
||||
emit_changed()
|
||||
|
||||
|
||||
func _needs_reset():
|
||||
# mouse wheel up and down can potentially send multiple inputs within a single frame
|
||||
# so we need to smooth this out a bit.
|
||||
return button == MOUSE_BUTTON_WHEEL_UP or button == MOUSE_BUTTON_WHEEL_DOWN
|
||||
|
||||
var _reset_to:Vector3
|
||||
var _was_pressed_this_frame:bool
|
||||
|
||||
func _reset() -> void:
|
||||
_was_pressed_this_frame = false
|
||||
_value = _reset_to
|
||||
|
||||
|
||||
func _input(event:InputEvent):
|
||||
if not event is InputEventMouseButton:
|
||||
return
|
||||
|
||||
if event.button_index != button:
|
||||
return
|
||||
|
||||
|
||||
if _needs_reset():
|
||||
# we always reset to the last event we received in a frame
|
||||
# so after the frame is over we're still in sync.
|
||||
_reset_to.x = 1.0 if event.pressed else 0.0
|
||||
|
||||
if event.pressed:
|
||||
_was_pressed_this_frame = true
|
||||
|
||||
if not event.pressed and _was_pressed_this_frame:
|
||||
# keep pressed state for this frame
|
||||
return
|
||||
|
||||
_value.x = 1.0 if event.pressed else 0.0
|
||||
|
||||
func is_same_as(other:GUIDEInput) -> bool:
|
||||
return other is GUIDEInputMouseButton and other.button == button
|
||||
|
||||
|
||||
func _to_string():
|
||||
return "(GUIDEInputMouseButton: button=" + str(button) + ")"
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Mouse Button"
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "A press of a mouse button. The mouse wheel is also a button."
|
||||
|
||||
|
||||
func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
|
||||
return GUIDEAction.GUIDEActionValueType.BOOL
|
41
addons/guide/inputs/guide_input_mouse_position.gd
Normal file
@@ -0,0 +1,41 @@
|
||||
@tool
|
||||
class_name GUIDEInputMousePosition
|
||||
extends GUIDEInput
|
||||
|
||||
|
||||
func _begin_usage() -> void :
|
||||
_update_mouse_position()
|
||||
|
||||
|
||||
func _input(event:InputEvent) -> void:
|
||||
if not event is InputEventMouseMotion:
|
||||
return
|
||||
|
||||
_update_mouse_position()
|
||||
|
||||
|
||||
func _update_mouse_position():
|
||||
var position:Vector2 = Engine.get_main_loop().root.get_mouse_position()
|
||||
|
||||
_value.x = position.x
|
||||
_value.y = position.y
|
||||
|
||||
|
||||
func is_same_as(other:GUIDEInput):
|
||||
return other is GUIDEInputMousePosition
|
||||
|
||||
|
||||
func _to_string():
|
||||
return "(GUIDEInputMousePosition)"
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Mouse Position"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Position of the mouse in the main viewport."
|
||||
|
||||
|
||||
func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
|
||||
return GUIDEAction.GUIDEActionValueType.AXIS_2D
|
83
addons/guide/inputs/guide_input_touch_angle.gd
Normal file
@@ -0,0 +1,83 @@
|
||||
## Input representing angle changes between two fingers.
|
||||
@tool
|
||||
class_name GUIDEInputTouchAngle
|
||||
extends GUIDEInput
|
||||
|
||||
const GUIDETouchState = preload("guide_touch_state.gd")
|
||||
|
||||
## Unit in which the angle should be provided
|
||||
enum AngleUnit {
|
||||
## Angle is provided in radians
|
||||
RADIANS = 0,
|
||||
## Angle is provided in degrees.
|
||||
DEGREES = 1
|
||||
}
|
||||
|
||||
## The unit in which the angle should be provided
|
||||
@export var unit:AngleUnit = AngleUnit.RADIANS
|
||||
|
||||
var _initial_angle:float = INF
|
||||
|
||||
# We use the reset call to calculate the angle for this frame
|
||||
# so it can serve as reference for the next frame
|
||||
func _needs_reset() -> bool:
|
||||
return true
|
||||
|
||||
func _reset():
|
||||
var angle = _calculate_angle()
|
||||
# update initial angle when input is actuated or stops being actuated
|
||||
if is_finite(_initial_angle) != is_finite(angle):
|
||||
_initial_angle = angle
|
||||
|
||||
func _input(event:InputEvent) -> void:
|
||||
if not GUIDETouchState.process_input_event(event):
|
||||
# not touch-related
|
||||
return
|
||||
|
||||
var angle := _calculate_angle()
|
||||
# if either current angle or initial angle is not set,
|
||||
# we are zero
|
||||
if not is_finite(angle) or not is_finite(_initial_angle):
|
||||
_value = Vector3.ZERO
|
||||
return
|
||||
|
||||
# we assume that _initial_distance is never 0 because
|
||||
# you cannot have two fingers physically at the same place
|
||||
# on a touch screen
|
||||
_value = Vector3(angle - _initial_angle, 0, 0)
|
||||
|
||||
|
||||
func _calculate_angle() -> float:
|
||||
var pos1:Vector2 = GUIDETouchState.get_finger_position(0, 2)
|
||||
# if we have no position for first finger, we can immediately abort
|
||||
if not pos1.is_finite():
|
||||
return INF
|
||||
|
||||
var pos2:Vector2 = GUIDETouchState.get_finger_position(1, 2)
|
||||
# if there is no second finger, we can abort as well
|
||||
if not pos2.is_finite():
|
||||
return INF
|
||||
|
||||
# calculate distance for the fingers
|
||||
return -pos1.angle_to_point(pos2)
|
||||
|
||||
|
||||
func is_same_as(other:GUIDEInput):
|
||||
return other is GUIDEInputTouchAngle and \
|
||||
other.unit == unit
|
||||
|
||||
|
||||
func _to_string():
|
||||
return "(GUIDEInputTouchAngle unit=" + ("radians" if unit == AngleUnit.RADIANS else "degrees") + ")"
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Touch Angle"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Angle changes of two touching fingers."
|
||||
|
||||
|
||||
func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
|
||||
return GUIDEAction.GUIDEActionValueType.AXIS_1D
|
44
addons/guide/inputs/guide_input_touch_axis_1d.gd
Normal file
@@ -0,0 +1,44 @@
|
||||
@tool
|
||||
class_name GUIDEInputTouchAxis1D
|
||||
extends GUIDEInputTouchAxisBase
|
||||
|
||||
enum GUIDEInputTouchAxis {
|
||||
X,
|
||||
Y
|
||||
}
|
||||
|
||||
@export var axis:GUIDEInputTouchAxis:
|
||||
set(value):
|
||||
if value == axis:
|
||||
return
|
||||
axis = value
|
||||
emit_changed()
|
||||
|
||||
func is_same_as(other:GUIDEInput):
|
||||
return other is GUIDEInputTouchAxis1D and \
|
||||
other.finger_count == finger_count and \
|
||||
other.finger_index == finger_index and \
|
||||
other.axis == axis
|
||||
|
||||
func _apply_value(value:Vector2):
|
||||
match axis:
|
||||
GUIDEInputTouchAxis.X:
|
||||
_value = Vector3(value.x, 0, 0)
|
||||
GUIDEInputTouchAxis.Y:
|
||||
_value = Vector3(value.y, 0, 0)
|
||||
|
||||
func _to_string():
|
||||
return "(GUIDEInputTouchAxis1D finger_count=" + str(finger_count) + \
|
||||
" finger_index=" + str(finger_index) +" axis=" + ("X" if axis == GUIDEInputTouchAxis.X else "Y") + ")"
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Touch Axis1D"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Relative movement of a touching finger on a single axis."
|
||||
|
||||
|
||||
func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
|
||||
return GUIDEAction.GUIDEActionValueType.AXIS_1D
|
27
addons/guide/inputs/guide_input_touch_axis_2d.gd
Normal file
@@ -0,0 +1,27 @@
|
||||
@tool
|
||||
class_name GUIDEInputTouchAxis2D
|
||||
extends GUIDEInputTouchAxisBase
|
||||
|
||||
func _apply_value(value:Vector2):
|
||||
_value = Vector3(value.x, value.y, 0)
|
||||
|
||||
func is_same_as(other:GUIDEInput):
|
||||
return other is GUIDEInputTouchAxis2D and \
|
||||
other.finger_count == finger_count and \
|
||||
other.finger_index == finger_index
|
||||
|
||||
|
||||
func _to_string():
|
||||
return "(GUIDEInputTouchAxis2D finger_count=" + str(finger_count) + \
|
||||
" finger_index=" + str(finger_index) +")"
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Touch Axis2D"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "2D relative movement of a touching finger."
|
||||
|
||||
func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
|
||||
return GUIDEAction.GUIDEActionValueType.AXIS_2D
|
46
addons/guide/inputs/guide_input_touch_axis_base.gd
Normal file
@@ -0,0 +1,46 @@
|
||||
## Base class for axis-like touch input.
|
||||
@tool
|
||||
class_name GUIDEInputTouchAxisBase
|
||||
extends GUIDEInputTouchBase
|
||||
|
||||
const GUIDETouchState = preload("guide_touch_state.gd")
|
||||
|
||||
var _last_position:Vector2 = Vector2.INF
|
||||
|
||||
# We use the reset call to calculate the position for this frame
|
||||
# so it can serve as reference for the next frame
|
||||
func _needs_reset() -> bool:
|
||||
return true
|
||||
|
||||
func _reset() -> void:
|
||||
_last_position = GUIDETouchState.get_finger_position(finger_index, finger_count)
|
||||
_apply_value(_calculate_value(_last_position))
|
||||
|
||||
func _input(event:InputEvent) -> void:
|
||||
if not GUIDETouchState.process_input_event(event):
|
||||
# not touch-related
|
||||
return
|
||||
|
||||
# calculate live position from the cache
|
||||
var new_position:Vector2 = GUIDETouchState.get_finger_position(finger_index, finger_count)
|
||||
|
||||
_apply_value(_calculate_value(new_position))
|
||||
|
||||
func _apply_value(value:Vector2):
|
||||
pass
|
||||
|
||||
func _calculate_value(new_position:Vector2) -> Vector2:
|
||||
# if we cannot calculate a delta because old or new position
|
||||
# are undefined, we say the delta is zero
|
||||
if not _last_position.is_finite() or not new_position.is_finite():
|
||||
return Vector2.ZERO
|
||||
|
||||
return new_position - _last_position
|
||||
|
||||
|
||||
func is_same_as(other:GUIDEInput):
|
||||
return other is GUIDEInputTouchAxis2D and \
|
||||
other.finger_count == finger_count and \
|
||||
other.finger_index == finger_index
|
||||
|
||||
|
22
addons/guide/inputs/guide_input_touch_base.gd
Normal file
@@ -0,0 +1,22 @@
|
||||
## Base class for generic touch input
|
||||
@tool
|
||||
class_name GUIDEInputTouchBase
|
||||
extends GUIDEInput
|
||||
|
||||
## The number of fingers to be tracked.
|
||||
@export_range(1, 5, 1, "or_greater") var finger_count:int = 1:
|
||||
set(value):
|
||||
if value < 1:
|
||||
value = 1
|
||||
finger_count = value
|
||||
emit_changed()
|
||||
|
||||
## The index of the finger for which the position/delta should be reported
|
||||
## (0 = first finger, 1 = second finger, etc.). If -1, reports the average position/delta for
|
||||
## all fingers currently touching.
|
||||
@export_range(-1, 4, 1, "or_greater") var finger_index:int = 0:
|
||||
set(value):
|
||||
if value < -1:
|
||||
value = -1
|
||||
finger_index = value
|
||||
emit_changed()
|
72
addons/guide/inputs/guide_input_touch_distance.gd
Normal file
@@ -0,0 +1,72 @@
|
||||
## Input representing the distance changes between two fingers.
|
||||
@tool
|
||||
class_name GUIDEInputTouchDistance
|
||||
extends GUIDEInput
|
||||
|
||||
const GUIDETouchState = preload("guide_touch_state.gd")
|
||||
|
||||
var _initial_distance:float = INF
|
||||
|
||||
# We use the reset call to calculate the distance for this frame
|
||||
# so it can serve as reference for the next frame
|
||||
func _needs_reset() -> bool:
|
||||
return true
|
||||
|
||||
func _reset():
|
||||
var distance = _calculate_distance()
|
||||
# update initial distance when input is actuated or stops being actuated
|
||||
if is_finite(_initial_distance) != is_finite(distance):
|
||||
_initial_distance = distance
|
||||
|
||||
|
||||
func _input(event:InputEvent) -> void:
|
||||
if not GUIDETouchState.process_input_event(event):
|
||||
# not touch-related
|
||||
return
|
||||
|
||||
var distance := _calculate_distance()
|
||||
# if either current distance or initial distance is not set,
|
||||
# we are zero
|
||||
if not is_finite(distance) or not is_finite(_initial_distance):
|
||||
_value = Vector3.ZERO
|
||||
return
|
||||
|
||||
# we assume that _initial_distance is never 0 because
|
||||
# you cannot have two fingers physically at the same place
|
||||
# on a touch screen
|
||||
_value = Vector3(distance / _initial_distance, 0, 0)
|
||||
|
||||
|
||||
func _calculate_distance() -> float:
|
||||
var pos1:Vector2 = GUIDETouchState.get_finger_position(0, 2)
|
||||
# if we have no position for first finger, we can immediately abort
|
||||
if not pos1.is_finite():
|
||||
return INF
|
||||
|
||||
var pos2:Vector2 = GUIDETouchState.get_finger_position(1, 2)
|
||||
# if there is no second finger, we can abort as well
|
||||
if not pos2.is_finite():
|
||||
return INF
|
||||
|
||||
# calculate distance for the fingers
|
||||
return pos1.distance_to(pos2)
|
||||
|
||||
|
||||
func is_same_as(other:GUIDEInput):
|
||||
return other is GUIDEInputTouchDistance
|
||||
|
||||
|
||||
func _to_string():
|
||||
return "(GUIDEInputTouchDistance)"
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Touch Distance"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Distance of two touching fingers."
|
||||
|
||||
|
||||
func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
|
||||
return GUIDEAction.GUIDEActionValueType.AXIS_1D
|
47
addons/guide/inputs/guide_input_touch_position.gd
Normal file
@@ -0,0 +1,47 @@
|
||||
@tool
|
||||
class_name GUIDEInputTouchPosition
|
||||
extends GUIDEInputTouchBase
|
||||
|
||||
const GUIDETouchState = preload("guide_touch_state.gd")
|
||||
|
||||
|
||||
func _begin_usage():
|
||||
_value = Vector3.INF
|
||||
|
||||
|
||||
func _input(event:InputEvent) -> void:
|
||||
# update touch state
|
||||
if not GUIDETouchState.process_input_event(event):
|
||||
# not touch-related
|
||||
return
|
||||
|
||||
# update finger position
|
||||
var position := GUIDETouchState.get_finger_position(finger_index, finger_count)
|
||||
if not position.is_finite():
|
||||
_value = Vector3.INF
|
||||
return
|
||||
|
||||
_value = Vector3(position.x, position.y, 0)
|
||||
|
||||
|
||||
func is_same_as(other:GUIDEInput):
|
||||
return other is GUIDEInputTouchPosition and \
|
||||
other.finger_count == finger_count and \
|
||||
other.finger_index == finger_index
|
||||
|
||||
|
||||
func _to_string():
|
||||
return "(GUIDEInputTouchPosition finger_count=" + str(finger_count) + \
|
||||
" finger_index=" + str(finger_index) +")"
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Touch Position"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Position of a touching finger."
|
||||
|
||||
|
||||
func _native_value_type() -> GUIDEAction.GUIDEActionValueType:
|
||||
return GUIDEAction.GUIDEActionValueType.AXIS_2D
|
73
addons/guide/inputs/guide_touch_state.gd
Normal file
@@ -0,0 +1,73 @@
|
||||
@tool
|
||||
## Shared information about current touch state. This simplifies implementation of the touch inputs
|
||||
## and avoids having to process the same events multiple times.
|
||||
|
||||
# Cached finger positions
|
||||
static var _finger_positions:Dictionary = {}
|
||||
|
||||
# Events processed this frame.
|
||||
static var _processed_events:Dictionary = {}
|
||||
|
||||
# Last frame we were called
|
||||
static var _last_frame:int = -1
|
||||
|
||||
|
||||
## Processes an input event and updates touch state. Returns true, if the given event
|
||||
## was touch-related.
|
||||
static func process_input_event(event:InputEvent) -> bool:
|
||||
if not event is InputEventScreenTouch and not event is InputEventScreenDrag:
|
||||
return false
|
||||
|
||||
var this_frame = Engine.get_process_frames()
|
||||
|
||||
# if we are in a new frame, clear the processed events
|
||||
if this_frame != _last_frame:
|
||||
_last_frame = this_frame
|
||||
_processed_events.clear()
|
||||
|
||||
# if the event already was processed, skip processing it again
|
||||
if _processed_events.has(event):
|
||||
return true
|
||||
|
||||
_processed_events[event] = true
|
||||
|
||||
var index:int = event.index
|
||||
|
||||
if event is InputEventScreenTouch:
|
||||
if event.pressed:
|
||||
_finger_positions[index] = event.position
|
||||
else:
|
||||
_finger_positions.erase(index)
|
||||
|
||||
if event is InputEventScreenDrag:
|
||||
_finger_positions[index] = event.position
|
||||
|
||||
return true
|
||||
|
||||
|
||||
## Gets the finger position of the finger at the given index.
|
||||
## If finger_index is < 0, returns the average of all finger positions.
|
||||
## Will only return a position if the amount of fingers
|
||||
## currently touching matches finger_count.
|
||||
##
|
||||
## If no finger position can be determined, returns Vector2.INF.
|
||||
static func get_finger_position(finger_index:int, finger_count:int) -> Vector2:
|
||||
# if we have no finger positions right now, we can cut it short here
|
||||
if _finger_positions.is_empty():
|
||||
return Vector2.INF
|
||||
|
||||
# If the finger count doesn't match we have no position right now
|
||||
if _finger_positions.size() != finger_count:
|
||||
return Vector2.INF
|
||||
|
||||
# if a finger index is set, use this fingers position, if available
|
||||
if finger_index > -1:
|
||||
return _finger_positions.get(finger_index, Vector2.INF)
|
||||
|
||||
|
||||
var result = Vector2.ZERO
|
||||
for value in _finger_positions.values():
|
||||
result += value
|
||||
|
||||
result /= float(finger_count)
|
||||
return result
|
23
addons/guide/modifiers/guide_modifier.gd
Normal file
@@ -0,0 +1,23 @@
|
||||
@tool
|
||||
@icon("res://addons/guide/modifiers/guide_modifier.svg")
|
||||
class_name GUIDEModifier
|
||||
extends Resource
|
||||
|
||||
## Called when the modifier is started to be used by GUIDE. Can be used to perform
|
||||
## initializations.
|
||||
func _begin_usage() -> void :
|
||||
pass
|
||||
|
||||
## Called, when the modifier is no longer used by GUIDE. Can be used to perform
|
||||
## cleanup.
|
||||
func _end_usage() -> void:
|
||||
pass
|
||||
|
||||
func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
|
||||
return input
|
||||
|
||||
func _editor_name() -> String:
|
||||
return ""
|
||||
|
||||
func _editor_description() -> String:
|
||||
return ""
|
12
addons/guide/modifiers/guide_modifier.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1.07241,0,0,1.07396,-3.11767,-2.34767)">
|
||||
<path d="M17.827,2.164C26.061,2.164 32.747,8.85 32.747,17.084C32.747,25.319 26.061,32.004 17.827,32.004C9.592,32.004 2.907,25.319 2.907,17.084C2.907,8.85 9.592,2.164 17.827,2.164ZM17.827,4.857C11.08,4.857 5.604,10.337 5.604,17.084C5.604,23.831 11.08,29.311 17.827,29.311C24.574,29.311 30.05,23.831 30.05,17.084C30.05,10.337 24.574,4.857 17.827,4.857Z" style="fill:rgb(253,150,0);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-6.66265,-2.69876)">
|
||||
<g transform="matrix(24,0,0,24,11.6286,27.2968)">
|
||||
<path d="M0.44,-0.259C0.444,-0.251 0.448,-0.243 0.452,-0.234C0.455,-0.225 0.459,-0.216 0.462,-0.207C0.465,-0.216 0.469,-0.225 0.473,-0.234C0.476,-0.242 0.48,-0.251 0.485,-0.26L0.728,-0.7C0.732,-0.708 0.736,-0.712 0.741,-0.714C0.746,-0.716 0.752,-0.717 0.761,-0.717L0.833,-0.717L0.833,-0L0.748,-0L0.748,-0.527C0.748,-0.534 0.748,-0.541 0.748,-0.549C0.748,-0.557 0.749,-0.565 0.75,-0.574L0.504,-0.126C0.496,-0.111 0.484,-0.103 0.469,-0.103L0.455,-0.103C0.44,-0.103 0.428,-0.111 0.42,-0.126L0.169,-0.575C0.17,-0.566 0.171,-0.558 0.171,-0.55C0.172,-0.541 0.172,-0.534 0.172,-0.527L0.172,-0L0.087,-0L0.087,-0.717L0.159,-0.717C0.167,-0.717 0.174,-0.716 0.179,-0.714C0.183,-0.712 0.188,-0.708 0.192,-0.7L0.44,-0.259Z" style="fill:rgb(253,150,0);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
38
addons/guide/modifiers/guide_modifier.svg.import
Normal file
@@ -0,0 +1,38 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://j64d8n4am2uh"
|
||||
path="res://.godot/imported/guide_modifier.svg-8cf939ca3244410aba00f7b558561d72.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/guide/modifiers/guide_modifier.svg"
|
||||
dest_files=["res://.godot/imported/guide_modifier.svg-8cf939ca3244410aba00f7b558561d72.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=0.5
|
||||
editor/scale_with_editor_scale=true
|
||||
editor/convert_colors_with_editor_theme=false
|
53
addons/guide/modifiers/guide_modifier_3d_coordinates.gd
Normal file
@@ -0,0 +1,53 @@
|
||||
## Converts a position input in viewport coordinates (e.g. from the mouse position input)
|
||||
## into 3D coordinates (e.g. 3D world coordinates). Useful to get a 3D 'world' position.
|
||||
## Returns a Vector3.INF if no 3D world coordinates can be determined.
|
||||
@tool
|
||||
class_name GUIDEModifier3DCoordinates
|
||||
extends GUIDEModifier
|
||||
|
||||
## The maximum depth of the ray cast used to detect the 3D position.
|
||||
@export var max_depth:float = 1000.0
|
||||
|
||||
## Whether the rays cast should collide with areas.
|
||||
@export var collide_with_areas:bool = false
|
||||
|
||||
## Collision mask to use for the ray cast.
|
||||
@export_flags_3d_physics var collision_mask:int
|
||||
|
||||
|
||||
func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
|
||||
# if we collide with nothing, no need to even try
|
||||
if collision_mask == 0:
|
||||
return Vector3.INF
|
||||
|
||||
if not input.is_finite():
|
||||
return Vector3.INF
|
||||
|
||||
var viewport = Engine.get_main_loop().root
|
||||
var camera:Camera3D = viewport.get_camera_3d()
|
||||
if camera == null:
|
||||
return Vector3.INF
|
||||
|
||||
|
||||
var input_position:Vector2 = Vector2(input.x, input.y)
|
||||
|
||||
var from:Vector3 = camera.project_ray_origin(input_position)
|
||||
var to:Vector3 = from + camera.project_ray_normal(input_position) * max_depth
|
||||
var query:= PhysicsRayQueryParameters3D.create(from, to, collision_mask)
|
||||
query.collide_with_areas = collide_with_areas
|
||||
|
||||
var result = viewport.world_3d.direct_space_state.intersect_ray(query)
|
||||
if result.has("position"):
|
||||
return result.position
|
||||
|
||||
return Vector3.INF
|
||||
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "3D coordinates"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Converts a position input in viewport coordinates (e.g. from the mouse position input)\n" + \
|
||||
"into 3D coordinates (e.g. 3D world coordinates). Useful to get a 3D 'world' position."
|
47
addons/guide/modifiers/guide_modifier_8_way_direction.gd
Normal file
@@ -0,0 +1,47 @@
|
||||
@tool
|
||||
## A filter that converts a 2D input into a boolean that is true when the
|
||||
## input direction matches the selected direction. Note, that north is negative Y,
|
||||
## because in Godot negative Y is up.
|
||||
class_name GUIDEModifier8WayDirection
|
||||
extends GUIDEModifier
|
||||
|
||||
enum GUIDEDirection {
|
||||
EAST = 0,
|
||||
NORTH_EAST = 1,
|
||||
NORTH = 2,
|
||||
NORTH_WEST = 3,
|
||||
WEST = 4,
|
||||
SOUTH_WEST = 5,
|
||||
SOUTH = 6,
|
||||
SOUTH_EAST = 7
|
||||
}
|
||||
|
||||
## The direction in which the input should point.
|
||||
@export var direction:GUIDEDirection = GUIDEDirection.EAST
|
||||
|
||||
func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
|
||||
if not input.is_finite():
|
||||
return Vector3.INF
|
||||
|
||||
if input.is_zero_approx():
|
||||
return Vector3.ZERO
|
||||
|
||||
|
||||
|
||||
# get the angle in which the direction is pointing in radians.
|
||||
var angle_radians = atan2( -input.y, input.x );
|
||||
var octant = roundi( 8 * angle_radians / TAU + 8 ) % 8;
|
||||
if octant == direction:
|
||||
return Vector3.RIGHT # (1, 0, 0) indicating boolean true
|
||||
else:
|
||||
return Vector3.ZERO
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "8-way direction"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Converts a 2D input into a boolean that is true when the\n" + \
|
||||
"input direction matches the selected direction. Note, that north is negative Y,\n" + \
|
||||
"because in Godot negative Y is up."
|
35
addons/guide/modifiers/guide_modifier_canvas_coordinates.gd
Normal file
@@ -0,0 +1,35 @@
|
||||
## Converts a position input in viewport coordinates (e.g. from the mouse position input)
|
||||
## into canvas coordinates (e.g. 2D world coordinates). Useful to get a 2D 'world' position.
|
||||
@tool
|
||||
class_name GUIDEModifierCanvasCoordinates
|
||||
extends GUIDEModifier
|
||||
|
||||
## If checked, the input will be treated as relative input (position change)
|
||||
## rather than absolute input (position).
|
||||
@export var relative_input:bool:
|
||||
set(value):
|
||||
relative_input = value
|
||||
emit_changed()
|
||||
|
||||
func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
|
||||
if not input.is_finite():
|
||||
return Vector3.INF
|
||||
|
||||
var viewport = Engine.get_main_loop().root
|
||||
var transform = viewport.canvas_transform.affine_inverse()
|
||||
var coordinates = transform * Vector2(input.x, input.y)
|
||||
|
||||
if relative_input:
|
||||
var origin = transform * Vector2.ZERO
|
||||
coordinates -= origin
|
||||
|
||||
return Vector3(coordinates.x, coordinates.y, input.z)
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Canvas coordinates"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Converts a position input in viewport coordinates (e.g. from the mouse position input)\n" + \
|
||||
"into canvas coordinates (e.g. 2D world coordinates). Useful to get a 2D 'world' position."
|
51
addons/guide/modifiers/guide_modifier_curve.gd
Normal file
@@ -0,0 +1,51 @@
|
||||
@tool
|
||||
## Applies a separate curve to each input axis.
|
||||
class_name GUIDEModifierCurve
|
||||
extends GUIDEModifier
|
||||
|
||||
|
||||
## The curve to apply to the x axis
|
||||
@export var curve: Curve = default_curve()
|
||||
|
||||
## Apply modifier to X axis
|
||||
@export var x: bool = true
|
||||
|
||||
## Apply modifier to Y axis
|
||||
@export var y: bool = true
|
||||
|
||||
## Apply modifier to Z axis
|
||||
@export var z: bool = true
|
||||
|
||||
|
||||
## Create default curve resource with a smoothstep, 0.0 - 1.0 input/output range
|
||||
static func default_curve() -> Curve:
|
||||
var curve = Curve.new()
|
||||
curve.add_point(Vector2(0.0, 0.0))
|
||||
curve.add_point(Vector2(1.0, 1.0))
|
||||
|
||||
return curve
|
||||
|
||||
|
||||
func _modify_input(input: Vector3, delta: float, value_type: GUIDEAction.GUIDEActionValueType) -> Vector3:
|
||||
# Curve should never be null
|
||||
if curve == null:
|
||||
push_error("No curve added to Curve modifier.")
|
||||
return input
|
||||
|
||||
if not input.is_finite():
|
||||
return Vector3.INF
|
||||
|
||||
# Return vector with enabled axes modified, others remain unchanged.
|
||||
return Vector3(
|
||||
curve.sample(input.x) if x else input.x,
|
||||
curve.sample(input.y) if y else input.y,
|
||||
curve.sample(input.z) if z else input.z
|
||||
)
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Curve"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Applies a curve to each input axis."
|
63
addons/guide/modifiers/guide_modifier_deadzone.gd
Normal file
@@ -0,0 +1,63 @@
|
||||
@tool
|
||||
## Inputs between the lower and upper threshold are mapped 0 -> 1.
|
||||
## Values outside the thresholds are clamped.
|
||||
class_name GUIDEModifierDeadzone
|
||||
extends GUIDEModifier
|
||||
|
||||
## Lower threshold for the deadzone.
|
||||
@export_range(0,1) var lower_threshold:float = 0.2:
|
||||
set(value):
|
||||
if value > upper_threshold:
|
||||
lower_threshold = upper_threshold
|
||||
else:
|
||||
lower_threshold = value
|
||||
emit_changed()
|
||||
|
||||
|
||||
## Upper threshold for the deadzone.
|
||||
@export_range(0,1) var upper_threshold:float = 1.0:
|
||||
set(value):
|
||||
if value < lower_threshold:
|
||||
upper_threshold = lower_threshold
|
||||
else:
|
||||
upper_threshold = value
|
||||
emit_changed()
|
||||
|
||||
|
||||
func _rescale(value:float) -> float:
|
||||
return min(1.0, (max(0.0, abs(value) - lower_threshold) / (upper_threshold - lower_threshold))) * sign(value)
|
||||
|
||||
|
||||
func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
|
||||
if upper_threshold <= lower_threshold:
|
||||
return input
|
||||
|
||||
if not input.is_finite():
|
||||
return Vector3.INF
|
||||
|
||||
match value_type:
|
||||
GUIDEAction.GUIDEActionValueType.BOOL, GUIDEAction.GUIDEActionValueType.AXIS_1D:
|
||||
return Vector3(_rescale(input.x), input.y, input.z)
|
||||
|
||||
GUIDEAction.GUIDEActionValueType.AXIS_2D:
|
||||
var v2d = Vector2(input.x, input.y)
|
||||
if v2d.is_zero_approx():
|
||||
return Vector3(0, 0, input.z)
|
||||
v2d = v2d.normalized() * _rescale(v2d.length())
|
||||
return Vector3(v2d.x, v2d.y, input.z)
|
||||
|
||||
GUIDEAction.GUIDEActionValueType.AXIS_3D:
|
||||
if input.is_zero_approx():
|
||||
return Vector3.ZERO
|
||||
return input.normalized() * _rescale(input.length())
|
||||
_:
|
||||
push_error("Unsupported value type. This is a bug. Please report it.")
|
||||
return input
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Deadzone"
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Inputs between the lower and upper threshold are mapped 0 -> 1.\n" + \
|
||||
"Values outside the thresholds are clamped."
|
43
addons/guide/modifiers/guide_modifier_input_swizzle.gd
Normal file
@@ -0,0 +1,43 @@
|
||||
## Swizzle the input vector components. Useful to map 1D input to 2D or vice versa.
|
||||
@tool
|
||||
class_name GUIDEModifierInputSwizzle
|
||||
extends GUIDEModifier
|
||||
|
||||
enum GUIDEInputSwizzleOperation {
|
||||
## Swap X and Y axes.
|
||||
YXZ,
|
||||
## Swap X and Z axes.
|
||||
ZYX,
|
||||
## Swap Y and Z axes.
|
||||
XZY,
|
||||
## Y to X, Z to Y, X to Z.
|
||||
YZX,
|
||||
## Y to Z, Z to X, X to Y.
|
||||
ZXY
|
||||
}
|
||||
|
||||
## The new order into which the input should be brought.
|
||||
@export var order:GUIDEInputSwizzleOperation = GUIDEInputSwizzleOperation.YXZ
|
||||
|
||||
|
||||
func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
|
||||
match order:
|
||||
GUIDEInputSwizzleOperation.YXZ:
|
||||
return Vector3(input.y, input.x, input.z)
|
||||
GUIDEInputSwizzleOperation.ZYX:
|
||||
return Vector3(input.z, input.y, input.x)
|
||||
GUIDEInputSwizzleOperation.XZY:
|
||||
return Vector3(input.x, input.z, input.y)
|
||||
GUIDEInputSwizzleOperation.YZX:
|
||||
return Vector3(input.y, input.z, input.x)
|
||||
GUIDEInputSwizzleOperation.ZXY:
|
||||
return Vector3(input.z, input.x, input.y)
|
||||
_:
|
||||
push_error("Unknown order ", order , " this is most likely a bug, please report it.")
|
||||
return input
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Input Swizzle"
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Swizzle the input vector components. Useful to map 1D input to 2D or vice versa."
|
67
addons/guide/modifiers/guide_modifier_map_range.gd
Normal file
@@ -0,0 +1,67 @@
|
||||
@tool
|
||||
## Maps an input range to an output range and optionally clamps the output.
|
||||
class_name GUIDEModifierMapRange
|
||||
extends GUIDEModifier
|
||||
|
||||
## Should the output be clamped to the range?
|
||||
@export var apply_clamp:bool = true
|
||||
|
||||
## The minimum input value
|
||||
@export var input_min:float = 0.0
|
||||
|
||||
## The maximum input value
|
||||
@export var input_max:float = 1.0
|
||||
|
||||
## The minimum output value
|
||||
@export var output_min:float = 0.0
|
||||
|
||||
## The maximum output value
|
||||
@export var output_max:float = 1.0
|
||||
|
||||
## Apply modifier to X axis
|
||||
@export var x:bool = true
|
||||
|
||||
## Apply modifier to Y axis
|
||||
@export var y:bool = true
|
||||
|
||||
## Apply modifier to Z axis
|
||||
@export var z:bool = true
|
||||
|
||||
var _omin:float
|
||||
var _omax:float
|
||||
|
||||
func _begin_usage():
|
||||
# we calculate the min and max of the output range here, so we can use them later and don't have to
|
||||
# recalculate them every time the modifier is used
|
||||
_omin = min(output_min, output_max)
|
||||
_omax = max(output_min, output_max)
|
||||
|
||||
func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
|
||||
if not input.is_finite():
|
||||
return Vector3.INF
|
||||
|
||||
var x_value:float = remap(input.x, input_min, input_max, output_min, output_max)
|
||||
var y_value:float = remap(input.y, input_min, input_max, output_min, output_max)
|
||||
var z_value:float = remap(input.z, input_min, input_max, output_min, output_max)
|
||||
|
||||
if apply_clamp:
|
||||
# clamp doesn't handle reverse ranges, so we need to use our calculated normalized output range
|
||||
# to clamp the output values
|
||||
x_value = clamp(x_value, _omin, _omax)
|
||||
y_value = clamp(y_value, _omin, _omax)
|
||||
z_value = clamp(z_value, _omin, _omax)
|
||||
|
||||
# Return vector with enabled axes set, others unchanged
|
||||
return Vector3(
|
||||
x_value if x else input.x,
|
||||
y_value if y else input.y,
|
||||
z_value if z else input.z,
|
||||
)
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Map Range"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Maps an input range to an output range and optionally clamps the output"
|
52
addons/guide/modifiers/guide_modifier_negate.gd
Normal file
@@ -0,0 +1,52 @@
|
||||
## Inverts input per axis.
|
||||
@tool
|
||||
class_name GUIDEModifierNegate
|
||||
extends GUIDEModifier
|
||||
|
||||
## Whether the X axis should be inverted.
|
||||
@export var x:bool = true:
|
||||
set(value):
|
||||
if x == value:
|
||||
return
|
||||
x = value
|
||||
_update_caches()
|
||||
emit_changed()
|
||||
|
||||
## Whether the Y axis should be inverted.
|
||||
@export var y:bool = true:
|
||||
set(value):
|
||||
if y == value:
|
||||
return
|
||||
y = value
|
||||
_update_caches()
|
||||
emit_changed()
|
||||
|
||||
## Whether the Z axis should be inverted.
|
||||
@export var z:bool = true:
|
||||
set(value):
|
||||
if z == value:
|
||||
return
|
||||
z = value
|
||||
_update_caches()
|
||||
emit_changed()
|
||||
|
||||
var _multiplier:Vector3 = Vector3.ONE * -1
|
||||
|
||||
func _update_caches():
|
||||
_multiplier.x = -1 if x else 1
|
||||
_multiplier.y = -1 if y else 1
|
||||
_multiplier.z = -1 if z else 1
|
||||
|
||||
|
||||
func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
|
||||
if not input.is_finite():
|
||||
return Vector3.INF
|
||||
|
||||
return input * _multiplier
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Negate"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Inverts input per axis."
|
17
addons/guide/modifiers/guide_modifier_normalize.gd
Normal file
@@ -0,0 +1,17 @@
|
||||
## Normalizes the input vector.
|
||||
@tool
|
||||
class_name GUIDEModifierNormalize
|
||||
extends GUIDEModifier
|
||||
|
||||
func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
|
||||
if not input.is_finite():
|
||||
return Vector3.INF
|
||||
|
||||
return input.normalized()
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Normalize"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Normalizes the input vector."
|
65
addons/guide/modifiers/guide_modifier_positive_negative.gd
Normal file
@@ -0,0 +1,65 @@
|
||||
## Limits inputs to positive or negative values.
|
||||
@tool
|
||||
class_name GUIDEModifierPositiveNegative
|
||||
extends GUIDEModifier
|
||||
|
||||
enum LimitRange {
|
||||
POSITIVE = 1,
|
||||
NEGATIVE = 2
|
||||
}
|
||||
|
||||
## The range of numbers to which the input should be limited
|
||||
@export var range:LimitRange = LimitRange.POSITIVE
|
||||
|
||||
## Whether the X axis should be affected.
|
||||
@export var x:bool = true:
|
||||
set(value):
|
||||
if x == value:
|
||||
return
|
||||
x = value
|
||||
emit_changed()
|
||||
|
||||
## Whether the Y axis should be affected.
|
||||
@export var y:bool = true:
|
||||
set(value):
|
||||
if y == value:
|
||||
return
|
||||
y = value
|
||||
emit_changed()
|
||||
|
||||
## Whether the Z axis should be affected.
|
||||
@export var z:bool = true:
|
||||
set(value):
|
||||
if z == value:
|
||||
return
|
||||
z = value
|
||||
emit_changed()
|
||||
|
||||
|
||||
|
||||
func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
|
||||
if not input.is_finite():
|
||||
return Vector3.INF
|
||||
|
||||
match range:
|
||||
LimitRange.POSITIVE:
|
||||
return Vector3(
|
||||
max(0, input.x) if x else input.x, \
|
||||
max(0, input.y) if y else input.y, \
|
||||
max(0, input.z) if z else input.z \
|
||||
)
|
||||
LimitRange.NEGATIVE:
|
||||
return Vector3(
|
||||
min(0, input.x) if x else input.x, \
|
||||
min(0, input.y) if y else input.y, \
|
||||
min(0, input.z) if z else input.z \
|
||||
)
|
||||
# should never happen
|
||||
return input
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Positive/Negative"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Clamps the input to positive or negative values."
|
35
addons/guide/modifiers/guide_modifier_scale.gd
Normal file
@@ -0,0 +1,35 @@
|
||||
@tool
|
||||
## Scales the input by the given value and optionally, delta time.
|
||||
class_name GUIDEModifierScale
|
||||
extends GUIDEModifier
|
||||
|
||||
## The scale by which the input should be scaled.
|
||||
@export var scale:Vector3 = Vector3.ONE:
|
||||
set(value):
|
||||
scale = value
|
||||
emit_changed()
|
||||
|
||||
|
||||
## If true, delta time will be multiplied in addition to the scale.
|
||||
@export var apply_delta_time:bool = false:
|
||||
set(value):
|
||||
apply_delta_time = value
|
||||
emit_changed()
|
||||
|
||||
|
||||
func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
|
||||
if not input.is_finite():
|
||||
return Vector3.INF
|
||||
|
||||
if apply_delta_time:
|
||||
return input * scale * delta
|
||||
else:
|
||||
return input * scale
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Scale"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Scales the input by the given value and optionally, delta time."
|
105
addons/guide/modifiers/guide_modifier_virtual_cursor.gd
Normal file
@@ -0,0 +1,105 @@
|
||||
## Stateful modifier which provides a virtual "mouse" cursor driven by input. The modifier
|
||||
## returns the current cursor position in pixels releative to the origin of the currently
|
||||
## active window.
|
||||
@tool
|
||||
class_name GUIDEModifierVirtualCursor
|
||||
extends GUIDEModifier
|
||||
|
||||
enum ScreenScale {
|
||||
## Input is not scaled with input screen size. This means that the cursor will
|
||||
## visually move slower on higher resolutions.
|
||||
NONE = 0,
|
||||
## Input is scaled with the longer axis of the screen size (e.g. width in
|
||||
## landscape mode, height in portrait mode). The cursor will move with
|
||||
## the same visual speed on all resolutions.
|
||||
LONGER_AXIS = 1,
|
||||
## Input is scaled with the shorter axis of the screen size (e.g. height in
|
||||
## landscape mode, width in portrait mode). The cursor will move with the
|
||||
## same visual speed on all resolutions.
|
||||
SHORTER_AXIS = 2
|
||||
}
|
||||
|
||||
## The initial position of the virtual cursor (given in screen relative coordinates)
|
||||
@export var initial_position:Vector2 = Vector2(0.5, 0.5):
|
||||
set(value):
|
||||
initial_position = value.clamp(Vector2.ZERO, Vector2.ONE)
|
||||
|
||||
## The cursor movement speed in pixels.
|
||||
@export var speed:Vector3 = Vector3.ONE
|
||||
|
||||
## Screen scaling to be applied to the cursor movement. This controls
|
||||
## whether the cursor movement speed is resolution dependent or not.
|
||||
## If set to anything but [code]None[/code] then the input value will
|
||||
## be multiplied with the window width/height depending on the setting.
|
||||
@export var screen_scale:ScreenScale = ScreenScale.LONGER_AXIS
|
||||
|
||||
## The scale by which the input should be scaled.
|
||||
## @deprecated: use [member speed] instead.
|
||||
var scale:Vector3:
|
||||
get: return speed
|
||||
set(value): speed = value
|
||||
|
||||
## If true, the cursor movement speed is in pixels per second, otherwise it is in pixels
|
||||
## per frame.
|
||||
@export var apply_delta_time:bool = true
|
||||
|
||||
|
||||
## Cursor offset in pixels.
|
||||
var _offset:Vector3 = Vector3.ZERO
|
||||
|
||||
## Returns the scaled screen size. This takes Godot's scaling factor for windows into account.
|
||||
func _get_scaled_screen_size():
|
||||
# Get window size, including scaling factor
|
||||
var window = Engine.get_main_loop().get_root()
|
||||
return window.get_screen_transform().affine_inverse() * Vector2(window.size)
|
||||
|
||||
func _begin_usage():
|
||||
var window_size = _get_scaled_screen_size()
|
||||
_offset = Vector3(window_size.x * initial_position.x, window_size.y * initial_position.y, 0)
|
||||
|
||||
|
||||
func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
|
||||
if not input.is_finite():
|
||||
# input is invalid, so just return current cursor position
|
||||
return _offset
|
||||
|
||||
var window_size = _get_scaled_screen_size()
|
||||
input *= speed
|
||||
|
||||
if apply_delta_time:
|
||||
input *= delta
|
||||
|
||||
var screen_scale_factor:float = 1.0
|
||||
match screen_scale:
|
||||
ScreenScale.LONGER_AXIS:
|
||||
screen_scale_factor = max(window_size.x, window_size.y)
|
||||
ScreenScale.SHORTER_AXIS:
|
||||
screen_scale_factor = min(window_size.x, window_size.y)
|
||||
|
||||
input *= screen_scale_factor
|
||||
|
||||
# apply input and clamp to window size
|
||||
_offset = (_offset + input).clamp(Vector3.ZERO, Vector3(window_size.x, window_size.y, 0))
|
||||
|
||||
return _offset
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Virtual Cursor"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Stateful modifier which provides a virtual \"mouse\" cursor driven by input. The modifier\n" + \
|
||||
"returns the current cursor position in pixels releative to the origin of the currently \n" + \
|
||||
"active window."
|
||||
|
||||
|
||||
# support for legacy properties
|
||||
func _get_property_list():
|
||||
return [
|
||||
{
|
||||
"name": "scale",
|
||||
"type": TYPE_VECTOR3,
|
||||
"usage": PROPERTY_USAGE_NO_EDITOR
|
||||
}
|
||||
]
|
||||
|
26
addons/guide/modifiers/guide_modifier_window_relative.gd
Normal file
@@ -0,0 +1,26 @@
|
||||
## Converts the value of the input into window-relative units between 0 and 1.
|
||||
## E.g. if a mouse cursor moves half a screen to the right and down, then
|
||||
## this modifier will return (0.5, 0.5).
|
||||
@tool
|
||||
class_name GUIDEModifierWindowRelative
|
||||
extends GUIDEModifier
|
||||
|
||||
|
||||
func _modify_input(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> Vector3:
|
||||
if not input.is_finite():
|
||||
return Vector3.INF
|
||||
|
||||
var window = Engine.get_main_loop().get_root()
|
||||
# We want real pixels, so we need to factor in any scaling that the window does.
|
||||
var window_size:Vector2 = window.get_screen_transform().affine_inverse() * Vector2(window.size)
|
||||
return Vector3(input.x / window_size.x, input.y / window_size.y, input.z)
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Window relative"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Converts the value of the input into window-relative units between 0 and 1.\n" + \
|
||||
"E.g. if a mouse cursor moves half a screen to the right and down, then \n" + \
|
||||
"this modifier will return (0.5, 0.5)."
|
7
addons/guide/plugin.cfg
Normal file
@@ -0,0 +1,7 @@
|
||||
[plugin]
|
||||
|
||||
name="Godot Unified Input Detection Engine (G.U.I.D.E)"
|
||||
description=""
|
||||
author="Jan Thomä"
|
||||
version="0.5.2"
|
||||
script="plugin.gd"
|
45
addons/guide/plugin.gd
Normal file
@@ -0,0 +1,45 @@
|
||||
@tool
|
||||
extends EditorPlugin
|
||||
const MainPanel = preload("editor/mapping_context_editor/mapping_context_editor.tscn")
|
||||
|
||||
var _main_panel:Control
|
||||
|
||||
|
||||
func _enable_plugin():
|
||||
add_autoload_singleton("GUIDE", "res://addons/guide/guide.gd")
|
||||
|
||||
func _enter_tree() -> void:
|
||||
_main_panel = MainPanel.instantiate()
|
||||
_main_panel.initialize(self)
|
||||
EditorInterface.get_editor_main_screen().add_child(_main_panel)
|
||||
# Hide the main panel. Very much required.
|
||||
_make_visible(false)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if is_instance_valid(_main_panel):
|
||||
_main_panel.queue_free()
|
||||
GUIDEInputFormatter.cleanup()
|
||||
|
||||
func _disable_plugin():
|
||||
remove_autoload_singleton("GUIDE")
|
||||
|
||||
|
||||
func _edit(object):
|
||||
if object is GUIDEMappingContext:
|
||||
_main_panel.edit(object)
|
||||
|
||||
func _get_plugin_name() -> String:
|
||||
return "G.U.I.D.E"
|
||||
|
||||
func _get_plugin_icon() -> Texture2D:
|
||||
return preload("res://addons/guide/editor/logo_editor_small.svg")
|
||||
|
||||
func _has_main_screen() -> bool:
|
||||
return true
|
||||
|
||||
func _handles(object:Variant) -> bool:
|
||||
return object is GUIDEMappingContext
|
||||
|
||||
func _make_visible(visible):
|
||||
if is_instance_valid(_main_panel):
|
||||
_main_panel.visible = visible
|
281
addons/guide/remapping/guide_input_detector.gd
Normal file
@@ -0,0 +1,281 @@
|
||||
@tool
|
||||
## Helper node for detecting inputs. Detects the next input matching a specification and
|
||||
## emits a signal with the detected input.
|
||||
class_name GUIDEInputDetector
|
||||
extends Node
|
||||
|
||||
## The device type for which the input should be filtered.
|
||||
enum DeviceType {
|
||||
## Only detect input from keyboard.
|
||||
KEYBOARD = 1,
|
||||
## Only detect input from the mouse.
|
||||
MOUSE = 2,
|
||||
## Only detect input from joysticks/gamepads.
|
||||
JOY = 4
|
||||
# touch doesn't make a lot of sense as this is usually
|
||||
# not remappable.
|
||||
}
|
||||
|
||||
## Which joy index should be used for detected joy events
|
||||
enum JoyIndex {
|
||||
# Use -1, so the detected input will match any joystick
|
||||
ANY = 0,
|
||||
# Use the actual index of the detected joystick.
|
||||
DETECTED = 1
|
||||
}
|
||||
|
||||
## A countdown between initiating a dection and the actual start of the
|
||||
## detection. This is useful because when the user clicks a button to
|
||||
## start a detection, we want to make sure that the player is actually
|
||||
## ready (and not accidentally moves anything). If set to 0, no countdown
|
||||
## will be started.
|
||||
@export_range(0, 2, 0.1, "or_greater") var detection_countdown_seconds:float = 0.5
|
||||
|
||||
## Minimum amplitude to detect any axis.
|
||||
@export_range(0, 1, 0.1, "or_greater") var minimum_axis_amplitude:float = 0.2
|
||||
|
||||
## If any of these inputs is encountered, the detector will
|
||||
## treat this as "abort detection".
|
||||
@export var abort_detection_on:Array[GUIDEInput] = []
|
||||
|
||||
## Which joy index should be returned for detected joy events.
|
||||
@export var use_joy_index:JoyIndex = JoyIndex.ANY
|
||||
|
||||
## Whether trigger buttons on controllers should be detected when
|
||||
## then action value type is limited to boolean.
|
||||
@export var allow_triggers_for_boolean_actions:bool = true
|
||||
|
||||
## Emitted when the detection has started (e.g. countdown has elapsed).
|
||||
## Can be used to signal this to the player.
|
||||
signal detection_started()
|
||||
|
||||
## Emitted when the input detector detects an input of the given type.
|
||||
## If detection was aborted the given input is null.
|
||||
signal input_detected(input:GUIDEInput)
|
||||
|
||||
# The timer for the detection countdown.
|
||||
var _timer:Timer
|
||||
|
||||
|
||||
|
||||
func _ready():
|
||||
_timer = Timer.new()
|
||||
_timer.one_shot = true
|
||||
add_child(_timer, false, Node.INTERNAL_MODE_FRONT)
|
||||
_timer.timeout.connect(_begin_detection)
|
||||
|
||||
var _is_detecting:bool
|
||||
|
||||
## Whether the input detector is currently detecting input.
|
||||
var is_detecting:bool:
|
||||
get: return _is_detecting
|
||||
|
||||
var _value_type:GUIDEAction.GUIDEActionValueType
|
||||
var _device_types:Array[DeviceType] = []
|
||||
|
||||
## Detects a boolean input type.
|
||||
func detect_bool(device_types:Array[DeviceType] = []) -> void:
|
||||
detect(GUIDEAction.GUIDEActionValueType.BOOL, device_types)
|
||||
|
||||
|
||||
## Detects a 1D axis input type.
|
||||
func detect_axis_1d(device_types:Array[DeviceType] = []) -> void:
|
||||
detect(GUIDEAction.GUIDEActionValueType.AXIS_1D, device_types)
|
||||
|
||||
|
||||
## Detects a 2D axis input type.
|
||||
func detect_axis_2d(device_types:Array[DeviceType] = []) -> void:
|
||||
detect(GUIDEAction.GUIDEActionValueType.AXIS_2D, device_types)
|
||||
|
||||
|
||||
## Detects a 3D axis input type.
|
||||
func detect_axis_3d(device_types:Array[DeviceType] = []) -> void:
|
||||
detect(GUIDEAction.GUIDEActionValueType.AXIS_3D, device_types)
|
||||
|
||||
|
||||
## Aborts a running detection. If no detection currently runs
|
||||
## does nothing.
|
||||
func abort_detection() -> void:
|
||||
_timer.stop()
|
||||
if _is_detecting:
|
||||
_is_detecting = false
|
||||
input_detected.emit(null)
|
||||
|
||||
## Detects the given input type. If device types are given
|
||||
## will only detect inputs from the given device types.
|
||||
## Otherwise will detect inputs from all supported device types.
|
||||
func detect(value_type:GUIDEAction.GUIDEActionValueType,
|
||||
device_types:Array[DeviceType] = []) -> void:
|
||||
if device_types == null:
|
||||
push_error("Device types must not be null. Supply an empty array if you want to detect input from all devices.")
|
||||
return
|
||||
|
||||
# reset all abort inputs
|
||||
for input in abort_detection_on:
|
||||
input._reset()
|
||||
|
||||
abort_detection()
|
||||
_value_type = value_type
|
||||
_device_types = device_types
|
||||
_timer.start(detection_countdown_seconds)
|
||||
|
||||
|
||||
func _begin_detection():
|
||||
_is_detecting = true
|
||||
detection_started.emit()
|
||||
|
||||
|
||||
func _input(event:InputEvent) -> void:
|
||||
if not _is_detecting:
|
||||
return
|
||||
|
||||
# feed the event into the abort inputs
|
||||
for input in abort_detection_on:
|
||||
input._input(event)
|
||||
# if it triggers, we abort
|
||||
if input._value.is_finite() and input._value.length() > 0:
|
||||
# eat the input so it doesn't accidentally trigger something else
|
||||
get_viewport().set_input_as_handled()
|
||||
abort_detection()
|
||||
return
|
||||
|
||||
# check if the event matches the device type we are
|
||||
# looking for
|
||||
if not _matches_device_types(event):
|
||||
return
|
||||
|
||||
# then check if it can be mapped to the desired
|
||||
# value type
|
||||
match _value_type:
|
||||
GUIDEAction.GUIDEActionValueType.BOOL:
|
||||
_try_detect_bool(event)
|
||||
GUIDEAction.GUIDEActionValueType.AXIS_1D:
|
||||
_try_detect_axis_1d(event)
|
||||
GUIDEAction.GUIDEActionValueType.AXIS_2D:
|
||||
_try_detect_axis_2d(event)
|
||||
GUIDEAction.GUIDEActionValueType.AXIS_3D:
|
||||
_try_detect_axis_3d(event)
|
||||
|
||||
|
||||
func _matches_device_types(event:InputEvent) -> bool:
|
||||
if _device_types.is_empty():
|
||||
return true
|
||||
|
||||
if event is InputEventKey:
|
||||
return _device_types.has(DeviceType.KEYBOARD)
|
||||
|
||||
if event is InputEventMouse:
|
||||
return _device_types.has(DeviceType.MOUSE)
|
||||
|
||||
if event is InputEventJoypadButton or event is InputEventJoypadMotion:
|
||||
return _device_types.has(DeviceType.JOY)
|
||||
|
||||
return false
|
||||
|
||||
|
||||
func _try_detect_bool(event:InputEvent) -> void:
|
||||
if event is InputEventKey and event.is_released():
|
||||
var result := GUIDEInputKey.new()
|
||||
result.key = event.physical_keycode
|
||||
result.shift = event.shift_pressed
|
||||
result.control = event.ctrl_pressed
|
||||
result.meta = event.meta_pressed
|
||||
result.alt = event.alt_pressed
|
||||
_deliver(result)
|
||||
return
|
||||
|
||||
if event is InputEventMouseButton and event.is_released():
|
||||
var result := GUIDEInputMouseButton.new()
|
||||
result.button = event.button_index
|
||||
_deliver(result)
|
||||
return
|
||||
|
||||
if event is InputEventJoypadButton and event.is_released():
|
||||
var result := GUIDEInputJoyButton.new()
|
||||
result.button = event.button_index
|
||||
result.joy_index = _find_joy_index(event.device)
|
||||
_deliver(result)
|
||||
|
||||
if allow_triggers_for_boolean_actions:
|
||||
# only allow joypad trigger buttons
|
||||
if not (event is InputEventJoypadMotion):
|
||||
return
|
||||
if event.axis != JOY_AXIS_TRIGGER_LEFT and \
|
||||
event.axis != JOY_AXIS_TRIGGER_RIGHT:
|
||||
return
|
||||
|
||||
var result := GUIDEInputJoyAxis1D.new()
|
||||
result.axis = event.axis
|
||||
result.joy_index = _find_joy_index(event.device)
|
||||
_deliver(result)
|
||||
|
||||
|
||||
|
||||
func _try_detect_axis_1d(event:InputEvent) -> void:
|
||||
if event is InputEventMouseMotion:
|
||||
var result := GUIDEInputMouseAxis1D.new()
|
||||
# Pick the direction in which the mouse was moved more.
|
||||
if abs(event.relative.x) > abs(event.relative.y):
|
||||
result.axis = GUIDEInputMouseAxis1D.GUIDEInputMouseAxis.X
|
||||
else:
|
||||
result.axis = GUIDEInputMouseAxis1D.GUIDEInputMouseAxis.Y
|
||||
_deliver(result)
|
||||
return
|
||||
|
||||
if event is InputEventJoypadMotion:
|
||||
if abs(event.axis_value) < minimum_axis_amplitude:
|
||||
return
|
||||
|
||||
var result := GUIDEInputJoyAxis1D.new()
|
||||
result.axis = event.axis
|
||||
result.joy_index = _find_joy_index(event.device)
|
||||
_deliver(result)
|
||||
|
||||
|
||||
func _try_detect_axis_2d(event:InputEvent) -> void:
|
||||
if event is InputEventMouseMotion:
|
||||
var result := GUIDEInputMouseAxis2D.new()
|
||||
_deliver(result)
|
||||
return
|
||||
|
||||
if event is InputEventJoypadMotion:
|
||||
if event.axis_value < minimum_axis_amplitude:
|
||||
return
|
||||
|
||||
var result := GUIDEInputJoyAxis2D.new()
|
||||
match event.axis:
|
||||
JOY_AXIS_LEFT_X, JOY_AXIS_LEFT_Y:
|
||||
result.x = JOY_AXIS_LEFT_X
|
||||
result.y = JOY_AXIS_LEFT_Y
|
||||
JOY_AXIS_RIGHT_X, JOY_AXIS_RIGHT_Y:
|
||||
result.x = JOY_AXIS_RIGHT_X
|
||||
result.y = JOY_AXIS_RIGHT_Y
|
||||
_:
|
||||
# not supported for detection
|
||||
return
|
||||
result.joy_index = _find_joy_index(event.device)
|
||||
_deliver(result)
|
||||
return
|
||||
|
||||
|
||||
func _try_detect_axis_3d(event:InputEvent) -> void:
|
||||
# currently no input for 3D
|
||||
pass
|
||||
|
||||
|
||||
func _find_joy_index(device_id:int) -> int:
|
||||
if use_joy_index == JoyIndex.ANY:
|
||||
return -1
|
||||
|
||||
var pads := Input.get_connected_joypads()
|
||||
for i in pads.size():
|
||||
if pads[i] == device_id:
|
||||
return i
|
||||
|
||||
return -1
|
||||
|
||||
func _deliver(input:GUIDEInput) -> void:
|
||||
_is_detecting = false
|
||||
# eat the input so it doesn't accidentally trigger something else
|
||||
get_viewport().set_input_as_handled()
|
||||
input_detected.emit(input)
|
307
addons/guide/remapping/guide_remapper.gd
Normal file
@@ -0,0 +1,307 @@
|
||||
class_name GUIDERemapper
|
||||
|
||||
## Emitted when the bound input of an item changes.
|
||||
signal item_changed(item:ConfigItem, input:GUIDEInput)
|
||||
|
||||
var _remapping_config:GUIDERemappingConfig = GUIDERemappingConfig.new()
|
||||
var _mapping_contexts:Array[GUIDEMappingContext] = []
|
||||
|
||||
const GUIDESet = preload("../guide_set.gd")
|
||||
|
||||
## Loads the default bindings as they are currently configured in the mapping contexts and a mapping
|
||||
## config for editing. Note that the given mapping config will not be modified, so editing can be
|
||||
## cancelled. Call get_mapping_config to get the modified mapping config.
|
||||
func initialize(mapping_contexts:Array[GUIDEMappingContext], remapping_config:GUIDERemappingConfig):
|
||||
_remapping_config = remapping_config.duplicate() if remapping_config != null else GUIDERemappingConfig.new()
|
||||
|
||||
_mapping_contexts.clear()
|
||||
|
||||
for mapping_context in mapping_contexts:
|
||||
if not is_instance_valid(mapping_context):
|
||||
push_error("Cannot add null mapping context. Ignoring.")
|
||||
return
|
||||
_mapping_contexts.append(mapping_context)
|
||||
|
||||
|
||||
## Returns the mapping config with all modifications applied.
|
||||
func get_mapping_config() -> GUIDERemappingConfig:
|
||||
return _remapping_config.duplicate()
|
||||
|
||||
|
||||
func set_custom_data(key:Variant, value:Variant):
|
||||
_remapping_config.custom_data[key] = value
|
||||
|
||||
|
||||
func get_custom_data(key:Variant, default:Variant = null) -> Variant:
|
||||
return _remapping_config.custom_data.get(key, default)
|
||||
|
||||
|
||||
func remove_custom_data(key:Variant) -> void:
|
||||
_remapping_config.custom_data.erase(key)
|
||||
|
||||
|
||||
## Returns all remappable items. Can be filtered by context, display category or
|
||||
## action.
|
||||
func get_remappable_items(context:GUIDEMappingContext = null,
|
||||
display_category:String = "",
|
||||
action:GUIDEAction = null) -> Array[ConfigItem]:
|
||||
|
||||
if action != null and not action.is_remappable:
|
||||
push_warning("Action filter was set but filtered action is not remappable.")
|
||||
return []
|
||||
|
||||
|
||||
var result:Array[ConfigItem] = []
|
||||
for a_context:GUIDEMappingContext in _mapping_contexts:
|
||||
if context != null and context != a_context:
|
||||
continue
|
||||
for action_mapping:GUIDEActionMapping in a_context.mappings:
|
||||
var mapped_action:GUIDEAction = action_mapping.action
|
||||
# filter non-remappable actions
|
||||
if not mapped_action.is_remappable:
|
||||
continue
|
||||
|
||||
# if action filter is set, only pick mappings for this action
|
||||
if action != null and action != mapped_action:
|
||||
continue
|
||||
|
||||
# make config items
|
||||
for index:int in action_mapping.input_mappings.size():
|
||||
var input_mapping:GUIDEInputMapping = action_mapping.input_mappings[index]
|
||||
if input_mapping.override_action_settings and not input_mapping.is_remappable:
|
||||
# skip non-remappable items
|
||||
continue
|
||||
|
||||
# Calculate effective display category
|
||||
var effective_display_category:String = \
|
||||
_get_effective_display_category(mapped_action, input_mapping)
|
||||
|
||||
# if display category filter is set, only pick mappings
|
||||
# in this category
|
||||
if display_category.length() > 0 and effective_display_category != display_category:
|
||||
continue
|
||||
|
||||
var item = ConfigItem.new(a_context, action_mapping.action, index, input_mapping)
|
||||
item_changed.connect(item._item_changed)
|
||||
result.append(item)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
static func _get_effective_display_category(action:GUIDEAction, input_mapping:GUIDEInputMapping) -> String:
|
||||
var result:String = ""
|
||||
if input_mapping.override_action_settings:
|
||||
result = input_mapping.display_category
|
||||
|
||||
if result.is_empty():
|
||||
result = action.display_category
|
||||
|
||||
return result
|
||||
|
||||
|
||||
static func _get_effective_display_name(action:GUIDEAction, input_mapping:GUIDEInputMapping) -> String:
|
||||
var result:String = ""
|
||||
if input_mapping.override_action_settings:
|
||||
result = input_mapping.display_name
|
||||
|
||||
if result.is_empty():
|
||||
result = action.display_name
|
||||
|
||||
return result
|
||||
|
||||
static func _is_effectively_remappable(action:GUIDEAction, input_mapping:GUIDEInputMapping) -> bool:
|
||||
return action.is_remappable and ((not input_mapping.override_action_settings) or input_mapping.is_remappable)
|
||||
|
||||
|
||||
static func _get_effective_value_type(action:GUIDEAction, input_mapping:GUIDEInputMapping) -> GUIDEAction.GUIDEActionValueType:
|
||||
if input_mapping.override_action_settings and input_mapping.input != null:
|
||||
return input_mapping.input._native_value_type()
|
||||
|
||||
return action.action_value_type
|
||||
|
||||
|
||||
## Returns a list of all collisions in all contexts when this new input would be applied to the config item.
|
||||
func get_input_collisions(item:ConfigItem, input:GUIDEInput) -> Array[ConfigItem]:
|
||||
if not _check_item(item):
|
||||
return []
|
||||
var result:Array[ConfigItem] = []
|
||||
|
||||
if input == null:
|
||||
# no item collides with absent input
|
||||
return result
|
||||
|
||||
# walk over all known contexts and find any mappings.
|
||||
for context:GUIDEMappingContext in _mapping_contexts:
|
||||
for action_mapping:GUIDEActionMapping in context.mappings:
|
||||
for index:int in action_mapping.input_mappings.size():
|
||||
var action := action_mapping.action
|
||||
if context == item.context and action == item.action and index == item.index:
|
||||
# collisions with self are allowed
|
||||
continue
|
||||
|
||||
var input_mapping:GUIDEInputMapping = action_mapping.input_mappings[index]
|
||||
var bound_input:GUIDEInput = input_mapping.input
|
||||
# check if this is currently overridden
|
||||
if _remapping_config._has(context, action, index):
|
||||
bound_input = _remapping_config._get_bound_input_or_null(context, action, index)
|
||||
|
||||
# We have a collision
|
||||
if bound_input != null and bound_input.is_same_as(input):
|
||||
var collision_item := ConfigItem.new(context, action, index, input_mapping)
|
||||
item_changed.connect(collision_item._item_changed)
|
||||
result.append(collision_item)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
## Gets the input currently bound to the action in the given context. Can be null if the input
|
||||
## is currently not bound.
|
||||
func get_bound_input_or_null(item:ConfigItem) -> GUIDEInput:
|
||||
if not _check_item(item):
|
||||
return null
|
||||
|
||||
# If the remapping config has a binding for this, this binding wins.
|
||||
if _remapping_config._has(item.context, item.action, item.index):
|
||||
return _remapping_config._get_bound_input_or_null(item.context, item.action, item.index)
|
||||
|
||||
# otherwise return the default binding for this action in the context
|
||||
for action_mapping:GUIDEActionMapping in item.context.mappings:
|
||||
if action_mapping.action == item.action:
|
||||
if action_mapping.input_mappings.size() > item.index:
|
||||
return action_mapping.input_mappings[item.index].input
|
||||
else:
|
||||
push_error("Action mapping does not have an index of ", item.index , ".")
|
||||
|
||||
return null
|
||||
|
||||
## Sets the bound input to the new value for the given config item. Ignores collisions
|
||||
## because collision resolution is highly game specific. Use get_input_collisions to find
|
||||
## potential collisions and then resolve them in a way that suits the game. Note that
|
||||
## bound input can be set to null, which deliberately unbinds the input. If you want
|
||||
## to restore the defaults, call restore_default instead.
|
||||
func set_bound_input(item:ConfigItem, input:GUIDEInput) -> void:
|
||||
if not _check_item(item):
|
||||
return
|
||||
|
||||
# first remove any custom binding we have
|
||||
_remapping_config._clear(item.context, item.action, item.index)
|
||||
|
||||
# Now check if the input is the same as the default
|
||||
var bound_input:GUIDEInput = get_bound_input_or_null(item)
|
||||
|
||||
if bound_input == null and input == null:
|
||||
item_changed.emit(item, input)
|
||||
return # nothing to do
|
||||
|
||||
if bound_input == null:
|
||||
_remapping_config._bind(item.context, item.action, input, item.index)
|
||||
item_changed.emit(item, input)
|
||||
return
|
||||
|
||||
if bound_input != null and input != null and bound_input.is_same_as(input):
|
||||
item_changed.emit(item, input)
|
||||
return # nothing to do
|
||||
|
||||
_remapping_config._bind(item.context, item.action, input, item.index)
|
||||
item_changed.emit(item, input)
|
||||
|
||||
|
||||
## Returns the default binding for the given config item.
|
||||
func get_default_input(item:ConfigItem) -> GUIDEInput:
|
||||
if not _check_item(item):
|
||||
return null
|
||||
|
||||
for mapping:GUIDEActionMapping in item.context.mappings:
|
||||
if mapping.action == item.action:
|
||||
# _check_item verifies the index exists, so no need to check here.
|
||||
return mapping.input_mappings[item.index].input
|
||||
|
||||
return null
|
||||
|
||||
|
||||
## Restores the default binding for the given config item. Note that this may
|
||||
## introduce a conflict if other bindings have bound conflicting input. You can
|
||||
## call get_default_input for the given item to get the default input and then
|
||||
## call get_input_collisions for that to find out whether you would get a collision.
|
||||
func restore_default_for(item:ConfigItem) -> void:
|
||||
if not _check_item(item):
|
||||
return
|
||||
|
||||
_remapping_config._clear(item.context, item.action, item.index)
|
||||
item_changed.emit(item, get_bound_input_or_null(item))
|
||||
|
||||
|
||||
|
||||
## Verifies that the given item is valid.
|
||||
func _check_item(item:ConfigItem) -> bool:
|
||||
if not _mapping_contexts.has(item.context):
|
||||
push_error("Given context is not known to this mapper. Did you call initialize()?")
|
||||
return false
|
||||
|
||||
var action_found := false
|
||||
var size_ok := false
|
||||
for mapping in item.context.mappings:
|
||||
if mapping.action == item.action:
|
||||
action_found = true
|
||||
if mapping.input_mappings.size() > item.index and item.index >= 0:
|
||||
size_ok = true
|
||||
break
|
||||
|
||||
if not action_found:
|
||||
push_error("Given action does not belong to the given context.")
|
||||
return false
|
||||
|
||||
if not size_ok:
|
||||
push_error("Given index does not exist for the given action's input binding.")
|
||||
|
||||
|
||||
if not item.action.is_remappable:
|
||||
push_error("Given action is not remappable.")
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
class ConfigItem:
|
||||
## Emitted when the input to this item has changed.
|
||||
signal changed(input:GUIDEInput)
|
||||
|
||||
var _input_mapping:GUIDEInputMapping
|
||||
|
||||
## The display category for this config item
|
||||
var display_category:String:
|
||||
get: return GUIDERemapper._get_effective_display_category(action, _input_mapping)
|
||||
|
||||
## The display name for this config item.
|
||||
var display_name:String:
|
||||
get: return GUIDERemapper._get_effective_display_name(action, _input_mapping)
|
||||
|
||||
## Whether this item is remappable.
|
||||
var is_remappable:bool:
|
||||
get: return GUIDERemapper._is_effectively_remappable(action, _input_mapping)
|
||||
|
||||
## The value type for this config item.
|
||||
var value_type:GUIDEAction.GUIDEActionValueType:
|
||||
get: return GUIDERemapper._get_effective_value_type(action, _input_mapping)
|
||||
|
||||
var context:GUIDEMappingContext
|
||||
var action:GUIDEAction
|
||||
var index:int
|
||||
|
||||
func _init(context:GUIDEMappingContext, action:GUIDEAction, index:int, input_mapping:GUIDEInputMapping):
|
||||
self.context = context
|
||||
self.action = action
|
||||
self.index = index
|
||||
_input_mapping = input_mapping
|
||||
|
||||
## Checks whether this config item is the same as some other
|
||||
## e.g. refers to the same input mapping.
|
||||
func is_same_as(other:ConfigItem) -> bool:
|
||||
return context == other.context and \
|
||||
action == other.action and \
|
||||
index == other.index
|
||||
|
||||
func _item_changed(item:ConfigItem, input:GUIDEInput):
|
||||
if item.is_same_as(self):
|
||||
changed.emit(input)
|
||||
|
85
addons/guide/remapping/guide_remapping_config.gd
Normal file
@@ -0,0 +1,85 @@
|
||||
@icon("res://addons/guide/guide_internal.svg")
|
||||
## A remapping configuration. This only holds changes to the context mapping,
|
||||
## so to get the full input map you need to apply this on top of one or more
|
||||
## mapping contexts. The settings from this config take precedence over the
|
||||
## settings from the mapping contexts.
|
||||
class_name GUIDERemappingConfig
|
||||
extends Resource
|
||||
|
||||
## Dictionary with remapped inputs. Structure is:
|
||||
## {
|
||||
## mapping_context : {
|
||||
## action : {
|
||||
## index : bound input
|
||||
## ...
|
||||
## }, ...
|
||||
## }
|
||||
## The bound input can be NULL which means that this was deliberately unbound.
|
||||
@export var remapped_inputs:Dictionary = {}
|
||||
|
||||
## Dictionary for additional custom data to store (e.g. modifier settings, etc.)
|
||||
## Note that this data is completely under application control and it's the responsibility
|
||||
## of the application to ensure that this data is serializable and gets applied at
|
||||
## the necessary point in time.
|
||||
@export var custom_data:Dictionary = {}
|
||||
|
||||
## Binds the given input to the given action. Index can be given to have
|
||||
## alternative bindings for the same action.
|
||||
func _bind(mapping_context:GUIDEMappingContext, action:GUIDEAction, input:GUIDEInput, index:int = 0) -> void:
|
||||
if not remapped_inputs.has(mapping_context):
|
||||
remapped_inputs[mapping_context] = {}
|
||||
|
||||
if not remapped_inputs[mapping_context].has(action):
|
||||
remapped_inputs[mapping_context][action] = {}
|
||||
|
||||
remapped_inputs[mapping_context][action][index] = input
|
||||
|
||||
|
||||
## Unbinds the given input from the given action. This is a deliberate unbind
|
||||
## which means that the action should not be triggerable by the input anymore. It
|
||||
## its not the same as _clear.
|
||||
func _unbind(mapping_context:GUIDEMappingContext, action:GUIDEAction, index:int = 0) -> void:
|
||||
_bind(mapping_context, action, null, index)
|
||||
|
||||
|
||||
## Removes the given input action binding from this configuration. The action will
|
||||
## now have the default input that it has in the mapping_context. This is not the
|
||||
## same as _unbind.
|
||||
func _clear(mapping_context:GUIDEMappingContext, action:GUIDEAction, index:int = 0) -> void:
|
||||
if not remapped_inputs.has(mapping_context):
|
||||
return
|
||||
|
||||
if not remapped_inputs[mapping_context].has(action):
|
||||
return
|
||||
|
||||
remapped_inputs[mapping_context][action].erase(index)
|
||||
|
||||
if remapped_inputs[mapping_context][action].is_empty():
|
||||
remapped_inputs[mapping_context].erase(action)
|
||||
|
||||
if remapped_inputs[mapping_context].is_empty():
|
||||
remapped_inputs.erase(mapping_context)
|
||||
|
||||
|
||||
## Returns the bound input for the given action name and index. Returns null
|
||||
## if there is matching binding.
|
||||
func _get_bound_input_or_null(mapping_context:GUIDEMappingContext, action:GUIDEAction, index:int = 0) -> GUIDEInput:
|
||||
if not remapped_inputs.has(mapping_context):
|
||||
return null
|
||||
|
||||
if not remapped_inputs[mapping_context].has(action):
|
||||
return null
|
||||
|
||||
return remapped_inputs[mapping_context][action].get(index, null)
|
||||
|
||||
|
||||
## Returns whether or not this mapping has a configuration for the given combination (even if the
|
||||
## combination is set to null).
|
||||
func _has(mapping_context:GUIDEMappingContext, action:GUIDEAction, index:int = 0) -> bool:
|
||||
if not remapped_inputs.has(mapping_context):
|
||||
return false
|
||||
|
||||
if not remapped_inputs[mapping_context].has(action):
|
||||
return false
|
||||
|
||||
return remapped_inputs[mapping_context][action].has(index)
|
65
addons/guide/triggers/guide_trigger.gd
Normal file
@@ -0,0 +1,65 @@
|
||||
@tool
|
||||
@icon("res://addons/guide/triggers/guide_trigger.svg")
|
||||
class_name GUIDETrigger
|
||||
extends Resource
|
||||
|
||||
enum GUIDETriggerState {
|
||||
## The trigger did not fire.
|
||||
NONE,
|
||||
## The trigger's conditions are partially met
|
||||
ONGOING,
|
||||
## The trigger has fired.
|
||||
TRIGGERED
|
||||
}
|
||||
|
||||
enum GUIDETriggerType {
|
||||
# If there are more than one explicit triggers at least one must trigger
|
||||
# for the action to trigger.
|
||||
EXPLICIT = 1,
|
||||
# All implicit triggers must trigger for the action to trigger.
|
||||
IMPLICIT = 2,
|
||||
# All blocking triggers prevent the action from triggering.
|
||||
BLOCKING = 3
|
||||
}
|
||||
|
||||
|
||||
@export var actuation_threshold:float = 0.5
|
||||
var _last_value:Vector3
|
||||
|
||||
## Returns the trigger type of this trigger.
|
||||
func _get_trigger_type() -> GUIDETriggerType:
|
||||
return GUIDETriggerType.EXPLICIT
|
||||
|
||||
|
||||
func _update_state(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
|
||||
func _is_actuated(input:Vector3, value_type:GUIDEAction.GUIDEActionValueType) -> bool:
|
||||
match value_type:
|
||||
GUIDEAction.GUIDEActionValueType.AXIS_1D, GUIDEAction.GUIDEActionValueType.BOOL:
|
||||
return _is_axis1d_actuated(input)
|
||||
GUIDEAction.GUIDEActionValueType.AXIS_2D:
|
||||
return _is_axis2d_actuated(input)
|
||||
GUIDEAction.GUIDEActionValueType.AXIS_3D:
|
||||
return _is_axis3d_actuated(input)
|
||||
|
||||
return false
|
||||
|
||||
## Checks if a 1D input is actuated.
|
||||
func _is_axis1d_actuated(input:Vector3) -> bool:
|
||||
return is_finite(input.x) and abs(input.x) > actuation_threshold
|
||||
|
||||
## Checks if a 2D input is actuated.
|
||||
func _is_axis2d_actuated(input:Vector3) -> bool:
|
||||
return is_finite(input.x) and is_finite(input.y) and Vector2(input.x, input.y).length_squared() > actuation_threshold * actuation_threshold
|
||||
|
||||
## Checks if a 3D input is actuated.
|
||||
func _is_axis3d_actuated(input:Vector3) -> bool:
|
||||
return input.is_finite() and input.length_squared() > actuation_threshold * actuation_threshold
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "GUIDETrigger"
|
||||
|
||||
func _editor_description() -> String:
|
||||
return ""
|
12
addons/guide/triggers/guide_trigger.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1.07241,0,0,1.07396,-3.11767,-2.34767)">
|
||||
<path d="M17.827,2.164C26.061,2.164 32.747,8.85 32.747,17.084C32.747,25.319 26.061,32.004 17.827,32.004C9.592,32.004 2.907,25.319 2.907,17.084C2.907,8.85 9.592,2.164 17.827,2.164ZM17.827,4.857C11.08,4.857 5.604,10.337 5.604,17.084C5.604,23.831 11.08,29.311 17.827,29.311C24.574,29.311 30.05,23.831 30.05,17.084C30.05,10.337 24.574,4.857 17.827,4.857Z" style="fill:rgb(253,150,0);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-2.69665,-2.69876)">
|
||||
<g transform="matrix(24,0,0,24,11.6286,27.2968)">
|
||||
<path d="M0.575,-0.717L0.575,-0.635L0.344,-0.635L0.344,-0L0.247,-0L0.247,-0.635L0.014,-0.635L0.014,-0.717L0.575,-0.717Z" style="fill:rgb(253,150,0);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
38
addons/guide/triggers/guide_trigger.svg.import
Normal file
@@ -0,0 +1,38 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://ca1eiagyinhl7"
|
||||
path="res://.godot/imported/guide_trigger.svg-cd87acbd491929cf49a255f8481b0b63.ctex"
|
||||
metadata={
|
||||
"has_editor_variant": true,
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/guide/triggers/guide_trigger.svg"
|
||||
dest_files=["res://.godot/imported/guide_trigger.svg-cd87acbd491929cf49a255f8481b0b63.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=0.5
|
||||
editor/scale_with_editor_scale=true
|
||||
editor/convert_colors_with_editor_theme=false
|
28
addons/guide/triggers/guide_trigger_chorded_action.gd
Normal file
@@ -0,0 +1,28 @@
|
||||
## Fires, when the given action is currently triggering. This trigger is implicit,
|
||||
## so it will prevent the action from triggering even if other triggers are successful.
|
||||
@tool
|
||||
class_name GUIDETriggerChordedAction
|
||||
extends GUIDETrigger
|
||||
|
||||
@export var action:GUIDEAction
|
||||
|
||||
|
||||
func _get_trigger_type() -> GUIDETriggerType:
|
||||
return GUIDETriggerType.IMPLICIT
|
||||
|
||||
func _update_state(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
|
||||
if action == null:
|
||||
push_warning("Chorded trigger without action will never trigger.")
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
if action.is_triggered():
|
||||
return GUIDETriggerState.TRIGGERED
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Chorded Action"
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Fires, when the given action is currently triggering. This trigger is implicit,\n" + \
|
||||
"so it will prevent the action from triggering even if other triggers are successful."
|
117
addons/guide/triggers/guide_trigger_combo.gd
Normal file
@@ -0,0 +1,117 @@
|
||||
@tool
|
||||
class_name GUIDETriggerCombo
|
||||
extends GUIDETrigger
|
||||
|
||||
enum ActionEventType {
|
||||
TRIGGERED = 1,
|
||||
STARTED = 2,
|
||||
ONGOING = 4,
|
||||
CANCELLED = 8,
|
||||
COMPLETED = 16
|
||||
}
|
||||
|
||||
## If set to true, the combo trigger will print information
|
||||
## about state changes to the debug log.
|
||||
@export var enable_debug_print:bool = false
|
||||
@export var steps:Array[GUIDETriggerComboStep] = []
|
||||
@export var cancellation_actions:Array[GUIDETriggerComboCancelAction] = []
|
||||
|
||||
var _current_step:int = -1
|
||||
var _remaining_time:float = 0
|
||||
|
||||
func _update_state(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
|
||||
if steps.is_empty():
|
||||
push_warning("Combo with no steps will never fire.")
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
# initial setup
|
||||
if _current_step == -1:
|
||||
for step in steps:
|
||||
step._prepare()
|
||||
for action in cancellation_actions:
|
||||
action._prepare()
|
||||
_reset()
|
||||
|
||||
|
||||
var current_action := steps[_current_step].action
|
||||
if current_action == null:
|
||||
push_warning("Step ", _current_step , " has no action ", resource_path)
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
# check if any of our cancellation actions fired
|
||||
for action in cancellation_actions:
|
||||
# if the action is the current action we don't count its firing as cancellation
|
||||
if action.action == current_action:
|
||||
continue
|
||||
|
||||
if action._has_fired:
|
||||
if enable_debug_print:
|
||||
print("Combo cancelled by action '", action.action._editor_name(), "'.")
|
||||
_reset()
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
# check if any of the steps has fired out of order
|
||||
for step in steps:
|
||||
if step.action == current_action:
|
||||
continue
|
||||
|
||||
if step._has_fired:
|
||||
if enable_debug_print:
|
||||
print("Combo out of order step by action '", step.action._editor_name(), "'.")
|
||||
_reset()
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
# check if we took too long (unless we're in the first step)
|
||||
if _current_step > 0:
|
||||
_remaining_time -= delta
|
||||
if _remaining_time <= 0.0:
|
||||
if enable_debug_print:
|
||||
print("Step time for step ", _current_step , " exceeded.")
|
||||
_reset()
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
# if the current action was fired, if so advance to the next
|
||||
if steps[_current_step]._has_fired:
|
||||
# reset this step, so it will not count as misfired next round
|
||||
steps[_current_step]._has_fired = false
|
||||
if _current_step + 1 >= steps.size():
|
||||
# we finished the combo
|
||||
if enable_debug_print:
|
||||
print("Combo fired.")
|
||||
_reset()
|
||||
return GUIDETriggerState.TRIGGERED
|
||||
|
||||
# otherwise, pick the next step
|
||||
_current_step += 1
|
||||
if enable_debug_print:
|
||||
print("Combo advanced to step " , _current_step, ".")
|
||||
_remaining_time = steps[_current_step].time_to_actuate
|
||||
|
||||
# Reset all steps and cancellation actions to "not fired" in
|
||||
# case they were triggered by this action. Otherwise a double-tap
|
||||
# would immediately fire for both taps once the first is through
|
||||
for step in steps:
|
||||
step._has_fired = false
|
||||
for action in cancellation_actions:
|
||||
action._has_fired = false
|
||||
|
||||
# and in any case we're still processing.
|
||||
return GUIDETriggerState.ONGOING
|
||||
|
||||
|
||||
func _reset():
|
||||
if enable_debug_print:
|
||||
print("Combo reset.")
|
||||
_current_step = 0
|
||||
_remaining_time = steps[0].time_to_actuate
|
||||
for step in steps:
|
||||
step._has_fired = false
|
||||
for action in cancellation_actions:
|
||||
action._has_fired = false
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Combo"
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Fires, when the input exceeds the actuation threshold."
|
||||
|
27
addons/guide/triggers/guide_trigger_combo_cancel_action.gd
Normal file
@@ -0,0 +1,27 @@
|
||||
@icon("res://addons/guide/guide_internal.svg")
|
||||
class_name GUIDETriggerComboCancelAction
|
||||
extends Resource
|
||||
|
||||
@export var action:GUIDEAction
|
||||
@export_flags("Triggered:1", "Started:2", "Ongoing:4", "Cancelled:8","Completed:16")
|
||||
var completion_events:int = GUIDETriggerCombo.ActionEventType.TRIGGERED
|
||||
|
||||
var _has_fired:bool = false
|
||||
|
||||
func _prepare():
|
||||
if completion_events & GUIDETriggerCombo.ActionEventType.TRIGGERED:
|
||||
action.triggered.connect(_fired)
|
||||
if completion_events & GUIDETriggerCombo.ActionEventType.STARTED:
|
||||
action.started.connect(_fired)
|
||||
if completion_events & GUIDETriggerCombo.ActionEventType.ONGOING:
|
||||
action.ongoing.connect(_fired)
|
||||
if completion_events & GUIDETriggerCombo.ActionEventType.CANCELLED:
|
||||
action.cancelled.connect(_fired)
|
||||
if completion_events & GUIDETriggerCombo.ActionEventType.COMPLETED:
|
||||
action.completed.connect(_fired)
|
||||
_has_fired = false
|
||||
|
||||
|
||||
func _fired():
|
||||
_has_fired = true
|
||||
|
29
addons/guide/triggers/guide_trigger_combo_step.gd
Normal file
@@ -0,0 +1,29 @@
|
||||
@icon("res://addons/guide/guide_internal.svg")
|
||||
class_name GUIDETriggerComboStep
|
||||
extends Resource
|
||||
|
||||
@export var action:GUIDEAction
|
||||
@export_flags("Triggered:1", "Started:2", "Ongoing:4", "Cancelled:8","Completed:16")
|
||||
var completion_events:int = GUIDETriggerCombo.ActionEventType.TRIGGERED
|
||||
@export var time_to_actuate:float = 0.5
|
||||
|
||||
|
||||
var _has_fired:bool = false
|
||||
|
||||
func _prepare():
|
||||
if completion_events & GUIDETriggerCombo.ActionEventType.TRIGGERED:
|
||||
action.triggered.connect(_fired)
|
||||
if completion_events & GUIDETriggerCombo.ActionEventType.STARTED:
|
||||
action.started.connect(_fired)
|
||||
if completion_events & GUIDETriggerCombo.ActionEventType.ONGOING:
|
||||
action.ongoing.connect(_fired)
|
||||
if completion_events & GUIDETriggerCombo.ActionEventType.CANCELLED:
|
||||
action.cancelled.connect(_fired)
|
||||
if completion_events & GUIDETriggerCombo.ActionEventType.COMPLETED:
|
||||
action.completed.connect(_fired)
|
||||
_has_fired = false
|
||||
|
||||
|
||||
func _fired():
|
||||
_has_fired = true
|
||||
|
20
addons/guide/triggers/guide_trigger_down.gd
Normal file
@@ -0,0 +1,20 @@
|
||||
## Fires, when the input exceeds the actuation threshold. This is
|
||||
## the default trigger when no trigger is specified.
|
||||
@tool
|
||||
class_name GUIDETriggerDown
|
||||
extends GUIDETrigger
|
||||
|
||||
func _update_state(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
|
||||
# if the input is actuated, then the trigger is triggered.
|
||||
if _is_actuated(input, value_type):
|
||||
return GUIDETriggerState.TRIGGERED
|
||||
# otherwise, the trigger is not triggered.
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Down"
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Fires, when the input exceeds the actuation threshold. This is\n" +\
|
||||
"the default trigger when no trigger is specified."
|
43
addons/guide/triggers/guide_trigger_hold.gd
Normal file
@@ -0,0 +1,43 @@
|
||||
@tool
|
||||
## A trigger that activates when the input is held down for a certain amount of time.
|
||||
class_name GUIDETriggerHold
|
||||
extends GUIDETrigger
|
||||
|
||||
## The time for how long the input must be held.
|
||||
@export var hold_treshold:float = 1.0
|
||||
## If true, the trigger will only fire once until the input is released. Otherwise the trigger will fire every frame.
|
||||
@export var is_one_shot:bool = false
|
||||
|
||||
var _accumulated_time:float = 0
|
||||
var _did_shoot:bool = false
|
||||
|
||||
func _update_state(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
|
||||
# if the input is actuated, accumulate time and check if the hold threshold has been reached
|
||||
if _is_actuated(input, value_type):
|
||||
_accumulated_time += delta
|
||||
|
||||
if _accumulated_time >= hold_treshold:
|
||||
# if the trigger is one shot and we already shot, then we will not trigger again.
|
||||
if is_one_shot and _did_shoot:
|
||||
return GUIDETriggerState.NONE
|
||||
else:
|
||||
# otherwise, we will just trigger.
|
||||
_did_shoot = true
|
||||
return GUIDETriggerState.TRIGGERED
|
||||
else:
|
||||
# if the hold threshold has not been reached, then the trigger is ongoing.
|
||||
return GUIDETriggerState.ONGOING
|
||||
else:
|
||||
# if the input is not actuated, then the trigger is not triggered and we reset the accumulated time.
|
||||
# and our one shot flag.
|
||||
_accumulated_time = 0
|
||||
_did_shoot = false
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Hold"
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Fires, once the input has remained actuated for hold_threshold seconds.\n" + \
|
||||
"My fire once or repeatedly."
|
22
addons/guide/triggers/guide_trigger_pressed.gd
Normal file
@@ -0,0 +1,22 @@
|
||||
@tool
|
||||
## A trigger that activates when the input is pushed down. Will only emit a
|
||||
## trigger event once. Holding the input will not trigger further events.
|
||||
class_name GUIDETriggerPressed
|
||||
extends GUIDETrigger
|
||||
|
||||
|
||||
func _update_state(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
|
||||
if _is_actuated(input, value_type):
|
||||
if not _is_actuated(_last_value, value_type):
|
||||
return GUIDETriggerState.TRIGGERED
|
||||
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Pressed"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Fires once, when the input exceeds actuation threshold. Holding the input\n" + \
|
||||
"will not fire additional triggers."
|
86
addons/guide/triggers/guide_trigger_pulse.gd
Normal file
@@ -0,0 +1,86 @@
|
||||
@tool
|
||||
## A trigger that activates when the input is pushed down and then repeatedly sends trigger events at a fixed interval.
|
||||
## Note: the trigger will be either triggering or ongoing until the input is released.
|
||||
## Note: at most one pulse will be emitted per frame.
|
||||
class_name GUIDETriggerPulse
|
||||
extends GUIDETrigger
|
||||
|
||||
## If true, the trigger will trigger immediately when the input is actuated. Otherwise, the trigger will wait for the initial delay.
|
||||
@export var trigger_on_start:bool = true
|
||||
## The delay after the initial actuation before pulsing begins.
|
||||
@export var initial_delay:float = 0.3:
|
||||
set(value):
|
||||
initial_delay = max(0, value)
|
||||
|
||||
## The interval between pulses. Set to 0 to pulse every frame.
|
||||
@export var pulse_interval:float = 0.1:
|
||||
set(value):
|
||||
pulse_interval = max(0, value)
|
||||
|
||||
## Maximum number of pulses. If <= 0, the trigger will pulse indefinitely.
|
||||
@export var max_pulses:int = 0
|
||||
|
||||
var _delay_until_next_pulse:float = 0
|
||||
var _emitted_pulses:int = 0
|
||||
|
||||
func _update_state(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
|
||||
if _is_actuated(input, value_type):
|
||||
if not _is_actuated(_last_value, value_type):
|
||||
# we went from "not actuated" to actuated, pulsing starts
|
||||
_delay_until_next_pulse = initial_delay
|
||||
if trigger_on_start:
|
||||
return GUIDETriggerState.TRIGGERED
|
||||
else:
|
||||
return GUIDETriggerState.ONGOING
|
||||
|
||||
# if we already are pulsing and have exceeded the maximum number of pulses, we will not pulse anymore.
|
||||
if max_pulses > 0 and _emitted_pulses >= max_pulses:
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
# subtract the delta from the delay until the next pulse
|
||||
_delay_until_next_pulse -= delta
|
||||
|
||||
if _delay_until_next_pulse > 0:
|
||||
# we are still waiting for the next pulse, nothing to do.
|
||||
return GUIDETriggerState.ONGOING
|
||||
|
||||
# now delta could be larger than our pulse, in which case we loose a few pulses.
|
||||
# as we can pulse at most once per frame.
|
||||
|
||||
# in case someone sets the pulse interval to 0, we will pulse every frame.
|
||||
if is_equal_approx(pulse_interval, 0):
|
||||
_delay_until_next_pulse = 0
|
||||
if max_pulses > 0:
|
||||
_emitted_pulses += 1
|
||||
return GUIDETriggerState.TRIGGERED
|
||||
|
||||
# Now add the delay until the next pulse
|
||||
_delay_until_next_pulse += pulse_interval
|
||||
|
||||
# If the interval is really small, we can potentially have skipped some pulses
|
||||
if _delay_until_next_pulse <= 0:
|
||||
# we have skipped some pulses
|
||||
var skipped_pulses:int = int(-_delay_until_next_pulse / pulse_interval)
|
||||
_delay_until_next_pulse += skipped_pulses * pulse_interval
|
||||
if max_pulses > 0:
|
||||
_emitted_pulses += skipped_pulses
|
||||
if _emitted_pulses >= max_pulses:
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
# Record a pulse and return triggered
|
||||
if max_pulses > 0:
|
||||
_emitted_pulses += 1
|
||||
return GUIDETriggerState.TRIGGERED
|
||||
|
||||
# if the input is not actuated, then the trigger is not triggered.
|
||||
_emitted_pulses = 0
|
||||
_delay_until_next_pulse = 0
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Pulse"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Fires at an interval while the input is actuated."
|
21
addons/guide/triggers/guide_trigger_released.gd
Normal file
@@ -0,0 +1,21 @@
|
||||
@tool
|
||||
## A trigger that activates when the input is released down. Will only emit a
|
||||
## trigger event once.
|
||||
class_name GUIDETriggerReleased
|
||||
extends GUIDETrigger
|
||||
|
||||
|
||||
func _update_state(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
|
||||
if not _is_actuated(input, value_type):
|
||||
if _is_actuated(_last_value, value_type):
|
||||
return GUIDETriggerState.TRIGGERED
|
||||
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Released"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Fires once, when the input goes from actuated to not actuated. The opposite of the Pressed trigger."
|
72
addons/guide/triggers/guide_trigger_stability.gd
Normal file
@@ -0,0 +1,72 @@
|
||||
@tool
|
||||
## Triggers depending on whether the input changes while actuated. This trigger is
|
||||
## is implicit, so it must succeed for all other triggers to succeed.
|
||||
class_name GUIDETriggerStability
|
||||
extends GUIDETrigger
|
||||
|
||||
enum TriggerWhen {
|
||||
## Input must be stable
|
||||
INPUT_IS_STABLE,
|
||||
## Input must change
|
||||
INPUT_CHANGES
|
||||
}
|
||||
|
||||
|
||||
## The maximum amount that the input can change after actuation before it is
|
||||
## considered "changed".
|
||||
@export var max_deviation:float = 1
|
||||
|
||||
## When should the trigger trigger?
|
||||
@export var trigger_when:TriggerWhen = TriggerWhen.INPUT_IS_STABLE
|
||||
|
||||
|
||||
var _initial_value:Vector3
|
||||
var _deviated:bool = false
|
||||
|
||||
|
||||
func _get_trigger_type() -> GUIDETriggerType:
|
||||
return GUIDETriggerType.IMPLICIT
|
||||
|
||||
|
||||
func _update_state(input:Vector3, delta:float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
|
||||
if _is_actuated(input, value_type):
|
||||
if _deviated:
|
||||
if trigger_when == TriggerWhen.INPUT_IS_STABLE:
|
||||
return GUIDETriggerState.NONE
|
||||
return GUIDETriggerState.TRIGGERED
|
||||
|
||||
|
||||
if not _is_actuated(_last_value, value_type):
|
||||
# we went from "not actuated" to actuated, start
|
||||
_initial_value = input
|
||||
if trigger_when == TriggerWhen.INPUT_IS_STABLE:
|
||||
return GUIDETriggerState.TRIGGERED
|
||||
else:
|
||||
return GUIDETriggerState.ONGOING
|
||||
|
||||
# calculate how far the input is from the initial value
|
||||
if _initial_value.distance_squared_to(input) > (max_deviation * max_deviation):
|
||||
_deviated = true
|
||||
if trigger_when == TriggerWhen.INPUT_IS_STABLE:
|
||||
return GUIDETriggerState.NONE
|
||||
return GUIDETriggerState.TRIGGERED
|
||||
|
||||
if trigger_when == TriggerWhen.INPUT_IS_STABLE:
|
||||
return GUIDETriggerState.TRIGGERED
|
||||
|
||||
return GUIDETriggerState.ONGOING
|
||||
|
||||
# if the input is not actuated
|
||||
_deviated = false
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
|
||||
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Stability"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Triggers depending on whether the input changes while actuated. This trigger\n" +\
|
||||
"is implicit, so it must succeed for all other triggers to succeed."
|
48
addons/guide/triggers/guide_trigger_tap.gd
Normal file
@@ -0,0 +1,48 @@
|
||||
@tool
|
||||
## A trigger that activates when the input is tapped and released before the time threshold is reached.
|
||||
class_name GUIDETriggerTap
|
||||
extends GUIDETrigger
|
||||
|
||||
## The time threshold for the tap to be considered a tap.
|
||||
@export var tap_threshold: float = 0.2
|
||||
|
||||
var _accumulated_time: float = 0
|
||||
|
||||
|
||||
func _update_state(input: Vector3, delta: float, value_type:GUIDEAction.GUIDEActionValueType) -> GUIDETriggerState:
|
||||
if _is_actuated(input, value_type):
|
||||
# if the input was actuated before, and the tap threshold has been exceeded, the trigger is locked down
|
||||
# until the input is released and we can exit out early
|
||||
if _is_actuated(_last_value, value_type) and _accumulated_time > tap_threshold:
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
# accumulate time
|
||||
_accumulated_time += delta
|
||||
|
||||
if _accumulated_time < tap_threshold:
|
||||
return GUIDETriggerState.ONGOING
|
||||
else:
|
||||
# we have exceeded the tap threshold, so the tap is not triggered.
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
else: # not actuated right now
|
||||
# if the input was actuated before...
|
||||
if _is_actuated(_last_value, value_type):
|
||||
# ... and the accumulated time is less than the threshold, then the tap is triggered.
|
||||
if _accumulated_time < tap_threshold:
|
||||
_accumulated_time = 0
|
||||
return GUIDETriggerState.TRIGGERED
|
||||
|
||||
# Otherwise, the tap is not triggered, but we reset the accumulated time
|
||||
# so the trigger is now again ready to be triggered.
|
||||
_accumulated_time = 0
|
||||
|
||||
# in either case, the trigger is not triggered.
|
||||
return GUIDETriggerState.NONE
|
||||
|
||||
func _editor_name() -> String:
|
||||
return "Tap"
|
||||
|
||||
|
||||
func _editor_description() -> String:
|
||||
return "Fires when the input is actuated and released within the given timeframe."
|
30
addons/guide/ui/guide_icon_renderer.gd
Normal file
@@ -0,0 +1,30 @@
|
||||
## Base class for icon renderers. Note that all icon renderers must be tool
|
||||
## scripts.
|
||||
@tool
|
||||
class_name GUIDEIconRenderer
|
||||
extends Control
|
||||
|
||||
## The priority of this icon renderer. Built-in renderers use priority 0. Built-in
|
||||
## fallback renderer uses priority 100. The smaller the number the higher the priority.
|
||||
@export var priority:int = 0
|
||||
|
||||
## Whether or not this renderer can render an icon for this input.
|
||||
func supports(input:GUIDEInput) -> bool:
|
||||
return false
|
||||
|
||||
## Set up the scene so that the given input can be rendered. This will
|
||||
## only be called for input where `supports` has returned true.
|
||||
func render(input:GUIDEInput) -> void:
|
||||
pass
|
||||
|
||||
|
||||
## A cache key for the given input. This should be unique for this renderer
|
||||
## and the given input. The same input should yield the same cache key for
|
||||
## each renderer.
|
||||
func cache_key(input:GUIDEInput) -> String:
|
||||
push_error("Custom renderers must override the cache_key function to ensure proper caching.")
|
||||
return "i-forgot-the-cache-key"
|
||||
|
||||
func _ready():
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
|