Add cap sprite to child scene and update project configuration

This commit is contained in:
2025-04-26 03:52:45 +02:00
parent d95176fba0
commit 0c1192536c
374 changed files with 11968 additions and 1276 deletions

View File

@@ -1,3 +0,0 @@
<svg height="16" width="16" viewBox="0 0 16 16" 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-linecap:round;stroke-linejoin:round;">
<path d="M8.378,13.915C8.378,13.915 5.426,14.047 4.724,10.406C4.746,6.98 8.051,2.631 8.051,2.631C8.051,2.631 10.859,4.921 11.682,10.223C11.472,13.834 8.378,13.915 8.378,13.915Z" style="fill:none;fill-rule:nonzero;stroke:rgb(141,165,243);stroke-width:2px;"/>
</svg>

Before

Width:  |  Height:  |  Size: 555 B

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 Fabrice Cipolla, Sp3ctralCat and Dragos Daian
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,10 +0,0 @@
<?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 16 16" 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.56913,0,0,1.56913,-4.55301,-4.55301)">
<circle cx="8" cy="8" r="5" style="fill:url(#_Radial1);"/>
</g>
<defs>
<radialGradient id="_Radial1" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.14354,-2.64625,2.64625,4.14354,8.03935,7.99693)"><stop offset="0" style="stop-color:white;stop-opacity:1"/><stop offset="0.52" style="stop-color:white;stop-opacity:0.76"/><stop offset="1" style="stop-color:white;stop-opacity:0"/></radialGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 979 B

View File

@@ -1,288 +0,0 @@
Godot Rapier incorporates third-party material from the projects listed below.
Godot Engine (https://github.com/godotengine/godot)
Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md).
Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
godot-cpp (https://github.com/godotengine/godot-cpp)
Copyright (c) 2017-present Godot Engine contributors.
Copyright (c) 2022-present Mikael Hermansson.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
rapier (https://github.com/dimforge/rapier)
Copyright 2020 Sébastien Crozet
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Godot Jolt (https://github.com/godot-jolt/godot-jolt)
Copyright (c) Mikael Hermansson and Godot Jolt contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
salva (https://github.com/dimforge/salva)
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2020 Sébastien Crozet
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>libgodot_rapier.ios</string>
<key>CFBundleName</key>
<string>Godot Rapier2D</string>
<key>CFBundleDisplayName</key>
<string>Godot Rapier2D</string>
<key>CFBundleIdentifier</key>
<string>org.godot-rapier2d.godot-rapier2d</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright (c) 2023-present Fabrice Cipolla, Sp3ctralCat and Dragos Daian.</string>
<key>CFBundleVersion</key>
<string>0.12.0</string>
<key>CFBundleShortVersionString</key>
<string>0.12.0</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CSResourcesFileMapped</key>
<true/>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

View File

@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>libgodot_rapier.macos.dylib</string>
<key>CFBundleIdentifier</key>
<string>org.godot-rapier2d.godot-rapier2d</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Godot Rapier2D</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>NSHumanReadableCopyright</key>
<string>Copyright (c) 2023-present Fabrice Cipolla, Sp3ctralCat and Dragos Daian.</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>LSMinimumSystemVersion</key>
<string>10.12</string>
</dict>
</plist>

View File

@@ -1,128 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>files</key>
<dict>
<key>Resources/Info.plist</key>
<data>
FZy7+eYDJuYSbntBY+mwtyopSvE=
</data>
</dict>
<key>files2</key>
<dict>
<key>Resources/Info.plist</key>
<dict>
<key>hash2</key>
<data>
MMDYIVdI76poLGbcF02jJpJ+oHcs7q1NjyfgsaN1jus=
</data>
</dict>
</dict>
<key>rules</key>
<dict>
<key>^Resources/</key>
<true/>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^Resources/.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Resources/Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^version.plist$</key>
<true/>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^.*</key>
<true/>
<key>^Info\.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^PkgInfo$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^Resources/</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^Resources/.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Resources/Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^[^/]+$</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^embedded\.provisionprofile$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^version\.plist$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
</dict>
</dict>
</plist>

View File

@@ -1,15 +0,0 @@
[gd_resource type="ArrayMesh" format=3 uid="uid://dahp28qij58i1"]
[resource]
_surfaces = [{
"2d": true,
"aabb": AABB(-16, -16, 0, 32, 32, 0),
"attribute_data": PackedByteArray(0, 0, 0, 0, 0, 0, 128, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 63, 0, 0, 0, 0, 0, 0, 128, 63, 0, 0, 128, 63),
"format": 34393296913,
"index_count": 6,
"index_data": PackedByteArray(3, 0, 0, 0, 1, 0, 1, 0, 2, 0, 3, 0),
"primitive": 3,
"uv_scale": Vector4(0, 0, 0, 0),
"vertex_count": 4,
"vertex_data": PackedByteArray(0, 0, 128, 193, 0, 0, 128, 65, 0, 0, 128, 193, 0, 0, 128, 193, 0, 0, 128, 65, 0, 0, 128, 193, 0, 0, 128, 65, 0, 0, 128, 65)
}]

View File

@@ -1,27 +0,0 @@
class_name Faucet2D
extends Fluid2D
@export var interval := 0.06
@export var max_particles: int = 1000
@export var width: int = 4
@export var height: int = 2
var points_new: PackedVector2Array
var velocities_new: PackedVector2Array
func _ready():
points_new = create_rectangle_points(width, height)
velocities_new.resize(points_new.size())
var gravity_value = ProjectSettings.get("physics/2d/default_gravity")
var gravity_dir = ProjectSettings.get("physics/2d/default_gravity_vector")
var dir = global_transform.basis_xform(gravity_dir * gravity_value)
velocities_new.fill(dir)
get_tree().create_timer(interval).timeout.connect(_on_timer_timeout)
func _on_timer_timeout():
get_tree().create_timer(interval).timeout.connect(_on_timer_timeout)
if len(points) > max_particles:
return
add_points_and_velocities(points_new, velocities_new)

View File

@@ -1,10 +0,0 @@
@tool
extends Fluid2D
@export var circle_radius := 10:
set(value):
if circle_radius != value:
circle_radius = value
points = create_circle_points(circle_radius)
get:
return circle_radius

View File

@@ -1,17 +0,0 @@
@tool
extends Fluid2D
@export var height := 10:
set(value):
if height != value:
height = value
points = create_rectangle_points(width, height)
get:
return height
@export var width := 10:
set(value):
if width != value:
width = value
points = create_rectangle_points(width, height)
get:
return width

View File

@@ -1,31 +0,0 @@
@tool
class_name Fluid2DRenderer
extends MultiMeshInstance2D
@export var fluid: Fluid2D
@export var color: Color = Color(0.8, 0.8, 0.8, 0.3)
@export var mesh_scale: Vector2 = Vector2(5, 5)
func _ready():
if multimesh == null:
multimesh = MultiMesh.new()
multimesh.mesh = load("res://addons/godot-rapier2d/circle_mesh.tres").duplicate()
multimesh.use_colors = true
if texture == null:
texture = load("res://addons/godot-rapier2d/Radial2D.svg")
func _process(_delta):
if fluid == null:
return
global_transform = fluid.global_transform
var index = 0
multimesh.instance_count = fluid.points.size()
var points = fluid.points
for i in points.size():
var point = points[i]
var new_transform: Transform2D = Transform2D(0, mesh_scale, 0, point)
multimesh.set_instance_transform_2d(index, new_transform)
multimesh.set_instance_color(index, color)
index += 1

View File

@@ -1,87 +0,0 @@
@tool
class_name Fluid2DShaderRenderer
extends CanvasLayer
@export var fluid: Fluid2D:
set(value):
fluid = value
update_configuration_warnings()
@export var camera: Camera2D
@export var water_material: Material = load("res://addons/godot-rapier2d/water_shader.tres")
@export var mesh_scale: Vector2 = Vector2(5, 5)
var fluid_renderer: Fluid2DRenderer
var inside_camera: Camera2D:
set(value):
inside_camera = value
update_configuration_warnings()
var sub_viewport_container: SubViewportContainer
var sub_viewport: SubViewport
func _get_configuration_warnings():
var warnings = []
if camera == null:
warnings += ["Camera property is empty."]
if fluid == null:
warnings += ["Fluid property is empty."]
return warnings
func _create_subviewport_container():
sub_viewport_container = SubViewportContainer.new()
sub_viewport_container.name = "SubViewportContainer"
add_child(sub_viewport_container)
sub_viewport_container.material = water_material
sub_viewport_container.size = Vector2(
ProjectSettings.get("display/window/size/viewport_width"),
ProjectSettings.get("display/window/size/viewport_height")
)
func _create_subviewport():
sub_viewport = SubViewport.new()
sub_viewport.name = "SubViewport"
sub_viewport_container.add_child(sub_viewport)
sub_viewport.transparent_bg = true
sub_viewport.size = sub_viewport_container.size
func _create_fluid_renderer():
fluid_renderer = Fluid2DRenderer.new()
fluid_renderer.name = "Fluid2DRenderer"
fluid_renderer.color = Color(255, 0, 255)
fluid_renderer.mesh_scale = mesh_scale
fluid_renderer.fluid = fluid
sub_viewport.add_child(fluid_renderer)
func _create_inside_camera():
inside_camera = Camera2D.new()
inside_camera.name = "Camera2D"
inside_camera.material = water_material
sub_viewport.add_child(inside_camera)
func _ready() -> void:
_create_subviewport_container()
_create_subviewport()
_create_fluid_renderer()
_create_inside_camera()
if fluid:
fluid.debug_draw = false
func _process(_delta: float) -> void:
if camera != null:
inside_camera.offset = camera.offset
inside_camera.zoom = camera.zoom
inside_camera.transform = camera.transform
sub_viewport_container.scale = Vector2(1.0 / camera.zoom.x, 1.0 / camera.zoom.y)
sub_viewport_container.position = camera.global_position
sub_viewport.size = sub_viewport_container.size
if camera.anchor_mode == Camera2D.AnchorMode.ANCHOR_MODE_FIXED_TOP_LEFT:
sub_viewport_container.position -= sub_viewport_container.size / 2
if !camera.ignore_rotation:
sub_viewport_container.rotation = camera.global_rotation
else:
sub_viewport_container.rotation = 0

View File

@@ -1,34 +0,0 @@
[configuration]
entry_symbol = "gdext_rust_init"
compatibility_minimum = 4.3
[libraries]
macos.debug = "bin/libgodot_rapier.macos.framework"
macos.release = "bin/libgodot_rapier.macos.framework"
windows.debug.x86_64 = "bin/libgodot_rapier.windows.x86_64-pc-windows-msvc.dll"
windows.release.x86_64 = "bin/libgodot_rapier.windows.x86_64-pc-windows-msvc.dll"
windows.debug.x86_32 = "bin/libgodot_rapier.windows.i686-pc-windows-msvc.dll"
windows.release.x86_32 = "bin/libgodot_rapier.windows.i686-pc-windows-msvc.dll"
windows.debug.arm64 = "bin/libgodot_rapier.windows.aarch64-pc-windows-msvc.dll"
windows.release.arm64 = "bin/libgodot_rapier.windows.aarch64-pc-windows-msvc.dll"
linux.debug.x86_64 = "bin/libgodot_rapier.linux.x86_64-unknown-linux-gnu.so"
linux.release.x86_64 = "bin/libgodot_rapier.linux.x86_64-unknown-linux-gnu.so"
android.debug.x86_64 = "bin/libgodot_rapier.android.x86_64-linux-android.so"
android.release.x86_64 = "bin/libgodot_rapier.android.x86_64-linux-android.so"
android.debug.x86_32 = "bin/libgodot_rapier.android.i686-linux-android.so"
android.release.x86_32 = "bin/libgodot_rapier.android.i686-linux-android.so"
android.debug.arm64 = "bin/libgodot_rapier.android.aarch64-linux-android.so"
android.release.arm64 = "bin/libgodot_rapier.android.aarch64-linux-android.so"
ios.debug = "bin/libgodot_rapier.ios.framework"
ios.release = "bin/libgodot_rapier.ios.framework"
web.debug.threads.wasm32 = "bin/godot_rapier.wasm"
web.release.threads.wasm32 = "bin/godot_rapier.wasm"
web.debug.wasm32 = "bin/wasm-nothreads/godot_rapier.wasm"
web.release.wasm32 = "bin/wasm-nothreads/godot_rapier.wasm"
[icons]
Fluid2D = "Fluid2D.svg"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,8 +0,0 @@
[plugin]
name="Godot Rapier 2D"
description="A 2D and 3D drop-in replacement for the Godot engine that adds stability and fluids."
author="appsinacup"
version="0.8.8"
flavour="godot-rapier-2d-single-simd-parallel"
script=""

View File

@@ -1,105 +0,0 @@
### Supports [CollisionObject2D], [Joint2D] and [CollisionShape2D].
@icon("res://addons/godot-rapier2d/logo_square_2d.png")
class_name Rapier2DState
extends Node
var state: Dictionary = {}
func _is_physics_object(node: Node) -> bool:
return node is CollisionObject2D or node is Joint2D
func _get_all_physics_nodes(p_node: Node, path: String = "/root/") -> Array[String]:
var results: Array[String] = []
if path == "/root/" && _is_physics_object(p_node):
results.append(path + p_node.name)
path += p_node.name + "/"
for node in p_node.get_children():
if _is_physics_object(node):
results.append(path + node.name)
if node.get_child_count() > 0:
results.append_array(_get_all_physics_nodes(node, path))
return results
## Save a node's physics state
func save_node(rid: RID, save_json: bool):
if save_json:
return JSON.parse_string(RapierPhysicsServer2D.export_json(rid))
return RapierPhysicsServer2D.export_binary(rid)
## Load a node's physics state
func load_node(rid: RID, data: PackedByteArray):
RapierPhysicsServer2D.import_binary(rid, data)
## Save the state of whole world (single space)
func save_state(save_json: bool = false) -> int:
var physics_nodes := _get_all_physics_nodes(get_tree().current_scene)
for node_path in physics_nodes:
var node := get_node(node_path)
var rid: RID
if node is CollisionObject2D:
rid = node.get_rid()
for owner_id in node.get_shape_owners():
for owner_shape_id in node.shape_owner_get_shape_count(owner_id):
var shape_rid = node.shape_owner_get_shape(owner_id, owner_shape_id).get_rid()
state[node_path + "/" + str(owner_id) + "/" + str(owner_shape_id)] = save_node(
shape_rid, save_json
)
if node is Joint2D:
rid = node.get_rid()
state[node_path] = save_node(rid, save_json)
var space_rid = get_viewport().world_2d.space
state["space"] = save_node(space_rid, save_json)
state["id"] = RapierPhysicsServer2D.get_global_id()
return hash(JSON.stringify(state))
## Load the state of whole world (single space)
func load_state() -> int:
var physics_nodes := _get_all_physics_nodes(get_tree().current_scene)
for node_path in physics_nodes:
var node := get_node(node_path)
var rid: RID
if node is CollisionObject2D:
rid = node.get_rid()
for owner_id in node.get_shape_owners():
for owner_shape_id in node.shape_owner_get_shape_count(owner_id):
var shape_rid = node.shape_owner_get_shape(owner_id, owner_shape_id).get_rid()
var shape_state = state[
node_path + "/" + str(owner_id) + "/" + str(owner_shape_id)
]
load_node(shape_rid, JSON.parse_string(shape_state))
if node is Joint2D:
rid = node.get_rid()
var node_state = state[node_path]
load_node(rid, JSON.parse_string(node_state))
var space_rid = get_viewport().world_2d.space
load_node(space_rid, JSON.parse_string(state["space"]))
RapierPhysicsServer2D.set_global_id(int(state["id"]))
return hash(JSON.stringify(state))
## Export the state to file
func export_state(file_name: String = "user://state.json"):
save_state(false)
FileAccess.open(file_name, FileAccess.WRITE).store_string(JSON.stringify(state, " "))
## Import the state from file
func import_state(file_name: String = "user://state.json"):
state = JSON.parse_string(FileAccess.open(file_name, FileAccess.READ).get_as_text())
load_state()
func _notification(what: int) -> void:
if what == NOTIFICATION_ENTER_TREE:
print("enter tree")
if what == NOTIFICATION_EXIT_TREE:
save_state(false)
FileAccess.open("user://save.json", FileAccess.WRITE).store_string(
JSON.stringify(state, " ")
)

View File

@@ -1,22 +0,0 @@
shader_type canvas_item;
uniform float threshold = 0.8;
uniform vec4 water_color: source_color = vec4(0.12,0.24,0.45,0.65);
uniform vec4 test_color: source_color = vec4(1,0,1,1);
uniform float speed = 0.1; // Speed of movement
uniform float amplitude = 0.1; // Amplitude of movement
uniform sampler2D water_texture;
void fragment(){
float displacement = sin(TIME * speed) * amplitude;
vec4 screen_tex = texture(TEXTURE, SCREEN_UV).rgba;
float color_distance = screen_tex.r;
if (color_distance > threshold) {
COLOR = texture(water_texture, SCREEN_UV + displacement).rgba * water_color;
} else {
COLOR = vec4(0.0);
}
}

View File

@@ -1,11 +0,0 @@
[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://cysnk7s2ll173"]
[ext_resource type="Shader" path="res://addons/godot-rapier2d/water_shader.gdshader" id="1_wgm3x"]
[resource]
shader = ExtResource("1_wgm3x")
shader_parameter/threshold = 0.8
shader_parameter/water_color = Color(0.12, 0.24, 0.45, 0.65)
shader_parameter/test_color = Color(1, 0, 1, 1)
shader_parameter/speed = 0.1
shader_parameter/amplitude = 0.1

View 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)

View 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

View File

@@ -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)

View File

@@ -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"

View 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)

View 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")

View 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)

View 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

View 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())

View 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"]

View 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 }

View 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

View 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"]

View 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

View 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()

View 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")

View 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)

View File

@@ -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"]

View 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

View 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

View File

@@ -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)

View File

@@ -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."

View 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

View 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")

View 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)

View 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

View 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")

View 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
View 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()

View 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", "")

View 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

View File

@@ -2,8 +2,8 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://cho3shol3rky2" uid="uid://bei7cw115tks0"
path="res://.godot/imported/Radial2D.svg-4380d9841bddc115105cff2016be7c6a.ctex" path="res://.godot/imported/guide_action.svg-4d1dfb47183d95c4796078798ce2d0ab.ctex"
metadata={ metadata={
"has_editor_variant": true, "has_editor_variant": true,
"vram_texture": false "vram_texture": false
@@ -11,8 +11,8 @@ metadata={
[deps] [deps]
source_file="res://addons/godot-rapier2d/Radial2D.svg" source_file="res://addons/guide/guide_action.svg"
dest_files=["res://.godot/imported/Radial2D.svg-4380d9841bddc115105cff2016be7c6a.ctex"] dest_files=["res://.godot/imported/guide_action.svg-4d1dfb47183d95c4796078798ce2d0ab.ctex"]
[params] [params]
@@ -33,6 +33,6 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=0.5
editor/scale_with_editor_scale=true editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View 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()

View 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

View 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())

View 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

View 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

View 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

View 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

View 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

View 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
View 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()

View 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

View 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

View File

@@ -2,8 +2,8 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://cv17s0qjrqj16" uid="uid://oku7f5t0ox3r"
path="res://.godot/imported/Fluid2D.svg-947567c51a8b586a0e695be7cba2d975.ctex" path="res://.godot/imported/guide_input.svg-d7e8ae255db039e6a02cccc3f844cc0e.ctex"
metadata={ metadata={
"has_editor_variant": true, "has_editor_variant": true,
"vram_texture": false "vram_texture": false
@@ -11,8 +11,8 @@ metadata={
[deps] [deps]
source_file="res://addons/godot-rapier2d/Fluid2D.svg" source_file="res://addons/guide/inputs/guide_input.svg"
dest_files=["res://.godot/imported/Fluid2D.svg-947567c51a8b586a0e695be7cba2d975.ctex"] dest_files=["res://.godot/imported/guide_input.svg-d7e8ae255db039e6a02cccc3f844cc0e.ctex"]
[params] [params]
@@ -33,6 +33,6 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 detect_3d/compress_to=1
svg/scale=1.0 svg/scale=0.5
editor/scale_with_editor_scale=true editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false editor/convert_colors_with_editor_theme=false

View 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

View 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
}
]

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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()

View 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

View 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

View 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

View 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 ""

View 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

View 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

View 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."

View 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."

Some files were not shown because too many files have changed in this diff Show More