├── .gitattributes
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets
├── icons
│ ├── box_switcher_2d.svg.import
│ ├── buffered_input_advancer.svg
│ ├── buffered_input_advancer.svg.import
│ ├── combat_tree.svg.import
│ ├── controller.svg
│ ├── controller.svg.import
│ ├── fighter_body_2d.svg
│ ├── fighter_body_2d.svg.import
│ ├── hit_attribute.svg
│ ├── hit_attribute.svg.import
│ ├── hit_attributes.svg.import
│ ├── hit_box_2d.svg.import
│ ├── hit_state_2d.svg
│ ├── hit_state_2d.svg.import
│ ├── hit_state_3d.svg
│ ├── hit_state_3d.svg.import
│ ├── hit_state_controller_2d.svg.import
│ ├── hit_state_manager_2d.svg
│ ├── hit_state_manager_2d.svg.import
│ ├── hit_state_manager_3d.svg
│ ├── hit_state_manager_3d.svg.import
│ ├── hitbox_2d.svg
│ ├── hitbox_2d.svg.import
│ ├── hitbox_3d.svg
│ ├── hitbox_3d.svg.import
│ ├── hurt_box_2d.svg
│ ├── hurt_box_2d.svg.import
│ ├── input_detector.svg
│ ├── input_detector.svg.import
│ ├── push_box_2d.svg
│ ├── push_box_2d.svg.import
│ ├── remove.svg
│ ├── remove.svg.import
│ ├── state_machine.svg
│ ├── state_machine.svg.import
│ ├── triple_bar.svg
│ ├── triple_bar.svg.import
│ ├── warning.svg
│ └── warning.svg.import
└── images
│ ├── .gdignore
│ ├── demo.gif
│ ├── fray_banner.gif
│ ├── fray_banner.png
│ ├── fray_logo.svg
│ ├── fray_thumbnail.png
│ ├── hit_state_inspector.png
│ ├── hitbox_tree.png
│ └── icon.png
├── docs
├── .gdignore
├── .vitepress
│ └── config.mts
├── hit
│ ├── creating-hitboxes.md
│ ├── managing-hitboxes.md
│ └── overview.md
├── index.md
├── input
│ ├── detecting-input-sequences.md
│ ├── detecting-inputs.md
│ ├── overview.md
│ └── registering-inputs.md
├── introduction
│ ├── installation.md
│ └── what-is-fray.md
├── package-lock.json
├── package.json
├── public
│ └── assets
│ │ ├── guides
│ │ ├── add-input-advancer-to-scene.png
│ │ ├── add-state-machine-to-scene.png
│ │ ├── building-state-machine-root.webp
│ │ ├── input-advancer-in-scene.png
│ │ ├── inspector-state-machine.png
│ │ └── state-machine-in-scene.png
│ │ └── icons
│ │ ├── fray-logo-dark.svg
│ │ └── fray-logo-light.svg
└── state-management
│ ├── building-a-state-machine.md
│ ├── controlling-state-transitions.md
│ ├── overview.md
│ ├── providing-data-to-states.md
│ ├── using-global-transitions.md
│ └── using-input-transitions.md
├── lib
├── data_structures
│ ├── circular_buffer.gd
│ ├── linked_list.gd
│ └── reversable_dictionary.gd
└── helpers
│ ├── child_change_detector.gd
│ ├── pseudo_interface.gd
│ └── utils
│ ├── signal_utils.gd
│ └── sorting.gd
├── plugin.cfg
├── plugin.gd
└── src
├── fray.gd
├── hit
├── 2d
│ ├── hit_state_2d.gd
│ ├── hit_state_manager_2d.gd
│ └── hitbox_2d.gd
├── 3d
│ ├── hit_state_3d.gd
│ ├── hit_state_manager_3d.gd
│ └── hitbox_3d.gd
└── hitbox_attribute.gd
├── input
├── autoloads
│ ├── fray_input.gd
│ └── fray_input_map.gd
├── controller.gd
├── device
│ ├── binds
│ │ ├── input_bind.gd
│ │ ├── input_bind_action.gd
│ │ ├── input_bind_fray_action.gd
│ │ ├── input_bind_joy_axis.gd
│ │ ├── input_bind_joy_button.gd
│ │ ├── input_bind_key.gd
│ │ ├── input_bind_mouse_button.gd
│ │ └── input_bind_simple.gd
│ ├── composites
│ │ ├── combination_input.gd
│ │ ├── composite_input.gd
│ │ ├── conditional_input.gd
│ │ ├── group_input.gd
│ │ └── simple_input.gd
│ ├── device_state.gd
│ ├── input_state.gd
│ └── virtual_device.gd
├── events
│ ├── fray_input_event.gd
│ ├── fray_input_event_bind.gd
│ └── fray_input_event_composite.gd
├── input_buffer.gd
├── sequence
│ ├── input_requirement.gd
│ ├── sequence_branch.gd
│ └── sequence_tree.gd
└── sequence_matcher.gd
└── state_mgmt
├── component
├── anim_tracker
│ ├── animator_tracker.gd
│ ├── animator_tracker_animated_sprite_2d.gd
│ ├── animator_tracker_animated_sprite_3d.gd
│ ├── animator_tracker_animation_player.gd
│ ├── animator_tracker_animation_tree.gd
│ └── animator_tracker_tween.gd
├── animation_observer.gd
└── buffered_input_advancer.gd
├── state
├── a_star_graph.gd
├── compound_state.gd
└── state.gd
├── state_machine.gd
└── transition
├── input_transition.gd
├── input_transition_press.gd
├── input_transition_sequence.gd
└── state_machine_transition.gd
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Normalize line endings for all files that Git considers text files.
2 | * text=auto eol=lf
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Godot 4+ specific ignores
2 | .godot/
3 |
4 | # Godot-specific ignores
5 | .import/
6 | export.cfg
7 | export_presets.cfg
8 |
9 | # Imported translations (automatically generated from CSV files)
10 | *.translation
11 |
12 | # Mono-specific ignores
13 | .mono/
14 | data_*/
15 | mono_crash.*.json
16 |
17 | # Doc-specific ignores
18 | docs/.vitepress/cache/
19 | docs/node_modules/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Pyxus
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Fray
2 |
3 |
4 |
5 |
6 |
7 |   
8 |
9 | ## 📖 About
10 |
11 | Fray is a modular Godot 4 addon designed to aid in the development of action-oriented games. It offers solutions for combatant state management, complex input detection, input buffering, and hitbox organization. If your project requires any of these functionalities you may benefit from using Fray.
12 |
13 | ## ⚠️ IMPORTANT
14 |
15 | **Fray is currently in an alpha state.**
16 |
17 | What does this mean?
18 |
19 | - It has not been tested rigorously enough for me to be comfortable recommending it for use in a serious project.
20 |
21 | - The documentation is incomplete and there is a lack of good examples.
22 |
23 | - Lastly, it is still susceptible to refactors, meaning the API is subject to change.
24 |
25 | That being said, a significant portion of Fray is functional, with any remaining bugs likely being simple oversights rather than major design flaws. If these issues do not concern you, and/or you are interested in testing the framework, please feel free to explore!
26 |
27 |
28 |
29 | ## ✨ Core Features
30 |
31 | ### Resource-Based Hierarchical State Machine
32 |
33 | - Build state machines declaratively in code using the included builder class.
34 |
35 | - Control state transitions using callable transition prerequisites and advance conditions.
36 |
37 | - Extend states and transitions to further control state flow and/or encapsulate game behavior within different states.
38 |
39 |
40 | [comment]:
41 |
42 | ### Composite Input Detection
43 |
44 | - Declaratively describe the many composite inputs featured in action / fighting games ([directional inputs](https://mugen.fandom.com/wiki/Command_input#Directional_inputs), [motion inputs](https://mugen.fandom.com/wiki/Command_input#Motion_input), [charged inputs](https://clips.twitch.tv/FuriousObservantOrcaGrammarKing-c1wo4zhroMVZ9I7y), and [sequence inputs](https://mugen.fandom.com/wiki/Command_input#Sequence_inputs)) using component based approach.
45 |
46 | - Check defined inputs anywhere using included input singleton.
47 |
48 | [comment]:
49 |
50 |
51 | ### Hitbox management
52 |
53 | - Define hitboxes using template class with extendable attributes resource.
54 |
55 | - Organize hitboxes using hit states and hit state managers.
56 |
57 | - Key active hitboxes in animation player using a single property for easier timeline management.
58 |
59 | [comment]:
60 |
61 | ## 📚 Getting Started
62 |
63 | Fray comes with comprehensive documentation integrated with Godot 4's documentation comments. This means you can access explanations for classes and functions directly within the Godot editor.
64 |
65 | For additional guides and resources, check out the [official Fray wiki](https://fray.pyxus.dev).
66 |
67 | ## 📦 Installation
68 |
69 | ### GitHub Release (Recommended, Stable)
70 |
71 | Coming soon...
72 |
73 | ### Asset Library (Recommended, Stable)
74 |
75 | Coming soon...
76 |
77 | ### GitHub Branch (Latest, Unstable, Godot 4.2+)
78 |
79 | 1. Downloaded the [latest main branch](https://github.com/Pyxus/fray/archive/refs/heads/main.zip)
80 | 2. Extract the zip file and move its contents to `addons/fray`.
81 | 3. Enable the addon inside `Project/Project Settings/Plugins`
82 |
83 | ---
84 |
85 | If you would like to know more about installing plugins see the [Official Godot Docs](https://docs.godotengine.org/en/stable/tutorials/plugins/editor/installing_plugins.html).
86 |
87 | ## 📃 Credits
88 |
89 | - Controller Button Images :
90 |
--------------------------------------------------------------------------------
/assets/icons/box_switcher_2d.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="StreamTexture"
5 | path="res://.import/box_switcher_2d.svg-7d0da11ebca61da1cfedd0fd0038cef5.stex"
6 | metadata={
7 | "vram_texture": false
8 | }
9 |
10 | [deps]
11 |
12 | source_file="res://addons/fray/assets/icons/box_switcher_2d.svg"
13 | dest_files=[ "res://.import/box_switcher_2d.svg-7d0da11ebca61da1cfedd0fd0038cef5.stex" ]
14 |
15 | [params]
16 |
17 | compress/mode=0
18 | compress/lossy_quality=0.7
19 | compress/hdr_mode=0
20 | compress/bptc_ldr=0
21 | compress/normal_map=0
22 | flags/repeat=0
23 | flags/filter=true
24 | flags/mipmaps=false
25 | flags/anisotropic=false
26 | flags/srgb=2
27 | process/fix_alpha_border=true
28 | process/premult_alpha=false
29 | process/HDR_as_SRGB=false
30 | process/invert_color=false
31 | process/normal_map_invert_y=false
32 | stream=false
33 | size_limit=0
34 | detect_3d=true
35 | svg/scale=1.0
36 |
--------------------------------------------------------------------------------
/assets/icons/buffered_input_advancer.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
37 |
--------------------------------------------------------------------------------
/assets/icons/buffered_input_advancer.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://cvbxhtsumglxk"
6 | path="res://.godot/imported/buffered_input_advancer.svg-9564a93a7978416f52254322f62b2ffa.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/fray/assets/icons/buffered_input_advancer.svg"
14 | dest_files=["res://.godot/imported/buffered_input_advancer.svg-9564a93a7978416f52254322f62b2ffa.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/assets/icons/combat_tree.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="StreamTexture"
5 | path="res://.import/combat_tree.svg-3a6885dd643c603283e1d8b1c261441e.stex"
6 | metadata={
7 | "vram_texture": false
8 | }
9 |
10 | [deps]
11 |
12 | source_file="res://addons/fray/assets/icons/combat_tree.svg"
13 | dest_files=[ "res://.import/combat_tree.svg-3a6885dd643c603283e1d8b1c261441e.stex" ]
14 |
15 | [params]
16 |
17 | compress/mode=0
18 | compress/lossy_quality=0.7
19 | compress/hdr_mode=0
20 | compress/bptc_ldr=0
21 | compress/normal_map=0
22 | flags/repeat=0
23 | flags/filter=true
24 | flags/mipmaps=false
25 | flags/anisotropic=false
26 | flags/srgb=2
27 | process/fix_alpha_border=true
28 | process/premult_alpha=false
29 | process/HDR_as_SRGB=false
30 | process/invert_color=false
31 | process/normal_map_invert_y=false
32 | stream=false
33 | size_limit=0
34 | detect_3d=true
35 | svg/scale=1.0
36 |
--------------------------------------------------------------------------------
/assets/icons/controller.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/assets/icons/controller.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://bpbw01ih06cvh"
6 | path="res://.godot/imported/controller.svg-a926362aa5e951fa4cb208d5484b0c2c.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/fray/assets/icons/controller.svg"
14 | dest_files=["res://.godot/imported/controller.svg-a926362aa5e951fa4cb208d5484b0c2c.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/assets/icons/fighter_body_2d.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
--------------------------------------------------------------------------------
/assets/icons/fighter_body_2d.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://bkhr4erdvlx0j"
6 | path="res://.godot/imported/fighter_body_2d.svg-aadf54b0424eebd2de0d56b0425a83f4.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/fray/assets/icons/fighter_body_2d.svg"
14 | dest_files=["res://.godot/imported/fighter_body_2d.svg-aadf54b0424eebd2de0d56b0425a83f4.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/assets/icons/hit_attribute.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/assets/icons/hit_attribute.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://c3k1f422k67x6"
6 | path="res://.godot/imported/hit_attribute.svg-2659a74ad63aeb781c97134b160335e4.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/fray/assets/icons/hit_attribute.svg"
14 | dest_files=["res://.godot/imported/hit_attribute.svg-2659a74ad63aeb781c97134b160335e4.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/assets/icons/hit_attributes.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://dnedynbm3x22h"
6 | path="res://.godot/imported/hit_attribute.svg-40500fac93b7c74c4cd6715b596c7f3e.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/fray/assets/icons/hit_attribute.svg"
14 | dest_files=["res://.godot/imported/hit_attribute.svg-40500fac93b7c74c4cd6715b596c7f3e.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/lossy_quality=0.7
20 | compress/hdr_compression=1
21 | compress/bptc_ldr=0
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/assets/icons/hit_box_2d.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="StreamTexture"
5 | path="res://.import/hit_box_2d.svg-0b1ba9ffc876a36448ac22548fb0a8a1.stex"
6 | metadata={
7 | "vram_texture": false
8 | }
9 |
10 | [deps]
11 |
12 | source_file="res://addons/fray/assets/icons/hit_box_2d.svg"
13 | dest_files=[ "res://.import/hit_box_2d.svg-0b1ba9ffc876a36448ac22548fb0a8a1.stex" ]
14 |
15 | [params]
16 |
17 | compress/mode=0
18 | compress/lossy_quality=0.7
19 | compress/hdr_mode=0
20 | compress/bptc_ldr=0
21 | compress/normal_map=0
22 | flags/repeat=0
23 | flags/filter=true
24 | flags/mipmaps=false
25 | flags/anisotropic=false
26 | flags/srgb=2
27 | process/fix_alpha_border=true
28 | process/premult_alpha=false
29 | process/HDR_as_SRGB=false
30 | process/invert_color=false
31 | process/normal_map_invert_y=false
32 | stream=false
33 | size_limit=0
34 | detect_3d=true
35 | svg/scale=1.0
36 |
--------------------------------------------------------------------------------
/assets/icons/hit_state_2d.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
31 |
--------------------------------------------------------------------------------
/assets/icons/hit_state_2d.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://bs76g103e6drq"
6 | path="res://.godot/imported/hit_state_2d.svg-7f09395ff53b6d8f454cdabcec7aebf2.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/fray/assets/icons/hit_state_2d.svg"
14 | dest_files=["res://.godot/imported/hit_state_2d.svg-7f09395ff53b6d8f454cdabcec7aebf2.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/assets/icons/hit_state_3d.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
31 |
--------------------------------------------------------------------------------
/assets/icons/hit_state_3d.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://yli0twusqnuj"
6 | path="res://.godot/imported/hit_state_3d.svg-eb0fee611169bb21de2b8fadf522f736.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/fray/assets/icons/hit_state_3d.svg"
14 | dest_files=["res://.godot/imported/hit_state_3d.svg-eb0fee611169bb21de2b8fadf522f736.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/assets/icons/hit_state_controller_2d.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="StreamTexture"
5 | path="res://.import/hit_state_controller_2d.svg-17a0bfe3ca7d6971e91e009a4a7c3009.stex"
6 | metadata={
7 | "vram_texture": false
8 | }
9 |
10 | [deps]
11 |
12 | source_file="res://addons/fray/assets/icons/hit_state_controller_2d.svg"
13 | dest_files=[ "res://.import/hit_state_controller_2d.svg-17a0bfe3ca7d6971e91e009a4a7c3009.stex" ]
14 |
15 | [params]
16 |
17 | compress/mode=0
18 | compress/lossy_quality=0.7
19 | compress/hdr_mode=0
20 | compress/bptc_ldr=0
21 | compress/normal_map=0
22 | flags/repeat=0
23 | flags/filter=true
24 | flags/mipmaps=false
25 | flags/anisotropic=false
26 | flags/srgb=2
27 | process/fix_alpha_border=true
28 | process/premult_alpha=false
29 | process/HDR_as_SRGB=false
30 | process/invert_color=false
31 | process/normal_map_invert_y=false
32 | stream=false
33 | size_limit=0
34 | detect_3d=true
35 | svg/scale=1.0
36 |
--------------------------------------------------------------------------------
/assets/icons/hit_state_manager_2d.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
--------------------------------------------------------------------------------
/assets/icons/hit_state_manager_2d.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://b6polbnuoy8dd"
6 | path="res://.godot/imported/hit_state_manager_2d.svg-2d2b6d7e7f9aed9263abd3c2cf988735.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/fray/assets/icons/hit_state_manager_2d.svg"
14 | dest_files=["res://.godot/imported/hit_state_manager_2d.svg-2d2b6d7e7f9aed9263abd3c2cf988735.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/assets/icons/hit_state_manager_3d.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
--------------------------------------------------------------------------------
/assets/icons/hit_state_manager_3d.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://bpa5q2f8ku053"
6 | path="res://.godot/imported/hit_state_manager_3d.svg-1bc0ba6aeb12554d6b179ac0569b28c7.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/fray/assets/icons/hit_state_manager_3d.svg"
14 | dest_files=["res://.godot/imported/hit_state_manager_3d.svg-1bc0ba6aeb12554d6b179ac0569b28c7.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/assets/icons/hitbox_2d.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
50 |
--------------------------------------------------------------------------------
/assets/icons/hitbox_2d.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://dx1avye0lsyo8"
6 | path="res://.godot/imported/hitbox_2d.svg-0287744ccbb25ba591ffe1494c93a0de.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/fray/assets/icons/hitbox_2d.svg"
14 | dest_files=["res://.godot/imported/hitbox_2d.svg-0287744ccbb25ba591ffe1494c93a0de.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/assets/icons/hitbox_3d.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
50 |
--------------------------------------------------------------------------------
/assets/icons/hitbox_3d.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://kttvnq85qchg"
6 | path="res://.godot/imported/hitbox_3d.svg-c8f35a7c82cf7bf00d5f34823fcf4d62.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/fray/assets/icons/hitbox_3d.svg"
14 | dest_files=["res://.godot/imported/hitbox_3d.svg-c8f35a7c82cf7bf00d5f34823fcf4d62.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/assets/icons/hurt_box_2d.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://cclk6lsklu7fw"
6 | path="res://.godot/imported/hurt_box_2d.svg-a987871b2e0be4c42a8ea61d4f510414.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/fray/assets/icons/hurt_box_2d.svg"
14 | dest_files=["res://.godot/imported/hurt_box_2d.svg-a987871b2e0be4c42a8ea61d4f510414.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/assets/icons/input_detector.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
24 |
--------------------------------------------------------------------------------
/assets/icons/input_detector.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://1vh8avwbaij2"
6 | path="res://.godot/imported/input_detector.svg-e5c626a063e06f4a6049ae330416c87c.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/fray/assets/icons/input_detector.svg"
14 | dest_files=["res://.godot/imported/input_detector.svg-e5c626a063e06f4a6049ae330416c87c.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/assets/icons/push_box_2d.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/assets/icons/push_box_2d.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://b15w0f2tie8k3"
6 | path="res://.godot/imported/push_box_2d.svg-54fc0cbc7615c134fbff2cf95054dc05.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/fray/assets/icons/push_box_2d.svg"
14 | dest_files=["res://.godot/imported/push_box_2d.svg-54fc0cbc7615c134fbff2cf95054dc05.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/assets/icons/remove.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/icons/remove.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://05b0pf1ew6sa"
6 | path="res://.godot/imported/remove.svg-2da7626be0fb40169774c6b36a4003ed.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/fray/assets/icons/remove.svg"
14 | dest_files=["res://.godot/imported/remove.svg-2da7626be0fb40169774c6b36a4003ed.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/assets/icons/state_machine.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
32 |
--------------------------------------------------------------------------------
/assets/icons/state_machine.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://be0dfn4546vvm"
6 | path="res://.godot/imported/state_machine.svg-729836a6b5958565b9e3dc564c773e44.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/fray/assets/icons/state_machine.svg"
14 | dest_files=["res://.godot/imported/state_machine.svg-729836a6b5958565b9e3dc564c773e44.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/assets/icons/triple_bar.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/icons/triple_bar.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://buufiynnq1pcx"
6 | path="res://.godot/imported/triple_bar.svg-7fda2c6e0f53fece856a115f49eacff4.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/fray/assets/icons/triple_bar.svg"
14 | dest_files=["res://.godot/imported/triple_bar.svg-7fda2c6e0f53fece856a115f49eacff4.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/assets/icons/warning.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/icons/warning.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://d4lasdylvk0ss"
6 | path="res://.godot/imported/warning.svg-bd39d80dac5c0519712efd94a6620a81.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/fray/assets/icons/warning.svg"
14 | dest_files=["res://.godot/imported/warning.svg-bd39d80dac5c0519712efd94a6620a81.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/assets/images/.gdignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyxus/fray/ab186b335f1fc42837092d16971dca8f4e580b18/assets/images/.gdignore
--------------------------------------------------------------------------------
/assets/images/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyxus/fray/ab186b335f1fc42837092d16971dca8f4e580b18/assets/images/demo.gif
--------------------------------------------------------------------------------
/assets/images/fray_banner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyxus/fray/ab186b335f1fc42837092d16971dca8f4e580b18/assets/images/fray_banner.gif
--------------------------------------------------------------------------------
/assets/images/fray_banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyxus/fray/ab186b335f1fc42837092d16971dca8f4e580b18/assets/images/fray_banner.png
--------------------------------------------------------------------------------
/assets/images/fray_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
39 |
--------------------------------------------------------------------------------
/assets/images/fray_thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyxus/fray/ab186b335f1fc42837092d16971dca8f4e580b18/assets/images/fray_thumbnail.png
--------------------------------------------------------------------------------
/assets/images/hit_state_inspector.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyxus/fray/ab186b335f1fc42837092d16971dca8f4e580b18/assets/images/hit_state_inspector.png
--------------------------------------------------------------------------------
/assets/images/hitbox_tree.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyxus/fray/ab186b335f1fc42837092d16971dca8f4e580b18/assets/images/hitbox_tree.png
--------------------------------------------------------------------------------
/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyxus/fray/ab186b335f1fc42837092d16971dca8f4e580b18/assets/images/icon.png
--------------------------------------------------------------------------------
/docs/.gdignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyxus/fray/ab186b335f1fc42837092d16971dca8f4e580b18/docs/.gdignore
--------------------------------------------------------------------------------
/docs/.vitepress/config.mts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitepress";
2 |
3 | // https://vitepress.dev/reference/site-config
4 | export default defineConfig({
5 | title: "Fray Documentation",
6 | description: "Modular combat framework for Godot 4",
7 | themeConfig: {
8 | // https://vitepress.dev/reference/default-theme-config
9 | logo: {
10 | dark: "/assets/icons/fray-logo-dark.svg",
11 | light: "/assets/icons/fray-logo-light.svg"
12 | },
13 | search: {
14 | provider: "local",
15 | },
16 | nav: [{ text: "Docs", link: "/introduction/what-is-fray" }],
17 | editLink: {
18 | pattern: "https://github.com/Pyxus/fray/tree/main/docs/:path",
19 | text: "Edit this page on GitHub",
20 | },
21 | lastUpdated: {
22 | text: "Last updated",
23 | formatOptions: {
24 | dateStyle: "short",
25 | timeStyle: "short",
26 | },
27 | },
28 | sidebar: [
29 | {
30 | text: "Introduction",
31 | collapsed: false,
32 | items: [
33 | { text: "What is fray?", link: "/introduction/what-is-fray" },
34 | { text: "Installation", link: "/introduction/installation" },
35 | ],
36 | },
37 | {
38 | text: "State Management Module",
39 | collapsed: false,
40 | link: "/state-management/overview",
41 | items: [
42 | {
43 | text: "Building A State Machine",
44 | link: "/state-management/building-a-state-machine",
45 | },
46 | {
47 | text: "Providing Data To States",
48 | link: "/state-management/providing-data-to-states",
49 | },
50 | {
51 | text: "Controlling State Transitions",
52 | link: "/state-management/controlling-state-transitions",
53 | },
54 | {
55 | text: "Using Input Transitions",
56 | link: "/state-management/using-input-transitions",
57 | },
58 | {
59 | text: "Using Global Transitions",
60 | link: "/state-management/using-global-transitions",
61 | },
62 | ],
63 | },
64 | {
65 | text: "Input Module",
66 | collapsed: false,
67 | link: "/input/overview",
68 | items: [
69 | {
70 | text: "Registering Inputs",
71 | link: "/input/registering-inputs",
72 | },
73 | {
74 | text: "Detecting Inputs",
75 | link: "/input/detecting-inputs",
76 | },
77 | {
78 | text: "Detecting Input Sequences",
79 | link: "/input/detecting-input-sequences",
80 | },
81 | ],
82 | },
83 | {
84 | text: "Hit Module",
85 | collapsed: false,
86 | link: "/hit/overview",
87 | items: [
88 | {
89 | text: "Creating Hitboxes",
90 | link: "/hit/creating-hitboxes",
91 | },
92 | {
93 | text: "Managing Hitboxes",
94 | link: "/hit/managing-hitboxes",
95 | },
96 | ],
97 | },
98 | {
99 | text: "Mixed Module Examples",
100 | collapsed: false,
101 | },
102 | ],
103 |
104 | socialLinks: [
105 | { icon: "github", link: "https://github.com/Pyxus/fray" },
106 | { icon: "twitter", link: "https://twitter.com/pyxus" },
107 | ],
108 | },
109 | });
110 |
--------------------------------------------------------------------------------
/docs/hit/creating-hitboxes.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | outline: [2, 6]
4 | ---
5 |
6 | # Creating Hitboxes
7 |
--------------------------------------------------------------------------------
/docs/hit/managing-hitboxes.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | outline: [2, 6]
4 | ---
5 |
6 | # Managing Hitboxes
7 |
--------------------------------------------------------------------------------
/docs/hit/overview.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | outline: [2, 6]
4 | ---
5 |
6 | # Hit Module
7 |
8 | ## Purpose Of Module
9 |
10 | While Godot natively provides the necessary tools for hit detection through `Area` nodes, the hitbox configurations in fighting games frequently change. For example, a character's hitboxes when standing may differ from the configuration when crouching, attacking, or getting hit. Hitboxes can also possess various properties and interactions, such as hyper armor, which requires being hit a specific number of times before a character is interrupted, unless the attack is a grab. Managing multiple hitboxes with varying properties can be a tedious task. Therefore, the hit module is designed to simplify this process.
11 |
12 | ## What Is A Hitbox
13 |
14 | In the context of this documentation, the term 'hitbox' refers to all forms of overlap detections, regardless of their purpose within the game. This includes both hitboxes that deal damage (often referred to as attack boxes) and hitboxes that detect damage (sometimes referred to as hurtboxes). It's important to note that Fray does not provide pre-defined implementations for attack or hurt boxes. However, Fray offers all that you need to define and manage your own hitboxes according to your game's requirements.
15 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: "Fray"
7 | text: "Documentation"
8 | tagline: 🐱👤 A modular combat framework for Godot 4.2+
9 | image:
10 | light: /assets/icons/fray-logo-light.svg
11 | dark: /assets/icons/fray-logo-dark.svg
12 | actions:
13 | - theme: brand
14 | text: Get Started
15 | link: /introduction/what-is-fray
16 | - theme: alt
17 | text: View on GitHub
18 | link: https://github.com/Pyxus/fray
19 |
20 | features:
21 | - title: State Management
22 | details: Flexible hierarchical state machine useful for combatant and general state management.
23 | - title: Complex Input Detection
24 | details: Easily detect the complex inputs featured in many fighting games such as directional inputs, motion inputs, charged inputs, and sequence inputs.
25 | - title: Hitbox Organization
26 | details: Organize hitboxes into discrete hit states which can be keyed in the animation player.
27 | ---
28 |
--------------------------------------------------------------------------------
/docs/input/detecting-input-sequences.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | outline: [2, 6]
4 | ---
5 |
6 | # Detecting Input Sequences
7 |
8 | ## Input Sequence Matching
9 |
10 | Fray provides a sequence matcher that can be used to detect input sequences. To use this feature, you first need to create a `FraySequenceTree` object, which contains a mapping of sequence names to branch objects. Each sequence name can be associated with multiple `FraySequenceBranch` objects, which allow for alternative inputs. Alternative inputs are useful for creating leniency in a sequence by adding multiple matches for a given sequence name.
11 |
12 | Example Usage:
13 |
14 | ```gdscript
15 | var sequence_tree := FraySequenceTree.new()
16 | var sequence_matcher := FraySequenceMatcher.new()
17 |
18 | func _ready() -> void:
19 | # The following sequence describes the input for Ryu's famous 'Hadouken' attack.
20 | sequence_tree.add("hadouken", FraySequenceBranch.builder()
21 | .first("down").then("down_right").then("right").then("attack")
22 | .build()
23 | )
24 |
25 | # It can be frustating as a player for an attack to not be performed because of overly strict inputs.
26 | # This is an alternative input for the 'Hadoken' which can match even if the 'down_right' is skipped.
27 | # This supports leniency by catching the case where a player accidently releases 'down' before pressing 'right'.
28 | sequence_tree.add("hadouken", FraySequenceBranch.builder()
29 | .first("down").then("right").then("attack")
30 | .build()
31 | )
32 |
33 | # This sequence describes Guile's Sonic Boom attack.
34 | # This type of sequence is known as a "charge input" since it requires 'left' to be held for a certain amount of time, 200ms in this example, before the rest of the sequence is performed.
35 | sequence_tree.add("sonic_boom", FraySequenceBranch.builder()
36 | .first("left", 200).then("right").then("attack")
37 | .build()
38 | )
39 |
40 | sequence_matcher.initialize(sequence_tree)
41 | ```
42 |
43 | In this example, I've named the sequences after the moves for the sake of clarity. However, while sequences generally stay the same, moves and their names are subject to change. For this reason it's recommend to choose names which describe the sequences rather than the move. A recommended naming convention is the Numpad Notation used in fighting games, which provides a clear and concise way to describe inputs using numbers that correspond to directions on a numeric keypad. You can find more information about Numpad Notation at https://www.dustloop.com/w/Notation.
44 |
45 | ## Understanding Negative Edge
46 |
47 | In some fighting games, there is a special input behavior known as negative edge. Typically, an input sequence is considered valid only when every input is pressed in succession. However, for sequences that support negative edge, the last input in the sequence can be triggered by either a input press or a input release. This means that you can hold the last input in the sequence down, enter the rest of the sequence, and then release it to complete the sequence and trigger the attack. Fray supports this feature, and you can enable it by calling the `enable_negative_edge()` method when building your `FraySequenceBranch` objects.
48 |
49 | Example Usage:
50 |
51 | ```gdscript
52 | # The following sequence describes the input for Ryu's famous 'Hadouken' attack.
53 | sequence_tree.add("hadouken", FraySequenceBranch.builder()
54 | .first("down").then("down_right").then("right").then("attack")
55 | .enable_negative_edge()
56 | .build()
57 | )
58 | ```
59 |
60 | ## Using Sequence Matcher
61 |
62 | To perform the match procedure, the sequence matcher uses `FrayInputEvent`s. You can feed events to the matcher using the `FrayInput` singleton's `input_detected` signal. If a matching sequence is found, the matcher will emit a `match_found` signal.
63 |
64 | Example Usage:
65 |
66 | ```gdscript
67 | var sequence_matcher := SequenceMatcher.new()
68 |
69 | func _ready() -> void:
70 | FrayInput.input_detected.connect(_on_FrayInput_input_detected)
71 | sequence_matcher.match_found(_on_SequenceMatcher_match_found)
72 |
73 | func _on_FrayInput_input_detected(input_event: Fray.Input.FrayInputEvent) -> void:
74 | sequence_matcher.read(input_event)
75 |
76 | func _on_SequenceMatcher_match_found(sequence_name: String) -> void:
77 | do_something_with_sequence(sequence_name)
78 |
79 | ```
80 |
--------------------------------------------------------------------------------
/docs/input/detecting-inputs.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | outline: [2, 6]
4 | ---
5 |
6 | # Detecting Inputs
7 |
8 | `FrayInput` is a singleton similar to Godot's `Input` singleton. After configuring the input map, this manager can be used to check if binds and composites are pressed using their assigned names. While inputs can be checked per-device, the default behavior is to use device 0, which typically corresponds to the keyboard/mouse and the 'player1' controller. The input manager also contains an input_detected signal, which can be used to detect inputs in a similar manner to Godot's built-in `_input()` virtual method.
9 |
10 | ```gdscript
11 | FrayInput.is_pressed("input_name")
12 | FrayInput.is_just_pressed("input_name")
13 | FrayInput.is_just_released("input_name")
14 | FrayInput.get_axis("negative_input_name", "positive_input_name")
15 | FrayInput.get_strength("input_name")
16 |
17 | ...
18 |
19 | func _ready() -> void:
20 | FrayInput.input_detected.connect(_on_FrayInput_input_dected)
21 |
22 | func _on_FrayInput_input_dected(event: FrayInputEvent) -> void:
23 | if event.input == "input_name" and event.is_pressed():
24 | do_something()
25 |
26 | ```
27 |
--------------------------------------------------------------------------------
/docs/input/overview.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | outline: [2, 6]
4 | ---
5 |
6 | # Input Module
7 |
8 | ## Purpose of the Module
9 |
10 | Action games, particularly fighting games, often associate combatant actions with various input combiatnions. Fray provides its own input singleton, offering a component-based approach to describing composite inputs. Additionally, Fray provides a sequence matcher capable of detecting input sequences.
11 |
12 | ## What Is a Composite Input?
13 |
14 | A composite input is a combination of two or more atomic inputs. Examples of composite inputs commonly found in fighting games include [directional inputs](https://mugen.fandom.com/wiki/Command_input#Directional_inputs), [motion inputs](https://mugen.fandom.com/wiki/Command_input#Motion_input), [charged inputs](https://clips.twitch.tv/FuriousObservantOrcaGrammarKing-c1wo4zhroMVZ9I7y), and [sequence inputs](https://mugen.fandom.com/wiki/Command_input#Sequence_inputs).
15 |
--------------------------------------------------------------------------------
/docs/introduction/installation.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | outline: [2, 6]
4 | ---
5 |
6 | # Installation
7 |
8 | ::: warning
9 | **Fray is currently in an alpha state.**
10 |
11 | What does this mean?
12 |
13 | - It has not been tested rigorously enough for me to be comfortable recommending it for use in a serious project.
14 |
15 | - The documentation is incomplete and there is a lack of good examples.
16 |
17 | - Lastly, it is still susceptible to refactors, meaning the API is subject to change.
18 |
19 | That being said, a significant portion of Fray is functional, with any remaining bugs likely being simple oversights rather than major design flaws. If these issues do not concern you, and/or you are interested in testing the framework, please feel free to explore!
20 | :::
21 |
22 |
23 | ## GitHub Release (Recommended, Stable)
24 |
25 | Coming soon...
26 |
27 | ## Asset Library (Recommended, Stable)
28 |
29 | Coming soon...
30 |
31 | ## GitHub Branch (Latest, Unstable, Godot 4.2+)
32 |
33 | 1. Downloaded the [latest main branch](https://github.com/Pyxus/fray/archive/refs/heads/main.zip)
34 | 2. Extract the zip file and move its contents to `addons/fray`.
35 | 3. Enable the addon inside `Project/Project Settings/Plugins`
36 |
37 | ---
38 |
39 | If you would like to know more about installing plugins see the [Official Godot Docs](https://docs.godotengine.org/en/stable/tutorials/plugins/editor/installing_plugins.html).
40 |
--------------------------------------------------------------------------------
/docs/introduction/what-is-fray.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | outline: [2, 6]
4 | ---
5 |
6 | # What is Fray?
7 |
8 | `Fray` is a modular Godot 4 addon designed to aid in the development of action-oriented games. It offers solutions for combatant state management, complex input detection, input buffering, and hitbox organization. If your project requires any of these functionalities you may benefit from using Fray.
9 |
10 | ## Modular Design
11 |
12 | Fray is structured into three modules: State Management, Input, and Hit. These modules operate independently and communicate solely through strings, offering the flexibility to replace Fray's solutions with your own and using only what you need.
13 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "vitepress": "^1.0.0-rc.40"
4 | },
5 | "scripts": {
6 | "docs:dev": "vitepress dev",
7 | "docs:build": "vitepress build",
8 | "docs:preview": "vitepress preview"
9 | }
10 | }
--------------------------------------------------------------------------------
/docs/public/assets/guides/add-input-advancer-to-scene.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyxus/fray/ab186b335f1fc42837092d16971dca8f4e580b18/docs/public/assets/guides/add-input-advancer-to-scene.png
--------------------------------------------------------------------------------
/docs/public/assets/guides/add-state-machine-to-scene.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyxus/fray/ab186b335f1fc42837092d16971dca8f4e580b18/docs/public/assets/guides/add-state-machine-to-scene.png
--------------------------------------------------------------------------------
/docs/public/assets/guides/building-state-machine-root.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyxus/fray/ab186b335f1fc42837092d16971dca8f4e580b18/docs/public/assets/guides/building-state-machine-root.webp
--------------------------------------------------------------------------------
/docs/public/assets/guides/input-advancer-in-scene.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyxus/fray/ab186b335f1fc42837092d16971dca8f4e580b18/docs/public/assets/guides/input-advancer-in-scene.png
--------------------------------------------------------------------------------
/docs/public/assets/guides/inspector-state-machine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyxus/fray/ab186b335f1fc42837092d16971dca8f4e580b18/docs/public/assets/guides/inspector-state-machine.png
--------------------------------------------------------------------------------
/docs/public/assets/guides/state-machine-in-scene.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pyxus/fray/ab186b335f1fc42837092d16971dca8f4e580b18/docs/public/assets/guides/state-machine-in-scene.png
--------------------------------------------------------------------------------
/docs/public/assets/icons/fray-logo-light.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/state-management/building-a-state-machine.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | outline: [2, 6]
4 | ---
5 |
6 | # Building A State Machine
7 |
8 | ## What is a Hierarchical State Machine?
9 |
10 | A state machine is a model that represents an entity's various states and the transitions between them in a finite and structured way. They can be visualized as a graph where each point is a state and the connecting lines are transitions. To be hierarchical means that within each state there can exist entire state machines, which is useful when modeling more complex behaviors.
11 |
12 | ## 1. Add State Machine To Scene
13 |
14 | Before you can begin building a state machine first add a `FrayStateMachine` node to the scene. For this guide the node will be named 'FrayStateMachine'.
15 |
16 | 
17 | 
18 |
19 | ## 2. Build State Machine Root
20 |
21 | All state machines require a `FrayCompoundState` root in order to function. Compound states are responsible for describing the set of states and transitions present within the state machine.
22 |
23 | The compound state can be configured directly through methods such as `add_state()` and `add_transition()`. However, it is recommended to construct states using the included `FrayCompoundState.Builder`class. An instance of the class can be obtained through the static `FrayCompoundState.builder()` method.
24 |
25 | ```gdscript
26 | # Explicit Configuration
27 | var root := FrayCompoundState.new()
28 | root.add_state("a", FrayState.new())
29 | root.add_state("b", FrayState.new())
30 | root.add_state("c", FrayState.new())
31 | root.add_transition("a", "b", FrayTransition.new())
32 | root.add_transition("b", "c", FrayTransition.new())
33 | root.add_transition("c", "a", FrayTransition.new())
34 | root.start_state = "a"
35 |
36 | # Builder Configuration
37 | var root := (FrayCompoundState.builder()
38 | .start_at("a")
39 | .transition("a", "b")
40 | .transition("b", "c")
41 | .transition("c", "a")
42 | .build()
43 | )
44 | ```
45 |
46 | 
47 |
48 | With the exception of `build()`, all builder methods return an instance of the builder, allowing for chain method calls. Additionally, the builder will create a state instance whenever a state is mentioned, meaning it is not required to add a state before using it. However, the builder's `add_state()` method is required when adding custom states.
49 |
50 | ## 3. Initialize State Machine
51 |
52 | Before a state machine can be used it needs to be initialized. The `initialize()` method takes 2 arguments. First a context, which is a dictionary that can be used to provide read-only data to custom states added to the system. Second, a `FrayCompoundState`, which serves as the root of the state machine.
53 |
54 | ```gdscript
55 | state_machine.initialize({}, FrayCompoundState.builder()
56 | .transition("a", "b", {auto_advance=true})
57 | .transition("b", "c", {auto_advance=true})
58 | .transition("c", "a", {auto_advance=true})
59 | .build()
60 | )
61 | ```
62 |
63 | Notice `transition()` takes an optional 3rd argument which is a dictionary that allows properties belonging to `FrayStateMachineTransition`. For this example auto advance is enabled as a simple way to see the state machine in action.
64 |
65 | ## 4. Observing State Transitions
66 |
67 | Now that the state machine has been initialized state transitions can be observed using the `state_changed` signal on the state machine.
68 |
69 | ```gdscript
70 | func _ready() -> void:
71 | state_machine.state_changed.connect(_on_StateMachine_state_changed)
72 |
73 | func _on_StateMachine_state_changed(from: StringName, to: StringName) -> void:
74 | print("State transitioned from '%s' to '%s'" % [from, to])
75 | ```
76 |
77 | Before you can see the newly created state machine in action first select the state machine node in the tree and then from the inspector set the `active` property to true. Additionally, set `advance_mode` to manual. At the moment the state machine has nothing to limit its transitions so allowing it to advance automatically will result in the state machine cycling to the next avaialble state every frame.
78 |
79 | 
80 |
81 | Lastly since the state machine is set to manual mode, call the state machine's `advance` method inside of the `_process()` whenever `ui_select` is just pressed.
82 |
83 | ```gdscript
84 | func _process(delta: float):
85 | if Input.is_action_just_pressed("ui_select"):
86 | state_machine.advance()
87 | ```
88 |
89 | Now whenever space is pressed the state will change and print a message informing that the current state has changed.
90 |
91 | Alternatively the `print_adj()` method can be used to quickly print the state of a state machine for debug purposes.
92 |
93 | ```gdscript
94 | func _process():
95 | if Input.is_action_just_pressed("ui_select"):
96 | state_machine.advance()
97 | state_machine.get_root().print_adj()
98 | ```
99 |
100 | ## Conclusion
101 |
102 | You should now have a script resembling the one below. This script assumes that the state machine is a direct child of the attached node.
103 |
104 | ```gdscript
105 | extends Node
106 |
107 | @onready var state_machine: FrayStateMachine = $FrayStateMachine
108 |
109 | func _ready() -> void:
110 | state_machine.state_changed.connect(_on_StateMachine_state_changed)
111 | state_machine.initialize({}, FrayCompoundState.builder()
112 | .transition("a", "b", {auto_advance=true})
113 | .transition("b", "c", {auto_advance=true})
114 | .transition("c", "a", {auto_advance=true})
115 | .build()
116 | )
117 |
118 | func _process(delta: float):
119 | if Input.is_action_just_pressed("ui_select"):
120 | state_machine.advance()
121 |
122 | func _on_StateMachine_state_changed(from: StringName, to: StringName) -> void:
123 | print("State transitioned from '%s' to '%s'" % [from, to])
124 | ```
125 |
--------------------------------------------------------------------------------
/docs/state-management/overview.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | outline: [2, 6]
4 | ---
5 |
6 | # State Management Module
7 |
8 | ## Purpose of Module
9 |
10 | State management is vital when designing complex game mechanics. It involves defining the set of states a game entity can occupy and specifying the corresponding transitions to other states. In the context of action games, this encompasses mapping buttons to actions and determining which actions are accessible from any given state.
11 |
12 | Fray provides an extendable general purpose hierarchical state machine which can be used to define state for entities such as combatants.
13 |
--------------------------------------------------------------------------------
/docs/state-management/providing-data-to-states.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: doc
3 | outline: [2, 6]
4 | ---
5 |
6 | # Providing Data To States
7 |
8 | ## Context
9 |
10 | Context is read-only data provided on initialization, accessible to all states during the initialization process. It can be used to provide states with initial values and objects for them to observe or command. While it's technically possible to share an object with mutable data across states within the context, it's recommended to avoid communicating between states using mutable data. Ideally, a state machine models well-defined states and transitions. When states rely on mutable data, it can become challenging to reason about the system, as states are no longer independent.
11 |
12 | ```gdscript
13 | state_machine.initialize({
14 | player_max_health=100
15 | }, ...)
16 | ```
17 |
18 | ```gdscript
19 | class_name CustomState
20 |
21 | var player_max_health: float
22 |
23 | func _ready(context: Dictionary) -> void:
24 | player_max_health = context.get("player_max_health", 0.0)
25 | ```
26 |
27 | ## Node Referencing
28 |
29 | Nodes, like any other objects, can be provided to a state machine within the context. However, it is also possible for states to fetch node references using their `get_node()`, `get_node_of_type()`, and `get_nodes_of_type()` methods. `get_node()` will fetch a node relative to the state machine, while the 'type' variants will fetch nodes that are direct children of the state machine.
30 |
31 | ```gdscript
32 | class_name CustomState
33 |
34 | var anim_player: AnimationPlayer
35 |
36 | func _ready(context: Dictionary) -> void:
37 | anim_player = get_node_of_type(AnimationPlayer)
38 | ```
39 |
40 | This allows states to interact with and observe nodes within the scene providing the ability to encapsulate control of the game's behavior within different states.
41 |
42 | ## Object Constructor
43 |
44 | When essential data is required for a state to function, it is recommended to provide this data through the state's constructor.
45 |
46 | ```gdscript
47 | class_name CustomState
48 | extends FrayState
49 |
50 | var dep
51 |
52 | func _init(dependency) -> void:
53 | dep = dependency
54 | ```
55 |
56 | This ensures that the state receives the necessary information during its initialization.
57 |
--------------------------------------------------------------------------------
/docs/state-management/using-global-transitions.md:
--------------------------------------------------------------------------------
1 | # Using Global Transitions
2 |
3 | ## What Are Global Transitions?
4 |
5 | Global transitions are a feature designed for convenience, enabling automatic connections between states based on transition rules. Within the state machine, states can be assigned tags, and transition rules can be established between tags. States with a specified 'from_tag' will automatically have transitions set up to states with a corresponding 'to_tag.' This simplifies the process of managing common transitions and reduces the need for repetitive code.
6 |
7 | ## How To Use Global Transitions
8 |
9 | To leverage global transitions, start by assigning tags to relevant states using the builder's `tag()` method. A state can be assigned multiple tags as needed. Next, specify rules using the `add_rule()` method. Finally, make a state global by supplying it with a global transition. A state can have multiple global transitions, and when a rule is matched, a transition will be attempted for each specified global transition.
10 |
11 | ```gdscript
12 | state_machine.initialize({}, FrayCompoundState.builder()
13 | .tag("idle", "normal")
14 | .tag("attack", "normal")
15 | .tag("sp_attack", "special")
16 | .add_rule("normal", "special")
17 | .transition_global("sp_attack")
18 | .transition("idle", "attack")
19 | .build()
20 | )
21 | ```
22 |
23 | Global versions also exist for the builder's `transition_press()` and `transition_sequence()` methods.
24 |
25 | ```gdscript
26 | state_machine.initialize({}, FrayCompoundState.builder()
27 | .tag("idle", "normal")
28 | .tag("attack", "normal")
29 | .tag("sp_attack", "special")
30 | .add_rule("normal", "special")
31 | .transition_sequence_global("sp_attack", {sequence="214K"})
32 | .transition_press("idle", "attack", {input="punch_button"})
33 | .build()
34 | )
35 | ```
36 |
--------------------------------------------------------------------------------
/docs/state-management/using-input-transitions.md:
--------------------------------------------------------------------------------
1 | # Using Input Transitions
2 |
3 | ## What Is An Input Transition?
4 |
5 | Input transitions, distinct from [state machine transition inputs](/state-management/controlling-state-transitions#define-accepted-input-custom-transitions), are `FrayStateMachineTransition` implementations used to describe transitions based on input from devices such as a controller. Fray offers two types of input transitions: `FrayInputTransitionPress` and `FrayInputTransitionSequence`.
6 |
7 | ## How Do Input Transitions Work?
8 |
9 | Input transitions do not automatically respond to device-inputs. All an input transition does is check if the provided state machine input contains specific entires satisfying the transition's configuration. Meaning the actual device-input must be checked for externally and then fed to the state machine though the `advance()` method.
10 |
11 | ```gdscript
12 | state_machine.advance({
13 | input="jump",
14 | is_pressed=true,
15 | time_since_last_input=0, time_held=0
16 | })
17 |
18 | state_machine.advance({
19 | sequence="hadoken",
20 | time_since_last_input=0
21 | })
22 | ```
23 |
24 | ::: tip Note
25 | In a previous guide the state machine's advance mode was set to 'Manual' before calling `advance()`. However, advance can be called regardless of advance mode. There is no issue with having the state machine automatically advance while trying to advance manually, especially when supplying input data.
26 | :::
27 |
28 | ## Using Press Transitions
29 |
30 | `FrayInputTransitionPress` is used to define a transition triggered by a single press, like a button on a controller or a key on a keyboard. Press transitions can be created using the builder's `transition_press()` method, with the input name provided in the transition configuration.
31 |
32 | ```gdscript
33 | state_machine.initialize({}, FrayCompoundState.builder()
34 | .transition_press("idle", "attack", {input="punch_button"})
35 | .build()
36 | )
37 | ```
38 |
39 | ## Using Sequence Transitions
40 |
41 | `FrayInputTransitionSequence` describes a transition triggered by a sequence of presses. Sequence transitions can be created using the builder's `transition_sequence()` method, with the sequence name provided in the transition configuration.
42 |
43 | ```gdscript
44 | state_machine.initialize({}, FrayCompoundState.builder()
45 | .transition_sequence("idle", "hadoken", {sequence="236P"})
46 | .build()
47 | )
48 | ```
49 |
50 | ## Taking Advantage of The Input Advancer
51 |
52 | Fray provides a `BufferedInputAdvancer`, a node designed to automatically feed buffered input data to a state machine. If an input is accepted by the state machine, the advancer will stop processing new inputs for the current frame.
53 |
54 | To use the advancer, first add it as a child of the desired state machine. Then, buffer the desired inputs using the included `buffer_press()` and `buffer_sequence()` methods.
55 |
56 | 
57 |
58 | ```gdscript
59 | if Input.is_action_just_pressed("ui_select"):
60 | advancer.buffer_press("jump_button")
61 | ```
62 |
63 | Additionally, the advancer can be paused to control the timing of input feeding. Note that pausing doesn't stop the advancer; inputs can still be buffered during pauses, and they will expire if they exceed the maximum buffer time. This behavior allows the timeframe during which device inputs have the opportunity to trigger state transitions to be defined. Even if input buffering is not required, the advancer offers a friendly interface for feeding device inputs to a state machine.
64 |
65 | ```gdscript
66 | advancer.paused = false
67 | ```
--------------------------------------------------------------------------------
/lib/data_structures/circular_buffer.gd:
--------------------------------------------------------------------------------
1 | extends RefCounted
2 | ## docstring
3 |
4 | #signals
5 |
6 | #enums
7 |
8 | #constants
9 |
10 | #preloaded scripts and scenes
11 |
12 | #exported variables
13 |
14 | var capacity: int = 1:
15 | set(value):
16 | if value <= 0:
17 | push_error("Circular buffer capacity can not be smaller than 1")
18 |
19 | capacity = max(1, value)
20 | _buffer.resize(capacity)
21 | _read_index = 0
22 | _write_index = 0
23 |
24 |
25 | var _read_index: int
26 | var _write_index: int
27 | var _buffer: Array
28 | var _is_full: bool
29 |
30 | #onready variables
31 |
32 |
33 | func _init(buffer_capacity: int = 1) -> void:
34 | set_capacity(buffer_capacity)
35 |
36 | #built-in virtual _ready method
37 |
38 | #remaining built-in virtual methods
39 |
40 | func set_capacity(value: int) -> void:
41 | if value <= 0:
42 | push_error("Circular buffer capacity can not be smaller than 1")
43 |
44 | capacity = max(1, value)
45 | _buffer.resize(capacity)
46 | _read_index = 0
47 | _write_index = 0
48 |
49 |
50 | func add(item) -> bool:
51 | if not _is_full:
52 | _buffer[_write_index] = item
53 | _write_index = (_write_index + 1) % capacity
54 | _is_full = _write_index == _read_index
55 | return true
56 | return false
57 |
58 |
59 | func insert(position: int, item):
60 | _buffer[position] = item
61 |
62 |
63 | func peek():
64 | return _buffer[_read_index]
65 |
66 |
67 | func peek_at(position: int):
68 | return _buffer[position]
69 |
70 |
71 | func read():
72 | if not empty():
73 | var item = peek()
74 | _read_index = (_read_index + 1) % capacity
75 | _is_full = false
76 | return item
77 | return null
78 |
79 |
80 | func full() -> bool:
81 | return _is_full
82 |
83 |
84 | func empty() -> bool:
85 | return _write_index == _read_index and not _is_full
86 |
87 |
88 | func get_count() -> int:
89 | return _write_index - _read_index
90 |
91 |
92 | func get_read_index() -> int:
93 | return _read_index
94 |
95 |
96 | func get_write_index() -> int:
97 | return _write_index
98 |
99 |
100 | func clear() -> void:
101 | _read_index = 0
102 | _write_index = 0
103 | _is_full = false
104 |
105 |
106 | func _iter_init(arg):
107 | return not empty()
108 |
109 |
110 | func _iter_next(arg):
111 | return not empty()
112 |
113 |
114 | func _iter_get(arg):
115 | return read()
116 |
117 | #signal methods
118 |
119 | #inner class
120 |
--------------------------------------------------------------------------------
/lib/data_structures/linked_list.gd:
--------------------------------------------------------------------------------
1 | extends RefCounted
2 |
3 | var _head: ListNode
4 | var _itter_current: ListNode
5 | var _count: int = 0
6 |
7 | func add(data) -> void:
8 | if _head == null:
9 | _head = ListNode.new(data)
10 | _count = 1
11 | else:
12 | var next_node := _head
13 | while next_node._next != null:
14 | next_node = next_node._next
15 | next_node._next = ListNode.new(data)
16 | _count += 1
17 |
18 | func print_list() -> void:
19 | if _head == null:
20 | print("[]")
21 | return
22 |
23 | var string := ""
24 | var next_node := _head
25 | while next_node != null:
26 | string += "[" + next_node.data.to_string() + "]"
27 | next_node = next_node.get_next()
28 | if next_node != null:
29 | string += " --> "
30 | print(string)
31 |
32 |
33 | func remove_first() -> void:
34 | if _head != null:
35 | _head = _head._next
36 | _count -= 1
37 |
38 |
39 | func get_head() -> ListNode:
40 | return _head
41 |
42 |
43 | func get_count() -> int:
44 | return _count
45 |
46 |
47 | func empty() -> bool:
48 | return _head == null
49 |
50 |
51 | func clear() -> void:
52 | _head = null
53 | _count = 0
54 |
55 |
56 | func _iter_init(arg):
57 | _itter_current = _head
58 | return _head != null
59 |
60 |
61 | func _iter_next(arg):
62 | _itter_current = _itter_current._next
63 | return _itter_current != null
64 |
65 |
66 | func _iter_get(arg):
67 | return _itter_current.data
68 |
69 |
70 | class ListNode:
71 | extends RefCounted
72 |
73 | ## Type: Variant
74 | var data
75 |
76 | ## Type: ListNode
77 | var _next: RefCounted
78 |
79 | func _init(node_data) -> void:
80 | data = node_data
81 |
82 | ## Returns: ListNode
83 | func get_next() -> RefCounted:
84 | return _next
85 |
--------------------------------------------------------------------------------
/lib/data_structures/reversable_dictionary.gd:
--------------------------------------------------------------------------------
1 | extends RefCounted
2 |
3 | var _main_dict: Dictionary
4 | var _reverse_dict: Dictionary
5 |
6 |
7 | func add(key, value) -> void:
8 | _main_dict[key] = value
9 | _reverse_dict[value] = key
10 |
11 |
12 | func clear() -> void:
13 | _main_dict.clear()
14 | _reverse_dict.clear()
15 |
16 |
17 | func is_empty() -> bool:
18 | return _main_dict.is_empty()
19 |
20 |
21 | func erase_key(key) -> bool:
22 | if _main_dict.has(key):
23 | var dict_item = _main_dict[key]
24 | _main_dict.erase(key)
25 | _reverse_dict.erase(dict_item)
26 | return true
27 | return false
28 |
29 |
30 | func erase_value(value) -> bool:
31 | if _reverse_dict.has(value):
32 | var dict_item = _reverse_dict[value]
33 | _reverse_dict.erase(value)
34 | _main_dict.erase(dict_item)
35 | return true
36 | return false
37 |
38 |
39 | func get_value(key, default = null):
40 | if _main_dict.has(key):
41 | return _main_dict[key]
42 | return default
43 |
44 |
45 | func get_key(value, default = null):
46 | if _reverse_dict.has(value):
47 | return _reverse_dict[value]
48 | return default
49 |
50 |
51 | func has_key(key) -> bool:
52 | return _main_dict.has(key)
53 |
54 |
55 | func has_value(value) -> bool:
56 | return _reverse_dict.has(value)
57 |
58 |
59 | func has_all_keys(keys: Array) -> bool:
60 | if _main_dict.is_empty():
61 | return false
62 |
63 | for key in keys:
64 | if not _main_dict.has(key):
65 | return false
66 |
67 | return true
68 |
69 |
70 | func has_all_values(values: Array) -> bool:
71 | if _reverse_dict.is_empty():
72 | return false
73 |
74 | for value in values:
75 | if not _reverse_dict.has(value):
76 | return false
77 |
78 | return true
79 |
80 |
81 | func hash_main() -> int:
82 | return _main_dict.hash()
83 |
84 |
85 | func hash_reverse() -> int:
86 | return _reverse_dict.hash()
87 |
88 |
89 | func size() -> int:
90 | return _main_dict.size()
91 |
92 |
93 | func keys() -> Array:
94 | return _main_dict.keys()
95 |
96 |
97 | func values() -> Array:
98 | return _main_dict.values()
99 |
--------------------------------------------------------------------------------
/lib/helpers/child_change_detector.gd:
--------------------------------------------------------------------------------
1 | extends RefCounted
2 |
3 | signal child_changed(node: Node, change: Change)
4 |
5 | enum Change{
6 | ADDED,
7 | REMOVED,
8 | RENAMED,
9 | SCRIPT_CHANGED,
10 | }
11 |
12 | const SignalUtils = preload("utils/signal_utils.gd")
13 |
14 | var _parent: Node
15 |
16 | func _init(parent: Node) -> void:
17 | if not parent.is_inside_tree():
18 | push_error("Failed to initialize. Given node parent is not inside of tree.")
19 | return
20 |
21 | var scene_tree = parent.get_tree()
22 | _parent = parent
23 | SignalUtils.safe_connect(scene_tree.node_added, _on_SceneTree_node_added)
24 | SignalUtils.safe_connect(scene_tree.node_removed, _on_SceneTree_node_removed)
25 |
26 |
27 | func _on_SceneTree_node_added(node: Node) -> void:
28 | if node.get_parent() == _parent:
29 | child_changed.emit(node, Change.ADDED)
30 | SignalUtils.safe_connect(node.script_changed, _on_ChildNode_script_changed, [node])
31 | SignalUtils.safe_connect(node.renamed, _on_ChildNode_renamed, [node])
32 |
33 |
34 | func _on_SceneTree_node_removed(node: Node) -> void:
35 | if node.get_parent() == _parent:
36 | child_changed.emit(node, Change.REMOVED)
37 | SignalUtils.safe_disconnect(node.script_changed, _on_ChildNode_script_changed)
38 | SignalUtils.safe_disconnect(node.renamed, _on_ChildNode_renamed)
39 |
40 |
41 | func _on_ChildNode_script_changed(node: Node) -> void:
42 | child_changed.emit(node, Change.SCRIPT_CHANGED)
43 |
44 |
45 | func _on_ChildNode_renamed(node: Node) -> void:
46 | child_changed.emit(node, Change.RENAMED)
47 |
--------------------------------------------------------------------------------
/lib/helpers/pseudo_interface.gd:
--------------------------------------------------------------------------------
1 | extends Object
2 |
3 | const _interfaces = {
4 | "IHitbox" : {
5 | "methods" : ["activate", "deactivate", "set_source"],
6 | "signals" : ["hitbox_entered", "hitbox_exited"],
7 | },
8 | }
9 |
10 | func _init() -> void:
11 | assert(false, "Thiss class only provides pseudo-interface membership test and is not intended to be instantiated")
12 | free()
13 |
14 |
15 | static func implements(obj: Object, interface: String) -> bool:
16 | if not _interfaces.has(interface):
17 | push_error("Failed to check interface. Interface '%s' is not defined." % interface)
18 | return false
19 |
20 | return _get_missing_members(obj, interface).is_empty()
21 |
22 |
23 | static func assert_implements(obj: Object, interface: String) -> void:
24 | var script_name: String = obj.get_script().resource_path.get_file()
25 | var has_implementation := implements(obj, interface)
26 |
27 | if not has_implementation:
28 | for missing_member in _get_missing_members(obj, interface):
29 | push_error(missing_member)
30 |
31 | assert(has_implementation, "Script '%s' does not implement interface '%s'" % [script_name, interface])
32 |
33 |
34 | static func _get_missing_members(obj: Object, interface: String) -> PackedStringArray:
35 | var script_name: String = obj.get_script().resource_path.get_file()
36 | var missing_members: PackedStringArray = []
37 |
38 | for method in _interfaces[interface]["methods"]:
39 | if not obj.has_method(method):
40 | missing_members.append("'%s' does not implement interface method '%s.%s'" % [script_name, interface, method])
41 |
42 | for sig in _interfaces[interface]["signals"]:
43 | if not obj.has_signal(sig):
44 | missing_members.append("'%s' does not implement interface signal '%s.%s'" % [script_name, interface, sig])
45 |
46 | return missing_members
47 |
--------------------------------------------------------------------------------
/lib/helpers/utils/signal_utils.gd:
--------------------------------------------------------------------------------
1 | extends RefCounted
2 |
3 | ## Connects signal only if not already connected.
4 | static func safe_connect(
5 | sig: Signal,
6 | method: Callable, binds: Array = [], flags: int = 0
7 | ) -> int:
8 | if not sig.is_connected(method):
9 | for bind in binds:
10 | method = method.bind(bind)
11 | return sig.connect(method, flags)
12 | return OK
13 |
14 | ## Disconnects signal only if already connected.
15 | static func safe_disconnect(
16 | sig: Signal,
17 | method: Callable) -> void:
18 | if sig.is_connected(method):
19 | sig.disconnect(method)
20 |
--------------------------------------------------------------------------------
/lib/helpers/utils/sorting.gd:
--------------------------------------------------------------------------------
1 | extends RefCounted
2 |
3 | static func sort_ascending(t1, t2, property: String = "") -> bool:
4 | if property.is_empty():
5 | if t1[property] > t2[property]:
6 | return true
7 | else:
8 | return false
9 | else:
10 | if t1 > t2:
11 | return true
12 | else:
13 | return false
14 |
--------------------------------------------------------------------------------
/plugin.cfg:
--------------------------------------------------------------------------------
1 | [plugin]
2 |
3 | name="Fray - Combat Framework"
4 | description="Provides tools which aid in the implementation of action/fighting game style combat."
5 | author="Pyxus"
6 | version="2.0.0-alpha"
7 | script="plugin.gd"
8 |
--------------------------------------------------------------------------------
/plugin.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | extends EditorPlugin
3 |
4 | var _added_types: Array
5 | var _added_singletons: Array
6 | var _editor_interface := get_editor_interface()
7 | var _editor_settings := _editor_interface.get_editor_settings()
8 |
9 | func _ready() -> void:
10 | pass
11 |
12 |
13 | func _enter_tree() -> void:
14 | add_autoload_singleton("FrayInputMap", "res://addons/fray/src/input/autoloads/fray_input_map.gd")
15 | add_autoload_singleton("FrayInput", "res://addons/fray/src/input/autoloads/fray_input.gd")
16 |
17 |
18 | func _exit_tree():
19 | for singleton in _added_singletons:
20 | remove_autoload_singleton(singleton)
21 |
22 | for type in _added_types:
23 | remove_custom_type(type)
24 |
25 |
26 | func add_autoload_singleton(name: String, path: String) -> void:
27 | super(name, path)
28 | _added_singletons.append(name)
29 |
30 |
31 | func add_custom_type(type: String, base: String, script: Script, icon: Texture2D) -> void:
32 | super(type, base, script, icon)
33 | _added_types.append(type)
34 |
--------------------------------------------------------------------------------
/src/fray.gd:
--------------------------------------------------------------------------------
1 | class_name Fray
2 | extends Object
3 | ## A collection of static helper functions and constants
4 |
5 | ## Converts time in [kbd]frames[/kbd] to time in milliseconds.
6 | ## [br]
7 | ## One frame is equal to 1 / fps
8 | ## [br]
9 | ## if [kbd]fps[/kbd] is a value less than or equal to 0 then fps defaults to [member Engine.physics_ticks_per_second].
10 | static func frame_to_msec(frames: int, fps: int = -1) -> int:
11 | fps = Engine.physics_ticks_per_second if fps <= 0 else fps
12 | return floori((frames / float(Engine.physics_ticks_per_second)) * 1000)
13 |
14 | ## Converts time in [kbd]msec[/kbd] to time in frames.
15 | ## [br]
16 | ## One frame is equal to 1 / fps
17 | ## [br]
18 | ## if [kbd]fps[/kbd] is a value less than or equal to 0 then fps defaults to [member Engine.physics_ticks_per_second].
19 | static func msec_to_frame(msec: int, fps: int = -1) -> int:
20 | fps = Engine.physics_ticks_per_second if fps <= 0 else fps
21 | return ceili((Engine.physics_ticks_per_second * msec) / 1000.0)
22 |
23 | ## Converts time in [kbd]frames[/kbd] to time in seconds.
24 | ## [br]
25 | ## One frame is equal to 1 / fps
26 | ## [br]
27 | ## if [kbd]fps[/kbd] is a value less than or equal to 0 then fps defaults to [member Engine.physics_ticks_per_second].
28 | static func frame_to_sec(frames: int, fps: int = -1) -> int:
29 | fps = Engine.physics_ticks_per_second if fps <= 0 else fps
30 | return floori(frames / float(Engine.physics_ticks_per_second))
31 |
32 | ## Converts time in [kbd]sec[/kbd] to time in frames.
33 | ## [br]
34 | ## One frame is equal to 1 / fps
35 | ## [br]
36 | ## if [kbd]fps[/kbd] is a value less than or equal to 0 then fps defaults to [member Engine.physics_ticks_per_second].
37 | static func sec_to_frame(sec: int, fps: int = -1) -> int:
38 | fps = Engine.physics_ticks_per_second if fps <= 0 else fps
39 | return ceili(Engine.physics_ticks_per_second * sec)
40 |
41 | ## Converts time in [kdb]milliseconds[/kbd] to time in seconds.
42 | static func msec_to_sec(msec: int) -> float:
43 | return msec / 1000.0
44 |
45 | ## Converts time in [kdb]seconds[/kbd] to time in milliseconds.
46 | static func sec_to_msec(sec: float) -> int:
47 | return roundi(sec * 1000)
48 |
49 | ## Returns true if given value is of the expected type.
50 | static func is_of_type(value, expected_type: Script) -> bool:
51 | if not value is Object:
52 | return false
53 |
54 | return is_of_type_script(value.get_script(), expected_type)
55 |
56 | ## Returns true if given scripts are of the same type.
57 | static func is_of_type_script(script: Script, expected_type: Script) -> bool:
58 | if script == null:
59 | return false
60 |
61 | return true if script == expected_type else is_of_type_script(script.get_base_script(), expected_type)
62 |
--------------------------------------------------------------------------------
/src/hit/2d/hit_state_manager_2d.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | @icon("res://addons/fray/assets/icons/hit_state_manager_2d.svg")
3 | class_name FrayHitStateManager2D
4 | extends Node2D
5 | ## Node used to enforce discrete hit states.
6 | ##
7 | ## This node only allows one hit state child to be active at a time.
8 | ## When [member FrayHitState2D.active_hitboxes] changes all other hit states will be deactivate.
9 |
10 | const _ChildChangeDetector = preload("res://addons/fray/lib/helpers/child_change_detector.gd")
11 | const _SignalUtils = preload("res://addons/fray/lib/helpers/utils/signal_utils.gd")
12 |
13 | ## Emitted when the received [kbd]detected_hitbox[/kbd] enters the child [kbd]detector_hitbox[/kbd].
14 | ## Requires child [FrayHitbox2D.monitoring] to be set to [code]true[/code].
15 | signal hitbox_intersected(detector_hitbox: FrayHitbox2D, detected_hitbox: FrayHitbox2D)
16 |
17 | ## Emitted when the received [kbd]detected_hitbox[/kbd] enters the child [kbd]detector_hitbox[/kbd].
18 | ## Requires child [FrayHitbox2D.monitoring] to be set to [code]true[/code].
19 | signal hitbox_separated(detector_hitbox: FrayHitbox2D, detected_hitbox: FrayHitbox2D)
20 |
21 | ## Source of the [FrayHitbox2D]s beneath this node.
22 | ## [br]
23 | ## This is a convinience that allows you to set the hitbox source from the inspector.
24 | ## However, this property only allows nodes to be used as sources.
25 | ## Any object can be used by calling [member set_hitbox_source].
26 | @export var source: Node = null:
27 | set(value):
28 | source = value
29 |
30 | for child in get_children():
31 | if child is FrayHitState2D:
32 | child["metadata/_editor_prop_ptr_source"] = (
33 | child.get_path_to(source) if source else NodePath()
34 | )
35 | set_hitbox_source(value)
36 |
37 | var _current_state: StringName = ""
38 | var _cc_detector: _ChildChangeDetector
39 |
40 | func _ready() -> void:
41 | if Engine.is_editor_hint():
42 | return
43 |
44 | for child in get_children():
45 | if child is FrayHitState2D:
46 | child.set_hitbox_source(source)
47 | child.hitbox_intersected.connect(_on_Hitstate_hitbox_intersected)
48 | child.hitbox_intersected.connect(_on_Hitstate_hitbox_separated)
49 | child.active_hitboxes_changed.connect(_on_HitState_active_hitboxes_changed.bind(child))
50 |
51 |
52 | func _get_configuration_warnings() -> PackedStringArray:
53 | var warnings: PackedStringArray = []
54 |
55 | if get_children().any(func(node): node is FrayHitState2D):
56 | warnings.append("This node has no hit states so there is nothing to manage. Consider adding a FrayHitState2D as a child.")
57 |
58 | return warnings
59 |
60 |
61 | func _enter_tree() -> void:
62 | if Engine.is_editor_hint():
63 | _cc_detector = _ChildChangeDetector.new(self)
64 | _cc_detector.child_changed.connect(_on_ChildChangeDetector_child_changed)
65 |
66 |
67 | func _set(property: StringName, value) -> bool:
68 | match property:
69 | "metadata/_editor_prop_ptr_source":
70 | var node: Node = get_node_or_null(value) as Node
71 | if value.is_empty() or node == null:
72 | set_meta("_editor_prop_ptr_source", NodePath())
73 | source = null
74 | else:
75 | set_meta("_editor_prop_ptr_source", value)
76 | source = node
77 | return true
78 | return false
79 |
80 | ## Returns the name of the current hit state
81 | func get_current_state() -> StringName:
82 | return _current_state
83 |
84 | ## Returns a reference to the current state. Returns null if no state is set.
85 | func get_current_state_obj() -> FrayHitState2D:
86 | return get_node_or_null(NodePath(_current_state)) as FrayHitState2D
87 |
88 | ## Sets the [kbd]source[/kbd] of all [FrayHitbox2D] beneath this node.
89 | func set_hitbox_source(source: Object) -> void:
90 | for child in get_children():
91 | if child is FrayHitState2D:
92 | child.set_hitbox_source(source)
93 |
94 |
95 | func _set_current_state(new_current_state: StringName) -> void:
96 | if new_current_state != _current_state:
97 | _current_state = new_current_state
98 |
99 | for child in get_children():
100 | if child is FrayHitState2D and child.name != _current_state:
101 | child.deactivate()
102 | print(child.name)
103 |
104 |
105 | func _on_ChildChangeDetector_child_changed(node: Node, change: _ChildChangeDetector.Change) -> void:
106 | if change == _ChildChangeDetector.Change.ADDED:
107 | set_deferred("source", source)
108 |
109 | if node is FrayHitState2D and change != _ChildChangeDetector.Change.REMOVED:
110 | _SignalUtils.safe_connect(node.active_hitboxes_changed, _on_HitState_active_hitboxes_changed, [node])
111 |
112 |
113 | func _on_Hitstate_hitbox_intersected(detector_hitbox: FrayHitbox2D, detected_hitbox: FrayHitbox2D) -> void:
114 | hitbox_intersected.emit(detector_hitbox, detected_hitbox)
115 |
116 |
117 | func _on_Hitstate_hitbox_separated(detector_hitbox: FrayHitbox2D, detected_hitbox: FrayHitbox2D) -> void:
118 | hitbox_separated.emit(detector_hitbox, detected_hitbox)
119 |
120 |
121 | func _on_HitState_active_hitboxes_changed(hitstate: FrayHitState2D) -> void:
122 | if hitstate.active_hitboxes != 0:
123 | _set_current_state(hitstate.name)
124 | hitstate.activate()
--------------------------------------------------------------------------------
/src/hit/2d/hitbox_2d.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | @icon("res://addons/fray/assets/icons/hitbox_2d.svg")
3 | class_name FrayHitbox2D
4 | extends Area2D
5 | ## 2D area intended to detect combat interactions.
6 | ##
7 | ## The hitbox node doesn't provide much functionality out of the box.
8 | ## It serves as a template you can expand upon through the use of [FrayHitboxAttribute]
9 |
10 | ## Emitted when the received [kbd]hitbox[/kbd] enters this hitbox. Requires monitoring to be set to [code]true[/code].
11 | signal hitbox_entered(hitbox: FrayHitbox2D)
12 |
13 | ## Emitted when the received [kbd]hitbox[/kbd] exits this hitbox. Requires monitoring to be set to [code]true[/code].
14 | signal hitbox_exited(hitbox: FrayHitbox2D)
15 |
16 | ## If true then hitboxes that share the same source as this one will still be detected
17 | @export var detect_source_hitboxes: bool = false
18 |
19 | ## The [FrayHitboxAttribute] assigned to this hitbox
20 | @export var attribute: FrayHitboxAttribute = null:
21 | set(value):
22 | attribute = value
23 |
24 | if attribute != null:
25 | _update_collision_colors()
26 |
27 | update_configuration_warnings()
28 |
29 | ## Source of this hitbox.
30 | ## Hitboxes with the same source will not detect one another unless [member detect_source_hitboxes] is enabled.
31 | var source: Object = null
32 |
33 | var _hitbox_exceptions: Array[FrayHitbox2D]
34 | var _source_exceptions: Array[Object]
35 |
36 |
37 | func _ready() -> void:
38 | if Engine.is_editor_hint():
39 | child_entered_tree.connect(
40 | func(node: Node):
41 | if node is CollisionShape2D:
42 | _update_collision_colors()
43 | )
44 | return
45 |
46 | area_entered.connect(_on_area_entered)
47 | area_exited.connect(_on_area_exited)
48 |
49 |
50 | func _get_configuration_warnings() -> PackedStringArray:
51 | var warnings: PackedStringArray = []
52 | if attribute == null:
53 | warnings.append("Hitboxes without attribute are effectively just Area2Ds. Consider giving this node a FrayHitboxAttribute resource.")
54 | elif not attribute.get_script().is_tool():
55 | warnings.append("Custom attribute must be a tool script to avoid editor errors. Consider adding the @tool annotation to the top of the script.")
56 | return warnings
57 |
58 | ## Returns a list of intersecting [FrayHitbox2D]s.
59 | func get_overlapping_hitboxes() -> Array[FrayHitbox2D]:
60 | var hitboxes: Array[FrayHitbox2D]
61 | for area in get_overlapping_areas():
62 | if can_detect(area):
63 | hitboxes.append(area)
64 | return hitboxes
65 |
66 | ## Adds a [kbd]hitbox[/kbd] to a list of hitboxes this hitbox can't detect
67 | func add_hitbox_exception_with(hitbox: FrayHitbox2D) -> void:
68 | if hitbox is FrayHitbox2D and not _hitbox_exceptions.has(hitbox):
69 | _hitbox_exceptions.append(hitbox)
70 |
71 | ## Removes a [kbd]hitbox[/kbd] from a list of hitboxes this hitbox can't detect
72 | func remove_hitbox_exception_with(hitbox: FrayHitbox2D) -> void:
73 | if _hitbox_exceptions.has(hitbox):
74 | _hitbox_exceptions.erase(hitbox)
75 |
76 | ## Adds a source [kbd]object[/kbd] to a list of sources whose hitboxes this hitbox can't detect
77 | func add_source_exception_with(object: Object) -> void:
78 | if not _source_exceptions.has(object):
79 | _source_exceptions.append(object)
80 |
81 | ## Removes a source [kbd]object[/kbd] to a list of sources whose hitboxes this hitbox can't detect
82 | func remove_source_exception_with(object: Object) -> void:
83 | if _source_exceptions.has(object):
84 | _source_exceptions.erase(object)
85 |
86 | ## Activates this hitbox allowing it to monitor and be monitored.
87 | func activate() -> void:
88 | monitorable = true
89 | monitoring = true
90 | show()
91 |
92 | ## Deactivates this hitobx preventing it from monitoring and being monitored.
93 | func deactivate() -> void:
94 | monitorable = false
95 | monitoring = false
96 | hide()
97 |
98 | ## Returns [code]true[/code] if this hitbox is able to detect the given [kbd]hitbox[/kbd].
99 | ## [br]
100 | ## A hitbox can not detect another hitbox if there is a source or hitbox exception
101 | ## or if the set hitbox attribute does not allow interaction with the given hitbox.
102 | func can_detect(hitbox: FrayHitbox2D) -> bool:
103 | return (
104 | not _hitbox_exceptions.has(hitbox)
105 | and not _source_exceptions.has(hitbox.source)
106 | and (detect_source_hitboxes or source == null or hitbox.source != source)
107 | and attribute.allows_detection_of(hitbox.attribute) if attribute != null else true
108 | )
109 |
110 |
111 | func _update_collision_colors() -> void:
112 | if attribute != null:
113 | for node in get_children():
114 | if node is CollisionShape2D:
115 | node.debug_color = attribute.get_color()
116 |
117 |
118 | func _on_area_entered(area: Area2D) -> void:
119 | if area is FrayHitbox2D and can_detect(area):
120 | hitbox_entered.emit(area)
121 |
122 |
123 | func _on_area_exited(area: Area2D) -> void:
124 | if area is FrayHitbox2D and can_detect(area):
125 | hitbox_exited.emit(area)
126 |
--------------------------------------------------------------------------------
/src/hit/3d/hit_state_manager_3d.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | @icon("res://addons/fray/assets/icons/hit_state_manager_3d.svg")
3 | class_name FrayHitStateManager3D
4 | extends Node3D
5 | ## Node used to enforce discrete hit states.
6 | ##
7 | ## This node only allows one hit state child to be active at a time.
8 | ## When [member FrayHitState3D.active_hitboxes] changes all other hit states will be deactivate.
9 |
10 | const _ChildChangeDetector = preload("res://addons/fray/lib/helpers/child_change_detector.gd")
11 | const _SignalUtils = preload("res://addons/fray/lib/helpers/utils/signal_utils.gd")
12 |
13 | ## Emitted when the received [kbd]detected_hitbox[/kbd] enters the child [kbd]detector_hitbox[/kbd].
14 | ## Requires child [FrayHitbox3D.monitoring] to be set to [code]true[/code].
15 | signal hitbox_intersected(detector_hitbox: FrayHitbox3D, detected_hitbox: FrayHitbox3D)
16 |
17 | ## Emitted when the received [kbd]detected_hitbox[/kbd] enters the child [kbd]detector_hitbox[/kbd].
18 | ## Requires child [FrayHitbox3D.monitoring] to be set to [code]true[/code].
19 | signal hitbox_separated(detector_hitbox: FrayHitbox3D, detected_hitbox: FrayHitbox3D)
20 |
21 | ## Source of the [FrayHitbox3D]s beneath this node.
22 | ## [br]
23 | ## This is a convinience that allows you to set the hitbox source from the inspector.
24 | ## However, this property only allows nodes to be used as sources.
25 | ## Any object can be used by calling [member set_hitbox_source].
26 | @export var source: Node = null:
27 | set(value):
28 | source = value
29 |
30 | for child in get_children():
31 | if child is FrayHitState3D:
32 | child["metadata/_editor_prop_ptr_source"] = (
33 | child.get_path_to(source) if source else NodePath()
34 | )
35 | set_hitbox_source(value)
36 |
37 | var _current_state: StringName = ""
38 | var _cc_detector: _ChildChangeDetector
39 |
40 | func _ready() -> void:
41 | if Engine.is_editor_hint():
42 | return
43 |
44 | for child in get_children():
45 | if child is FrayHitState3D:
46 | child.set_hitbox_source(source)
47 | child.hitbox_intersected.connect(_on_Hitstate_hitbox_intersected)
48 | child.hitbox_intersected.connect(_on_Hitstate_hitbox_separated)
49 | child.active_hitboxes_changed.connect(_on_HitState_active_hitboxes_changed.bind(child))
50 |
51 |
52 | func _get_configuration_warnings() -> PackedStringArray:
53 | var warnings: PackedStringArray = []
54 |
55 | if get_children().any(func(node): node is FrayHitState3D):
56 | warnings.append("This node has no hit states so there is nothing to manage. Consider adding a FrayHitState3D as a child.")
57 |
58 | return warnings
59 |
60 |
61 | func _enter_tree() -> void:
62 | if Engine.is_editor_hint():
63 | _cc_detector = _ChildChangeDetector.new(self)
64 | _cc_detector.child_changed.connect(_on_ChildChangeDetector_child_changed)
65 |
66 |
67 | func _set(property: StringName, value) -> bool:
68 | match property:
69 | "metadata/_editor_prop_ptr_source":
70 | var node: Node = get_node_or_null(value) as Node
71 | if value.is_empty() or node == null:
72 | set_meta("_editor_prop_ptr_source", NodePath())
73 | source = null
74 | else:
75 | set_meta("_editor_prop_ptr_source", value)
76 | source = node
77 | return true
78 | return false
79 |
80 | ## Returns the name of the current hit state
81 | func get_current_state() -> StringName:
82 | return _current_state
83 |
84 | ## Returns a reference to the current state. Returns null if no state is set.
85 | func get_current_state_obj() -> FrayHitState3D:
86 | return get_node_or_null(NodePath(_current_state)) as FrayHitState3D
87 |
88 | ## Sets the [kbd]source[/kbd] of all [FrayHitbox3D] beneath this node.
89 | func set_hitbox_source(source: Object) -> void:
90 | for child in get_children():
91 | if child is FrayHitState3D:
92 | child.set_hitbox_source(source)
93 |
94 |
95 | func _set_current_state(new_current_state: StringName) -> void:
96 | if new_current_state != _current_state:
97 | _current_state = new_current_state
98 |
99 | for child in get_children():
100 | if child is FrayHitState3D and child.name != _current_state:
101 | child.deactivate()
102 |
103 |
104 | func _on_ChildChangeDetector_child_changed(node: Node, change: _ChildChangeDetector.Change) -> void:
105 | if change == _ChildChangeDetector.Change.ADDED:
106 | set_deferred("source", source)
107 |
108 | if node is FrayHitState3D and change != _ChildChangeDetector.Change.REMOVED:
109 | _SignalUtils.safe_connect(node.active_hitboxes_changed, _on_HitState_active_hitboxes_changed, [node])
110 |
111 |
112 | func _on_Hitstate_hitbox_intersected(detector_hitbox: FrayHitbox3D, detected_hitbox: FrayHitbox3D) -> void:
113 | hitbox_intersected.emit(detector_hitbox, detected_hitbox)
114 |
115 |
116 | func _on_Hitstate_hitbox_separated(detector_hitbox: FrayHitbox3D, detected_hitbox: FrayHitbox3D) -> void:
117 | hitbox_separated.emit(detector_hitbox, detected_hitbox)
118 |
119 |
120 | func _on_HitState_active_hitboxes_changed(hitstate: FrayHitState3D) -> void:
121 | if hitstate.active_hitboxes != 0:
122 | _set_current_state(hitstate.name)
123 | hitstate.activate()
124 |
--------------------------------------------------------------------------------
/src/hit/3d/hitbox_3d.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | @icon("res://addons/fray/assets/icons/hitbox_3d.svg")
3 | class_name FrayHitbox3D
4 | extends Area3D
5 | ## 3D area intended to detect combat interactions.
6 | ##
7 | ## The hitbox node doesn't provide much functionality out of the box.
8 | ## It serves as a template you can expand upon through the use of [FrayHitboxAttribute]
9 |
10 | ## Emitted when the received [kbd]hitbox[/kbd] enters this hitbox. Requires monitoring to be set to [code]true[/code].
11 | signal hitbox_entered(hitbox: FrayHitbox3D)
12 |
13 | ## Emitted when the received [kbd]hitbox[/kbd] exits this hitbox. Requires monitoring to be set to [code]true[/code].
14 | signal hitbox_exited(hitbox: FrayHitbox3D)
15 |
16 | ## If true then hitboxes that share the same source as this one will still be detected
17 | @export var detect_source_hitboxes: bool = false
18 |
19 | ## The [FrayHitboxAttribute] assigned to this hitbox
20 | @export var attribute: FrayHitboxAttribute:
21 | set(value):
22 | attribute = value
23 |
24 | if attribute != null:
25 | _update_collision_colors()
26 |
27 | update_configuration_warnings()
28 |
29 | ## Source of this hitbox.
30 | ## Hitboxes with the same source will not detect one another unless [member detect_source_hitboxes] is enabled.
31 | var source: Object = null
32 |
33 | var _hitbox_exceptions: Array[FrayHitbox3D]
34 | var _source_exceptions: Array[Object]
35 | var _debug_meshes: Array[MeshInstance3D]
36 |
37 |
38 | func _ready() -> void:
39 | if Engine.is_editor_hint():
40 | child_entered_tree.connect(
41 | func(node: Node):
42 | if node is CollisionShape3D:
43 | _update_collision_colors()
44 | )
45 | return
46 |
47 | area_entered.connect(_on_area_entered)
48 | area_exited.connect(_on_area_exited)
49 |
50 |
51 | func _get_configuration_warnings() -> PackedStringArray:
52 | var warnings: PackedStringArray = []
53 | if attribute == null:
54 | warnings.append("Hitboxes without attribute are effectively just Area2Ds. Consider giving this node a FrayHitboxAttribute resource.")
55 | elif not attribute.get_script().is_tool():
56 | warnings.append("Custom attribute must be a tool script to avoid editor errors. Consider adding the @tool annotation to the top of the script.")
57 | return warnings
58 |
59 | ## Returns a list of intersecting [FrayHitbox3D]s.
60 | func get_overlapping_hitboxes() -> Array[FrayHitbox3D]:
61 | var hitboxes: Array[FrayHitbox3D]
62 | for area in get_overlapping_areas():
63 | if can_detect(area):
64 | hitboxes.append(area)
65 | return hitboxes
66 |
67 | ## Adds a [kbd]hitbox[/kbd] to a list of hitboxes this hitbox can't detect
68 | func add_hitbox_exception_with(hitbox: FrayHitbox3D) -> void:
69 | if hitbox is FrayHitbox3D and not _hitbox_exceptions.has(hitbox):
70 | _hitbox_exceptions.append(hitbox)
71 |
72 | ## Removes a [kbd]hitbox[/kbd] from a list of hitboxes this hitbox can't detect
73 | func remove_hitbox_exception_with(hitbox: FrayHitbox3D) -> void:
74 | if _hitbox_exceptions.has(hitbox):
75 | _hitbox_exceptions.erase(hitbox)
76 |
77 | ## Adds a source [kbd]object[/kbd] to a list of sources whose hitboxes this hitbox can't detect
78 | func add_source_exception_with(object: Object) -> void:
79 | if not _source_exceptions.has(object):
80 | _source_exceptions.append(object)
81 |
82 | ## Removes a source [kbd]object[/kbd] to a list of sources whose hitboxes this hitbox can't detect
83 | func remove_source_exception_with(object: Object) -> void:
84 | if _source_exceptions.has(object):
85 | _source_exceptions.erase(object)
86 |
87 | ## Activates this hitbox allowing it to monitor and be monitored.
88 | func activate() -> void:
89 | monitorable = true
90 | monitoring = true
91 | show()
92 |
93 | ## Deactivates this hitobx preventing it from monitoring and being monitored.
94 | func deactivate() -> void:
95 | monitorable = false
96 | monitoring = false
97 | hide()
98 |
99 | ## Returns [code]true[/code] if this hitbox is able to detect the given [kbd]hitbox[/kbd].
100 | ## [br]
101 | ## A hitbox can not detect another hitbox if there is a source or hitbox exception
102 | ## or if the set hitbox attribute does not allow interaction with the given hitbox.
103 | func can_detect(hitbox: FrayHitbox3D) -> bool:
104 | return (
105 | not _hitbox_exceptions.has(hitbox)
106 | and not _source_exceptions.has(hitbox.source)
107 | and (detect_source_hitboxes or source == null or hitbox.source != source)
108 | and attribute.allows_detection_of(hitbox.attribute) if attribute != null else true
109 | )
110 |
111 |
112 | func _update_collision_colors() -> void:
113 | # 3D debug colors are planned for Godot 4.x
114 | # I was going to do my own implementation but since it's
115 | # not that important i'll leave it for now
116 | pass
117 |
118 |
119 | func _on_area_entered(area: Area3D) -> void:
120 | if area is FrayHitbox3D and can_detect(area):
121 | hitbox_entered.emit(area)
122 |
123 |
124 | func _on_area_exited(area: Area3D) -> void:
125 | if area is FrayHitbox3D and can_detect(area):
126 | hitbox_exited.emit(area)
127 |
--------------------------------------------------------------------------------
/src/hit/hitbox_attribute.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | @icon("res://addons/fray/assets/icons/hit_attribute.svg")
3 | class_name FrayHitboxAttribute
4 | extends Resource
5 | ## Abstract data class used to define hitbox attribute.
6 |
7 | ## Returns the color a hitbox with this attribute should be.
8 | func get_color() -> Color:
9 | return _get_color_impl()
10 |
11 | ## Returns true if a hitbox with this attribute should allow detection of the given attribute.
12 | func allows_detection_of(attribute: FrayHitboxAttribute) -> bool:
13 | return _allows_detection_of_impl(attribute)
14 |
15 | ## [code]Virtual method[/code] used to implement [method get_color].
16 | ## Currently this does nothing for [FrayHitbox3D].
17 | func _get_color_impl() -> Color:
18 | return Color(0, 0, 0, .5)
19 |
20 | ## [code]Virtual method[/code] used to implement [method allows_detection_of] method
21 | func _allows_detection_of_impl(attribute: FrayHitboxAttribute) -> bool:
22 | return true
23 |
--------------------------------------------------------------------------------
/src/input/controller.gd:
--------------------------------------------------------------------------------
1 | @icon("res://addons/fray/assets/icons/controller.svg")
2 | class_name FrayController
3 | extends Node
4 | ## A node representing a controller
5 | ##
6 | ## This node is a helper node which wrapper around `FrayInput` input checks.
7 | ## It can be used to decouple entities from the inputs they check in a way that is accesible from the node tree.
8 | ## If an entity needs to respond to inputs from another device you only need to change the controller's [member device] value.
9 | ## It is also possible to hand the controller off to an AI by using virtual devices.
10 |
11 | ## The ID of the device to check for
12 | @export var device: int = _FrayInput.DEVICE_KBM_JOY1
13 |
14 | ## If [code]true[/code] then all input checks will return false.
15 | @export var disabled: bool = false
16 |
17 | @onready var _fray_input: Node = get_node("/root/FrayInput")
18 |
19 | func _ready() -> void:
20 | if _fray_input == null:
21 | push_error("Failed to access FrayInput singleton. Fray plugin may not be enabled.")
22 | return
23 |
24 | ## Returns true if this controller is connected
25 | func is_device_connected() -> bool:
26 | return _fray_input.is_device_connected(device)
27 |
28 | ## Returns true if an input is being pressed.
29 | func is_pressed(input: StringName) -> bool:
30 | return not disabled and is_device_connected() and _fray_input.is_pressed(input, device)
31 |
32 | ## Returns true if any of the inputs given are being pressed
33 | func is_any_pressed(inputs: PackedStringArray) -> bool:
34 | return not disabled and is_device_connected() and _fray_input.is_any_pressed(inputs, device)
35 |
36 | ## Returns true when a user starts pressing the input,
37 | ## meaning it's true only on the frame the user pressed down the input.
38 | func is_just_pressed(input: StringName) -> bool:
39 | return not disabled and is_device_connected() and _fray_input.is_just_pressed(input, device)
40 |
41 | ## Returns true if input was physically pressed
42 | ## meaning it is only true if the press was not trigerred virtually.
43 | func is_just_pressed_real(input: StringName) -> bool:
44 | return not disabled and is_device_connected() and _fray_input.is_just_pressed_real(input, device)
45 |
46 | ## Returns true when the user stops pressing the input,
47 | ## meaning it's true only on the frame that the user released the button.
48 | func is_just_released(input: StringName) -> bool:
49 | return not disabled and is_device_connected() and _fray_input.is_just_released(input, device)
50 |
51 | ## Returns a value between 0 and 1 representing the intensity of an input.
52 | ## If the input has no range of strngth a discrete value of 0 or 1 will be returned.
53 | func get_strength(input: StringName) -> float:
54 | return _fray_input.get_strength(input, device) if is_device_connected() and not disabled else 0.0
55 |
56 | ## Get axis input by specifiying two input ids, one negative and one positive.
57 | func get_axis(negative_input: StringName, positive_input: StringName) -> float:
58 | return _fray_input.get_axis(negative_input, positive_input, device) if is_device_connected() and not disabled else 0.0
59 |
--------------------------------------------------------------------------------
/src/input/device/binds/input_bind.gd:
--------------------------------------------------------------------------------
1 | class_name FrayInputBind
2 | extends Resource
3 | ## Abstract base class for all input binds
4 | ##
5 | ## An input bind is used to map physical device presses to inputs fray input names.
6 |
7 |
8 | ## Returns true if the bind is pressed
9 | func is_pressed(device: int = 0) -> bool:
10 | return _is_pressed_impl(device)
11 |
12 | ## Returns true if this bind is equal to the given bind
13 | func equals(input_bind: Resource) -> bool:
14 | return _equals_impl(input_bind)
15 |
16 | ## Returns a value between 0 and 1 representing the intensity of an input.
17 | func get_strength(device: int = 0) -> float:
18 | return _get_strength_impl()
19 |
20 | ## [code]Abstract method[/code] used to implement a bind's [method is_pressed] method.
21 | func _is_pressed_impl(device: int = 0) -> bool:
22 | assert(false, "Method not implemented")
23 | return false
24 |
25 | ## [code]Virtual method[/code] used to implement [method equals] method
26 | func _equals_impl(input_bind: Resource) -> bool:
27 | return input_bind is FrayInputBind
28 |
29 | ## [code]Virtual method[/code] used to implement [method get_strength] method
30 | func _get_strength_impl(device: int = 0) -> float:
31 | return float(is_pressed(device))
32 |
--------------------------------------------------------------------------------
/src/input/device/binds/input_bind_action.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | class_name FrayInputBindAction
3 | extends "input_bind_simple.gd"
4 | ## Action input bind
5 | ##
6 | ## Bind that makes use of godot's actions
7 |
8 | ## Action name
9 | var action: StringName
10 |
11 |
12 | func _init(action_name: StringName = "") -> void:
13 | action = action_name
14 |
15 |
16 | func _is_pressed_impl(_device: int = 0) -> bool:
17 | if not InputMap.has_action(action):
18 | push_warning("Action '%s' does not exist in Godot InputMap" % action)
19 | return false
20 | return Input.is_action_pressed(action)
21 |
22 |
23 | func _equals_impl(input_bind: Resource) -> bool:
24 | return (
25 | super(input_bind)
26 | and action == input_bind.action)
27 |
28 |
29 | func _get_strength_impl(_device: int = 0) -> float:
30 | return Input.get_action_strength(action)
31 |
--------------------------------------------------------------------------------
/src/input/device/binds/input_bind_fray_action.gd:
--------------------------------------------------------------------------------
1 | class_name FrayInputBindFrayAction
2 | extends "input_bind.gd"
3 | ## Fray action input bind
4 | ##
5 | ## Bind that makes use of simple binds in a way that mimic's Godot's actions.
6 |
7 | var _binds: Array[FrayInputBindSimple]
8 |
9 | func _init(binds: Array[FrayInputBindSimple] = []) -> void:
10 | for bind in binds:
11 | add_bind(bind)
12 |
13 |
14 | func _is_pressed_impl(device: int = 0) -> bool:
15 | for bind in _binds:
16 | if bind.is_pressed(device):
17 | return true
18 | return false
19 |
20 | ## Adds bind to this action.
21 | func add_bind(simple_bind: FrayInputBindSimple) -> void:
22 | _binds.append(simple_bind)
23 |
24 | ## Erases bind from this action.
25 | func erase_bind(simple_bind: FrayInputBindSimple) -> void:
26 | _binds.erase(simple_bind)
27 |
--------------------------------------------------------------------------------
/src/input/device/binds/input_bind_joy_axis.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | class_name FrayInputBindJoyAxis
3 | extends "input_bind_simple.gd"
4 | ## Joy axis input bind
5 |
6 | ## Joy axis identifier. See JoyStickList
7 | @export var axis: int
8 |
9 | ## Determines whether to check the positive or negative side of the axis
10 | @export var use_positive_axis: bool
11 |
12 | ## Joystick deadzone
13 | @export var deadzone: float:
14 | set(value):
15 | deadzone = clamp(value, 0, 1)
16 |
17 | func _init(joys_axis: int = -1, joy_use_positive_axis: bool = true, joy_deadzone: float = .5) -> void:
18 | axis = joys_axis
19 | deadzone = joy_deadzone
20 | use_positive_axis = joy_use_positive_axis
21 |
22 |
23 | func _is_pressed_impl(device: int = 0) -> bool:
24 | var joy_axis := Input.get_joy_axis(device, axis)
25 | var is_positive_dir: bool = sign(joy_axis) == 1
26 |
27 | if use_positive_axis != is_positive_dir:
28 | return false
29 |
30 | return abs(joy_axis) >= deadzone
31 |
32 |
33 | func _equals_impl(input_bind: Resource) -> bool:
34 | return (
35 | super(input_bind)
36 | and axis == input_bind.axis
37 | and use_positive_axis == input_bind.use_positive_axis)
38 |
39 |
40 | func _get_strength_impl(device: int = 0) -> float:
41 | var joy_axis := Input.get_joy_axis(device, axis)
42 | var is_positive_dir: bool = sign(joy_axis) == 1
43 |
44 | if abs(joy_axis) < deadzone or use_positive_axis != is_positive_dir:
45 | return 0.0
46 |
47 | return abs(joy_axis)
48 |
--------------------------------------------------------------------------------
/src/input/device/binds/input_bind_joy_button.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | class_name FrayInputBindJoyButton
3 | extends "input_bind_simple.gd"
4 | ## Joystick input bind
5 |
6 |
7 | ## Button identifier. One of the JoyStickList button constants
8 | @export var button: int
9 |
10 | func _init(joystick_button: int = -1) -> void:
11 | button = joystick_button
12 |
13 |
14 | func _is_pressed_impl(device: int = 0) -> bool:
15 | return Input.is_joy_button_pressed(device, button)
16 |
17 | func _equals_impl(input_bind: Resource) -> bool:
18 | return (
19 | super(input_bind)
20 | and button == input_bind.button)
21 |
--------------------------------------------------------------------------------
/src/input/device/binds/input_bind_key.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | class_name FrayInputBindKey
3 | extends "input_bind_simple.gd"
4 | ## Keyboard input bind
5 |
6 | ## The key scancode, which corresponds to one of the KeyList constants.
7 | @export var key: int
8 |
9 | func _init(keyboard_key: int = -1) -> void:
10 | key = keyboard_key
11 |
12 |
13 | func _is_pressed_impl(_device: int = 0) -> bool:
14 | return Input.is_key_pressed(key)
15 |
16 |
17 | func _equals_impl(input_bind: Resource) -> bool:
18 | return (
19 | super(input_bind)
20 | and key == input_bind.key)
21 |
--------------------------------------------------------------------------------
/src/input/device/binds/input_bind_mouse_button.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | class_name FrayInputBindMouseButton
3 | extends "input_bind_simple.gd"
4 | ## Mouse input bind
5 |
6 | ## The mouse button identifier, one of the ButtonList buttons or button wheel constants.
7 | @export var button: int
8 |
9 | func _init(mouse_button: int = -1) -> void:
10 | button = mouse_button
11 |
12 |
13 | func _is_pressed_impl(_device: int = 0) -> bool:
14 | return Input.is_mouse_button_pressed(button)
15 |
16 |
17 | func _equals_impl(input_bind: Resource) -> bool:
18 | return (
19 | super(input_bind)
20 | and button == input_bind.button)
21 |
--------------------------------------------------------------------------------
/src/input/device/binds/input_bind_simple.gd:
--------------------------------------------------------------------------------
1 | class_name FrayInputBindSimple
2 | extends "input_bind.gd"
3 | ## Abstract base class representing all simple inputs
4 |
--------------------------------------------------------------------------------
/src/input/device/composites/combination_input.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | class_name FrayCombinationInput
3 | extends FrayCompositeInput
4 | ## A composite input used to create combination inputs
5 | ##
6 | ## A combination will be considered as press when
7 | ## all components are pressed according to the mode.
8 |
9 | enum Mode {
10 | SYNC, ## Components must all be pressed at the same time.
11 | ASYNC, ## Components can be pressed at any time so long as they are all pressed.
12 | ORDERED, ## Like asynchronous but the presses must occur in order.
13 | }
14 |
15 | ## Determines press condition necessary to trigger combination
16 | var mode: Mode = Mode.SYNC
17 |
18 | ## Returns a builder instance
19 | static func builder() -> Builder:
20 | return Builder.new()
21 |
22 | func _is_pressed_impl(device: int) -> bool:
23 | match mode:
24 | Mode.SYNC:
25 | return _is_combination_quick_enough(device)
26 | Mode.ASYNC:
27 | return _is_all_components_pressed(device)
28 | Mode.ORDERED:
29 | return _is_combination_in_order(device)
30 | _:
31 | push_error("Failed to check combination. Unknown mode '%d'" % mode)
32 |
33 | return false
34 |
35 |
36 | func _decompose_impl(device: int) -> Array[StringName]:
37 | # Returns all components decomposed and joined
38 | var binds: Array[StringName]
39 | for component in _components:
40 | binds.append_array(component.decompose(device))
41 | return binds
42 |
43 |
44 | # Returns: InputState[]
45 | func _get_decomposed_states(device: int) -> Array:
46 | var decomposed_states := []
47 | for bind in decompose(device):
48 | decomposed_states.append(get_bind_state(bind, device))
49 | return decomposed_states
50 |
51 |
52 | func _is_all_components_pressed(device: int) -> bool:
53 | for component in _components:
54 | if not component.is_pressed(device):
55 | return false
56 | return true
57 |
58 |
59 | func _is_combination_quick_enough(device: int, tolerance: float = 10) -> bool:
60 | var decomposed_states := _get_decomposed_states(device)
61 | var avg_difference := 0
62 |
63 | for i in len(decomposed_states):
64 | if i > 0:
65 | var input1: FrayInputState = decomposed_states[i]
66 | var input2: FrayInputState = decomposed_states[i - 1]
67 |
68 | if not input1.is_pressed or not input2.is_pressed:
69 | return false
70 |
71 | avg_difference += abs(input1.time_pressed - input2.time_pressed)
72 |
73 | avg_difference /= float(decomposed_states.size())
74 | return avg_difference <= tolerance
75 |
76 |
77 | func _is_combination_in_order(device: int, tolerance: float = 10) -> bool:
78 | var decomposed_states := _get_decomposed_states(device)
79 |
80 | for i in len(decomposed_states):
81 | if i > 0:
82 | var input1: FrayInputState = decomposed_states[i]
83 | var input2: FrayInputState = decomposed_states[i - 1]
84 |
85 | if not input1.is_pressed or not input2.is_pressed:
86 | return false
87 |
88 | if input2.time_pressed - tolerance > input1.time_pressed:
89 | return false
90 |
91 | return true
92 |
93 |
94 | class Builder:
95 | extends FrayCompositeInput.Builder
96 |
97 | func _init() -> void:
98 | _composite_input = FrayCombinationInput.new()
99 |
100 | ## Builds the composite input.
101 | ## [br]
102 | ## Returns a reference to the newly built combination input.
103 | func build() -> FrayCombinationInput:
104 | return _composite_input
105 |
106 | ## Sets the combination to async mode.
107 | ## [br]
108 | ## Returns a reference to the newly built combination input.
109 | func mode_async() -> Builder:
110 | _composite_input.mode = FrayCombinationInput.Mode.ASYNC
111 | return self
112 |
113 | ## Sets the combination to sync mode.
114 | ## [br]
115 | ## Returns a reference to the newly built combination input.
116 | func mode_sync() -> Builder:
117 | _composite_input.mode = FrayCombinationInput.Mode.SYNC
118 | return self
119 |
120 | ## Sets the combination to ordered mode.
121 | ## [br]
122 | ## Returns a reference to the newly built combination input.
123 | func mode_ordered() -> Builder:
124 | _composite_input.mode = FrayCombinationInput.Mode.ORDERED
125 | return self
126 |
127 | func add_component(composite_input: FrayCompositeInput) -> Builder:
128 | return super(composite_input)
129 |
130 | func add_component_simple(bind: StringName) -> Builder:
131 | return super(bind)
132 |
133 | func is_virtual(value: bool = true) -> Builder:
134 | return super(value)
135 |
136 | func priority(value: int) -> Builder:
137 | return super(value)
--------------------------------------------------------------------------------
/src/input/device/composites/conditional_input.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | class_name FrayConditionalInput
3 | extends FrayCompositeInput
4 |
5 | ## A composite input used to create conditional inputs
6 | ##
7 | ## @desc:
8 | ## Returns whether a specific component is pressed based on a string condition.
9 | ## Useful for creating directional inputs which change based on what side a
10 | ## combatant stands on as is seen in many 2D fighting games.
11 | ##
12 | ## If no condition is true then the input will default to checking the first component.
13 |
14 | # Type: Array[func(device: int) -> bool]; index=component_index
15 | var _conditions: Array[Callable]
16 |
17 |
18 | ## Returns a builder instance.
19 | static func builder() -> Builder:
20 | return Builder.new()
21 |
22 |
23 | ## Sets the condition associated with a given component.
24 | ## [br]
25 | ## [kbd]component_index[/kbd] is the index of the component based on the order added.
26 | ## [br]
27 | ## [kbd]condition[/kbd] is a function of type func(device: int) -> bool.
28 | func set_condition(component_index: int, condition: Callable) -> void:
29 | if component_index == 0:
30 | push_warning(
31 | "The first component is treated as the default input. Condition will be ignored"
32 | )
33 | elif component_index >= 1 and component_index < _components.size():
34 | _conditions[component_index - 1] = condition
35 | else:
36 | push_warning("Failed to set condition on input. Given index out of range")
37 |
38 |
39 | func add_component(component: FrayCompositeInput) -> void:
40 | super(component)
41 |
42 | if get_component_count() > 1:
43 | _conditions.append(Callable())
44 |
45 |
46 | func _is_pressed_impl(device: int) -> bool:
47 | if _components.is_empty():
48 | push_warning("Conditional input has no components")
49 | return false
50 |
51 | return _get_active_component(device).is_pressed(device)
52 |
53 |
54 | func _decompose_impl(device: int) -> Array[StringName]:
55 | # Returns the first component with a true condition. Defaults to component at index 0
56 |
57 | if _components.is_empty():
58 | return []
59 |
60 | return _get_active_component(device).decompose(device)
61 |
62 |
63 | func _get_active_component(device: int) -> FrayCompositeInput:
64 | var comp: FrayCompositeInput = _components[0]
65 |
66 | for i in len(_conditions):
67 | var condition := _conditions[i]
68 |
69 | if condition.is_null():
70 | push_error(
71 | "Failed to check condition for component at index %d. Condition not set." % [i + 1]
72 | )
73 | return comp
74 |
75 | if _conditions[i].call(device):
76 | comp = _components[i + 1]
77 | break
78 | return comp
79 |
80 |
81 | class Builder:
82 | extends FrayCompositeInput.Builder
83 |
84 | func _init() -> void:
85 | _composite_input = FrayConditionalInput.new()
86 |
87 | ## Builds the composite input
88 | ## [br]
89 | ## Returns a reference to the newly built conditional input.
90 | func build() -> FrayConditionalInput:
91 | return _composite_input
92 |
93 | ## Sets the condition of the previously added component.
94 | ## Will do nothing for the first component added as this component is treated as default.
95 | ## [br]
96 | ## Returns a reference to this builder.
97 | func use_condition(condition: Callable) -> Builder:
98 | var component_count := _composite_input.get_component_count()
99 |
100 | if component_count == 0:
101 | push_warning("No components have been added. Condition will be ignored.")
102 | elif component_count == 1:
103 | push_warning("The first component is treated as default. Condition will be ignored.")
104 | else:
105 | _composite_input.set_condition(component_count - 1, condition)
106 |
107 | return self
108 |
109 | func add_component(composite_input: FrayCompositeInput) -> Builder:
110 | return super(composite_input)
111 |
112 | func add_component_simple(bind: StringName) -> Builder:
113 | return super(bind)
114 |
115 | func is_virtual(value: bool = true) -> Builder:
116 | return super(value)
117 |
118 | func priority(value: int) -> Builder:
119 | return super(value)
--------------------------------------------------------------------------------
/src/input/device/composites/group_input.gd:
--------------------------------------------------------------------------------
1 | class_name FrayGroupInput
2 | extends FrayCompositeInput
3 | ## A composite input used to create group inputs
4 | ##
5 | ## A group will be considered press when the
6 | ## minimum number of components in the group is pressed.
7 |
8 | ## The minimum number of components that must be pressed for the input to
9 | ## be considered as pressed.
10 | @export var min_pressed: int = 1:
11 | set(value):
12 | min_pressed = max(1, value)
13 |
14 | # Note: I really don't like the composite inputs maintaing state. They're really supposed to
15 | # just peek at the fray input state and determine if they're considered pressed.
16 | # But this is the only way I could think to handle their decomposition.
17 | # Type: Dictionary
18 | var _last_inputs_by_device: Dictionary
19 |
20 |
21 | ## Returns a builder instance.
22 | static func builder() -> Builder:
23 | return Builder.new()
24 |
25 |
26 | func _is_pressed_impl(device: int) -> bool:
27 | var press_count := 0
28 | var last_inputs: Array[StringName] = []
29 |
30 | for component in _components:
31 | if component.is_pressed(device):
32 | last_inputs.append_array(component.decompose(device))
33 | press_count += 1
34 |
35 | if press_count >= min(min_pressed, _components.size()):
36 | _last_inputs_by_device[device] = last_inputs
37 | return true
38 |
39 | return false
40 |
41 |
42 | func _decompose_impl(device: int) -> Array[StringName]:
43 | return _last_inputs_by_device[device] if _last_inputs_by_device.has(device) else []
44 |
45 |
46 | class Builder:
47 | extends FrayCompositeInput.Builder
48 | ## [FrayGroupInput] builder.
49 |
50 | func _init() -> void:
51 | _composite_input = FrayGroupInput.new()
52 |
53 | ## Builds the composite input
54 | ## [br]
55 | ## Returns a reference to the newly built composite input.
56 | func build() -> FrayGroupInput:
57 | return _composite_input
58 |
59 | ## Sets the minimum number of components that must be pressed.
60 | ## [br]
61 | ## Returns a reference to this builder.
62 | func min_pressed(value: int) -> Builder:
63 | _composite_input.min_pressed = value
64 | return self
65 |
66 | func add_component(composite_input: FrayCompositeInput) -> Builder:
67 | return super(composite_input)
68 |
69 | func add_component_simple(bind: StringName) -> Builder:
70 | return super(bind)
71 |
72 | func is_virtual(value: bool = true) -> Builder:
73 | return super(value)
74 |
75 | func priority(value: int) -> Builder:
76 | return super(value)
--------------------------------------------------------------------------------
/src/input/device/composites/simple_input.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | class_name FraySimpleInput
3 | extends FrayCompositeInput
4 | ## A composite input used as a wrapper around input binds
5 | ##
6 | ## Simple inputs do nothing with thier components and will ignore them.
7 | ## They are considered pressed when their bind is pressed.
8 | ## Simple inputs are intended to be the 'leaf' that ends any input composition.
9 |
10 | ## Name of the bind associated with this input
11 | var bind: StringName
12 |
13 |
14 | func _is_pressed_impl(device: int) -> bool:
15 | return get_bind_state(bind, device).is_pressed
16 |
17 |
18 | ## Returns a builder instance.
19 | static func builder() -> Builder:
20 | return Builder.new()
21 |
22 |
23 | ## Returns a simple input using the given [kbd]bind[/kbd].
24 | ## [br]
25 | ## Shorthand for [code]builder().bind(bind).build()[/code]
26 | static func from_bind(bind: StringName) -> FraySimpleInput:
27 | return builder().bind(bind).build()
28 |
29 |
30 | func _decompose_impl(device: int) -> Array[StringName]:
31 | return [bind]
32 |
33 |
34 | class Builder:
35 | extends FrayCompositeInput.Builder
36 | ## [FraySimpleInput] builder.
37 |
38 | func _init() -> void:
39 | _composite_input = FraySimpleInput.new()
40 |
41 | ## Builds the composite input
42 | ## [br]
43 | ## Returns a reference to the newly built simple input.
44 | func build() -> FraySimpleInput:
45 | return _composite_input
46 |
47 | ## Adds a bind to this simple input.
48 | ## [br]
49 | ## Returns a reference to this ComponentBuilder.
50 | func bind(bind_name: StringName) -> Builder:
51 | _composite_input.bind = bind_name
52 | return self
53 |
--------------------------------------------------------------------------------
/src/input/device/device_state.gd:
--------------------------------------------------------------------------------
1 | class_name FrayDeviceState
2 | extends RefCounted
3 | ## Used by [_FrayInput] to track device state
4 |
5 | # Type: Dictionary
6 | var _input_state_by_name: Dictionary
7 | var _is_valid := true
8 |
9 |
10 | ## Returns [code]true[/code] if the input state is still valid.
11 | func is_valid() -> bool:
12 | return _is_valid
13 |
14 |
15 | ## Invalidates this input state resulting in it being removed from the [_FrayInput] singleton.
16 | func invalidate() -> void:
17 | _is_valid = false
18 |
19 |
20 | ## Returns an array containing the names of every pressed input in this device state.
21 | func get_pressed_inputs() -> PackedStringArray:
22 | var pressed_inputs: PackedStringArray
23 | for input in _input_state_by_name:
24 | if _input_state_by_name[input].pressed:
25 | pressed_inputs.append(input)
26 | return pressed_inputs
27 |
28 |
29 | ## Returns an array containing the names of every unpressed input in this device state.
30 | func get_unpressed_inputs() -> PackedStringArray:
31 | var unpressed_inputs: PackedStringArray
32 | for input in _input_state_by_name:
33 | if not _input_state_by_name[input].pressed:
34 | unpressed_inputs.append(input)
35 | return unpressed_inputs
36 |
37 |
38 | ## Returns the names of all inputs tracked by this device.
39 | func get_all_inputs() -> PackedStringArray:
40 | return PackedStringArray(_input_state_by_name.keys())
41 |
42 |
43 | ## Returns the input state of an input associated with a given [kbd]input_name[\kbd],
44 | ## if it exists.
45 | func get_input_state(input_name: StringName) -> FrayInputState:
46 | if _input_state_by_name.has(input_name):
47 | return _input_state_by_name[input_name]
48 | return register_input_state(input_name)
49 |
50 |
51 | ## Creates a new input state for the given [kbd]input_name[\kbd].
52 | func register_input_state(input_name: StringName) -> FrayInputState:
53 | var input_state := FrayInputState.new(input_name)
54 | _input_state_by_name[input_name] = input_state
55 | return input_state
56 |
57 |
58 | ## Flags all [kbd]inputs[/kbd] given as being used by given [kbd]composite[/kbd].
59 | func flag_inputs_use_in_composite(composite: StringName, inputs: PackedStringArray) -> void:
60 | for input in inputs:
61 | if _input_state_by_name.has(input):
62 | _input_state_by_name[input].composites_used_in[composite] = true
63 |
64 |
65 | ## Unflags all [kbd]inputs[/kbd] given as being used by given [kbd]composite[/kbd].
66 | func unflag_inputs_use_in_composite(composite: StringName, inputs: PackedStringArray) -> void:
67 | for input in inputs:
68 | if _input_state_by_name.has(input):
69 | _input_state_by_name[input].composites_used_in.erase(composite)
70 |
71 |
72 | ## Sets all [kbd]inputs[/kbd] as distinct.
73 | func set_inputs_as_distinct(inputs: PackedStringArray, ignore_in_comp_check: bool = false) -> void:
74 | for input in inputs:
75 | if (
76 | _input_state_by_name.has(input)
77 | and (ignore_in_comp_check or _input_state_by_name[input].composites_used_in.is_empty())
78 | ):
79 | _input_state_by_name[input].is_distinct = true
80 |
81 |
82 | ## Unsets all [kbd]inputs[/kbd] as distinct.
83 | func unset_inputs_as_distinct(inputs: PackedStringArray) -> void:
84 | for input in inputs:
85 | if _input_state_by_name.has(input):
86 | _input_state_by_name[input].is_distinct = false
87 |
88 |
89 | ## Returns [code]true[/code] if all [kbd]inputs[/kbd] are distinct.
90 | func is_all_distinct(inputs: PackedStringArray) -> bool:
91 | for input in inputs:
92 | if _input_state_by_name.has(input) and _input_state_by_name[input].is_distinct:
93 | return true
94 | return false
95 |
--------------------------------------------------------------------------------
/src/input/device/input_state.gd:
--------------------------------------------------------------------------------
1 | class_name FrayInputState
2 | extends RefCounted
3 | ## Used by FrayInput to track state of individual inputs
4 |
5 | ## Set of composites this input is used in
6 | # Type: Peudo-HashSet
7 | var composites_used_in: Dictionary
8 |
9 | ## Name of this input
10 | var input: StringName
11 |
12 | ## Physics frame this input was pressed
13 | var physics_frame: int = -1
14 |
15 | ## Process frame this input was pressed
16 | var process_frame: int = -1
17 |
18 | ## Time in miliseconds this input was pressed
19 | var time_pressed: int = -1
20 |
21 | ## Press intensity of this input.
22 | ## [br]
23 | ## Will be 0 or 1 for inputs with boolean state.
24 | var strength: float
25 |
26 | ## If [code]true[/code] then the input was pressed.
27 | var is_pressed: bool
28 |
29 | ## If [code]true[/code] then the input was pressed virtually.
30 | var is_virtually_pressed: bool
31 |
32 | ## If [code]true[/code] then the input is considered pressed without any overlapping inputs.
33 | var is_distinct: bool = true
34 |
35 |
36 | func _init(input_name: StringName) -> void:
37 | input = input_name
38 |
39 | ## Presses the input and records the new input state.
40 | func press() -> void:
41 | is_distinct = true
42 | is_pressed = true
43 | physics_frame = Engine.get_physics_frames()
44 | process_frame = Engine.get_process_frames()
45 | time_pressed = Time.get_ticks_msec()
46 |
47 | if strength <= 0:
48 | strength = 1
49 |
50 | func press_virtually() -> void:
51 | press()
52 | is_virtually_pressed = true
53 |
54 | ## Unpresses the input and records the new input state.
55 | func unpress() -> void:
56 | is_pressed = false
57 | physics_frame = Engine.get_physics_frames()
58 | process_frame = Engine.get_process_frames()
59 | strength = 0
60 | composites_used_in.clear()
61 | is_distinct = true
62 |
--------------------------------------------------------------------------------
/src/input/device/virtual_device.gd:
--------------------------------------------------------------------------------
1 | class_name FrayVirtualDevice
2 | extends RefCounted
3 | ## Manually controlled device.
4 | ##
5 | ## A device's whos inputs must be manually controlled through code.
6 |
7 | var _device_state: FrayDeviceState
8 | var _id: int
9 |
10 | func _init(device_state: FrayDeviceState, id: int):
11 | _device_state = device_state
12 | _id = id
13 |
14 | func _notification(what: int) -> void:
15 | if what == NOTIFICATION_PREDELETE:
16 | _device_state.invalidate()
17 |
18 | ## presses given input on virtual device
19 | func press(input: StringName, press_strength: float = 1.0) -> void:
20 | match _device_state.get_input_state(input):
21 | var input_state:
22 | input_state.strength = press_strength
23 | input_state.press()
24 | null:
25 | push_error("Unrecognized input '%s. Failed to press input on virtual device with id '%d'" % [input, _id])
26 | pass
27 | pass
28 |
29 | ## Unpresses given input on virtual device
30 | func unpress(input: StringName) -> void:
31 | match _device_state.get_input_state(input):
32 | var input_state:
33 | input_state.unpress()
34 | null:
35 | push_error("Unrecognized input '%s. Failed to unpress input on virtual device with id '%d'" % [input, _id])
36 | pass
37 | pass
38 |
39 | ## Returns the integer id used by the FrayInput singleton to store
40 | ## the virtual device's device state.
41 | func get_id() -> int:
42 | return _id
43 |
44 | ## Disconnects the virtual device by invalidating the input state whichs removes it from the FrayInput singleton.
45 | ## The input state is automatically invalidated when the virtual device is no longer being referenced.
46 | func unplug() -> void:
47 | _device_state.invalidate()
48 |
--------------------------------------------------------------------------------
/src/input/events/fray_input_event.gd:
--------------------------------------------------------------------------------
1 | class_name FrayInputEvent
2 | extends RefCounted
3 | ## Base class for inputs detected by the FrayInput singleton.
4 |
5 | ## Time in miliseconds that the input was initially pressed
6 | var time_pressed: int = 0
7 |
8 | ## Time in miliseconds that the input was detected. This is recorded when the signal is emitted
9 | var time_detected: int = 0
10 |
11 | ## The physics frame when the input was first pressed
12 | var physics_frame: int = 0
13 |
14 | ## The idle frame when the input was first pressed
15 | var process_frame: int = 0
16 |
17 | ## The ID of the device that caused this event
18 | var device: int = 0
19 |
20 | ## The input's name
21 | var input: StringName = ""
22 |
23 | var _is_echo: bool = false
24 | var _is_pressed: bool = false
25 | var _is_virtually_pressed: bool = false
26 | var _is_distinct: bool = false
27 |
28 |
29 | func _to_string() -> String:
30 | return "{input:%s, pressed:%s, device:%d}" % [input, is_pressed(), device]
31 |
32 | ## If [code]true[/code], the input is being pressed. If false, it is released
33 | func is_pressed() -> bool:
34 | return _is_pressed
35 |
36 | ## If [code]true[/code], this event was triggered by a virtual press.
37 | func is_virtually_pressed() -> bool:
38 | return _is_virtually_pressed
39 |
40 | ## If [code]true[/code], the input has already been detected
41 | func is_echo() -> bool:
42 | return _is_echo
43 |
44 | ## If [code]true[/code], this input is considered to have occured before any other overlapping inputs.
45 | ## If multiple composite inputs which share binds are overlapping then try increasing
46 | ## the more complex input's [member FrayCompositeInput.priority].
47 | func is_distinct() -> bool:
48 | return _is_distinct
49 |
50 | ## Returns the time between two input events in miliseconds.
51 | func get_time_between_msec(fray_input_event: RefCounted, use_time_pressed: bool = false) -> int:
52 | var t1: int = fray_input_event.time_pressed if use_time_pressed else fray_input_event.time_detected
53 | var t2: int = time_pressed if use_time_pressed else time_detected
54 | return int(abs(t1 - t2))
55 |
56 | ## Returns the time between two input events in seconds
57 | func get_time_between_sec(fray_input_event: RefCounted, use_time_pressed: bool = false) -> float:
58 | return get_time_between_msec(fray_input_event, use_time_pressed) / 1000.0
59 |
60 | ## returns how long this input was held in miliseconds
61 | func get_time_held_msec() -> int:
62 | return time_detected - time_pressed
63 |
64 | ## returns how long this input was held in seconds
65 | func get_time_held_sec() -> float:
66 | return get_time_held_msec() / 1000.0
67 |
68 | ## Returns true if input was pressed with no echo
69 | func is_just_pressed() -> bool:
70 | return is_pressed() and not is_echo()
71 |
72 | ## Returns true if input was not virtually pressed
73 | func is_just_pressed_real() -> bool:
74 | return is_just_pressed() and not is_virtually_pressed()
75 |
--------------------------------------------------------------------------------
/src/input/events/fray_input_event_bind.gd:
--------------------------------------------------------------------------------
1 | class_name FrayInputEventBind
2 | extends "fray_input_event.gd"
3 | ## Fray input event type for bind inputs
4 |
5 | ## An array of composite inputs that this bind was a part of when the event was emitted
6 | ## Type: Pesudo-HashSet
7 | var composites_used_in: Dictionary
8 |
9 | ## Returns true if this input press was used as a part of a composite input press
10 | ## when the event was emitted.
11 | func is_used_in_composite() -> bool:
12 | return not composites_used_in.is_empty()
13 |
--------------------------------------------------------------------------------
/src/input/events/fray_input_event_composite.gd:
--------------------------------------------------------------------------------
1 | class_name FrayInputEventComposite
2 | extends "fray_input_event.gd"
3 | ## Fray input event type for composite inputs
4 |
--------------------------------------------------------------------------------
/src/input/sequence/input_requirement.gd:
--------------------------------------------------------------------------------
1 | class_name FrayInputRequirement
2 | extends Resource
3 | ## Used by [FraySequenceBranch] to describe a sequence of inputs.
4 |
5 | ## The name of the input
6 | var input: StringName
7 |
8 | ## The minimum amount of time that the input must be held in milliseconds.
9 | var min_time_held: int
10 |
11 | ## The max delay between this input and the last in milliseconds.
12 | var max_delay: int
13 |
14 | ## Returns [code]true[/code] if the input is a charge input.[br]
15 | ##
16 | ## An input is considered a charge input if its [member min_time_held] is greater than 0.
17 | func is_charge_input() -> bool:
18 | return min_time_held > 0
19 |
--------------------------------------------------------------------------------
/src/input/sequence/sequence_branch.gd:
--------------------------------------------------------------------------------
1 | class_name FraySequenceBranch
2 | extends Resource
3 | ## Contains data on the inputs required for a sequence to be recognized.
4 |
5 | ## Array holding the InputRequirements used to define this sequence.
6 | var input_requirements: Array[FrayInputRequirement]
7 |
8 | ## If true the final input in the sequence is allowed to be triggered by a button release..
9 | ## Search "fighting game negative edge" for more info on the concept
10 | var is_negative_edge_enabled: bool
11 |
12 | ## Returns new builder instance.
13 | static func builder() -> Builder:
14 | return Builder.new()
15 |
16 |
17 | class Builder:
18 | extends RefCounted
19 | ## [FraySequenceBranch] builder.
20 |
21 | var _input_requirements: Array[FrayInputRequirement]
22 | var _is_negative_edge_enabled: bool
23 | var _first_input: FrayInputRequirement
24 |
25 | ## Returns newly constructed sequence branch.
26 | func build() -> FraySequenceBranch:
27 | if _first_input != null:
28 | _input_requirements.push_front(_first_input)
29 |
30 | var branch := FraySequenceBranch.new()
31 | branch.input_requirements = _input_requirements
32 | branch.is_negative_edge_enabled = _is_negative_edge_enabled
33 | return branch
34 |
35 | ## Appends an input requirement to the end of this sequence
36 | ## [br]
37 | ## Returns a reference to this builder.
38 | ## [br][br]
39 | ## [kbd]max_delay[/kbd] is the maximum time in miliseconds between two inputs.
40 | ## A negative delay means that an infinite amount of time is allowed between inputs.
41 | ## This parameter has no effect on the first requirement of a sequence.
42 | ## [br]
43 | ## [kbd]min_time_held[/kbd] is the minimum time in miliseconds that the input is required to be held.
44 | ## Inputs with a non-zero time are considered to be "charged inputs" and will only match with releases, not presses.
45 | func then(input: StringName, max_delay := 200, min_time_held := 0) -> Builder:
46 | var input_requirement := FrayInputRequirement.new()
47 | input_requirement.input = input
48 | input_requirement.max_delay = max_delay
49 | input_requirement.min_time_held = min_time_held
50 | _input_requirements.append(input_requirement)
51 | return self
52 |
53 | ## Sets the first input of this sequence.
54 | ## [br]
55 | ## This is an optional method that works similar to [method then]
56 | ## except it directly sets the first input, it doest not append.
57 | ## It also omits the max_delay parameter since the first input in a sequence ignores the delay.
58 | ## This exists to make the builder read better when chaining calls.
59 | ## [br]
60 | ## [kbd]min_time_held[/kbd] is the minimum time in miliseconds that the input is required to be held.
61 | func first(input: StringName, min_time_held := 0) -> Builder:
62 | _first_input = FrayInputRequirement.new()
63 | _first_input.input = input
64 | _first_input.max_delay = 0
65 | _first_input.min_time_held = min_time_held
66 | return self
67 |
68 | ## Used to neable negative edge.
69 | ##
70 | ## Returns a reference to this sequence builder.
71 | ## [br]
72 | ## If true the final input in the sequence is allowed to be triggered by a button release..
73 | ## Search "fighting game negative edge" for more info on the concept
74 | func enable_negative_edge() -> Builder:
75 | _is_negative_edge_enabled = true
76 | return self
77 |
--------------------------------------------------------------------------------
/src/input/sequence/sequence_tree.gd:
--------------------------------------------------------------------------------
1 | class_name FraySequenceTree
2 | extends Resource
3 | ## Contains a mapping of sequence names to [FraySequenceBranch]
4 | ##
5 | ## A sequence name can be associated with one or more sequence branches.
6 | ## This is to support sequence leneiency where partial or 'dirty' sequences
7 | ## will still be considered valid.
8 |
9 | # Type: Dictionary
10 | # Hint:
11 | var _sequence_branch_by_name: Dictionary
12 |
13 | ## Adds a sequence to list under a given name.
14 | ## [br][br]
15 | ## [kbd]sequence_name[/kbd] is the name of the sequence, a name can be associated with many sequence branches.
16 | ## A sequence can have many branchs which allows support for 'lenient inputs'.
17 | ## These are inputs that do not exactly match the intended sequence.
18 | ## [br][br]
19 | ## [kbd]sequence_branch[/kbd] is a collection of input requirements that define a branch.
20 | func add(sequence_name: StringName, sequence_branch: FraySequenceBranch) -> void:
21 | if not _sequence_branch_by_name.has(sequence_name):
22 | _sequence_branch_by_name[sequence_name] = []
23 | _sequence_branch_by_name[sequence_name].append(sequence_branch)
24 |
25 | ## Removes a sequence branch at given index.
26 | func remove_sequence_branch(sequence_name: StringName, branch_index: int) -> void:
27 | if _sequence_branch_by_name.has(sequence_name):
28 | var sequences: Array = _sequence_branch_by_name[sequence_name]
29 | if sequences.size() < branch_index and branch_index >= 0:
30 | sequences.remove_at(branch_index)
31 | else:
32 | push_error("Index out of range")
33 |
34 | ## Removes all sequence branchs associated with a given sequence.
35 | func remove_sequence_branch_all(sequence_name: StringName) -> void:
36 | if _sequence_branch_by_name.has(sequence_name):
37 | _sequence_branch_by_name[sequence_name].clear()
38 |
39 | ## Removes sequence along with all its branchs.
40 | func remove_sequence(sequence_name: StringName) -> void:
41 | if _sequence_branch_by_name.has(sequence_name):
42 | _sequence_branch_by_name.erase(sequence_name)
43 |
44 | ## Returns the sequence branch at a given index.
45 | func get_sequence_branch(sequence_name: StringName, branch_index: int = 0) -> FraySequenceBranch:
46 | if _sequence_branch_by_name.has(sequence_name):
47 | var sequences: Array = _sequence_branch_by_name[sequence_name]
48 | if sequences.size() < branch_index and branch_index >= 0:
49 | return sequences[branch_index]
50 | else:
51 | push_error("Index out of range")
52 | return null
53 |
54 | ## Returns an array of all sequence branchs associated with the given sequence name.
55 | func get_sequence_branchs(sequence_name: StringName) -> Array:
56 | if _sequence_branch_by_name.has(sequence_name):
57 | return _sequence_branch_by_name[sequence_name]
58 | return []
59 |
60 | ## Returns an array of all sequence names in the list.
61 | func get_sequence_names() -> PackedStringArray:
62 | return PackedStringArray(_sequence_branch_by_name.keys())
63 |
--------------------------------------------------------------------------------
/src/state_mgmt/component/anim_tracker/animator_tracker.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | class_name FrayAnimatorTracker
3 | extends Resource
4 | ## Abstract base class for animator trackers.
5 |
6 | ## Emitted when the observed animation controller reaches the start of the animation.
7 | signal animation_started(animation: StringName)
8 |
9 | ## Emitted when the observed animation controller reaches the end of the animation.
10 | signal animation_finished(animation: StringName)
11 |
12 | ## Emited when the observed animation controller updates the animation progress.
13 | signal animation_updated(animation: StringName, play_position: float)
14 |
15 | ## func(node: [Node]) -> [NodePath]
16 | ## [br]
17 | ## Returns the path from a given node to the tracker property of the observer containing this tracker..
18 | var fn_get_path_from: Callable = Callable()
19 |
20 | ## func(path: [NodePath]) -> [Node]
21 | ## [br]
22 | ## Fetches a node relative to the observer containing this tracker.
23 | var fn_get_node_or_null: Callable = Callable()
24 |
25 |
26 | ## Readies this tracker.
27 | ## [br]
28 | ## This method is only intended to be used by the [FrayAnimationObserver].
29 | func ready() -> void:
30 | _ready_impl()
31 | _add_anim_signals(_get_animation_list_impl())
32 |
33 |
34 | ## Processes this tracker.
35 | ## [br]
36 | ## This method is only intended to be used by the [FrayAnimationObserver].
37 | func process(delta: float) -> void:
38 | _process_impl(delta)
39 |
40 |
41 | ## Physics process this tracker.
42 | ## [br]
43 | ## This method is only intended to be used by the [FrayAnimationObserver].
44 | func physics_process(delta: float) -> void:
45 | _physics_process_impl(delta)
46 |
47 |
48 | ## [code]Virtual method[/code] invoked when the [FrayAnimationObserver] using this tracker is readied.
49 | func _ready_impl() -> void:
50 | pass
51 |
52 |
53 | ## [code]Virtual method[/code] invoked when the tracker is being processed. [kbd]delta[/kbd] is in seconds.
54 | func _process_impl(delta: float) -> void:
55 | pass
56 |
57 |
58 | ## [code]Virtual method[/code] invoked when the tracker is being processed. [kbd]delta[/kbd] is in seconds.
59 | func _physics_process_impl(delta: float) -> void:
60 | pass
61 |
62 |
63 | ## [code]Virtual method[/code] used to report configuration warnings to the [FrayAnimationObserver].
64 | func _get_configuration_warnings_impl() -> PackedStringArray:
65 | return []
66 |
67 |
68 | ## Used to emit an animation started signal. Emits both built-in signal and per-animation user signal.
69 | func emit_anim_started(animation: StringName) -> void:
70 | animation_started.emit(animation)
71 |
72 | if has_user_signal(format_usignal_started(animation)):
73 | emit_signal(format_usignal_started(animation))
74 |
75 |
76 | ## Used to emit an animation finished signal. Emits both built-in signal and per-animation user signal.
77 | func emit_anim_finished(animation: StringName) -> void:
78 | animation_started.emit(animation)
79 |
80 | if has_user_signal(format_usignal_finished(animation)):
81 | emit_signal(format_usignal_finished(animation))
82 |
83 |
84 | ## Used to emit an animation updated signal. Emits both built-in signal and per-animation user signal.
85 | func emit_anim_updated(animation: StringName, play_position: float) -> void:
86 | animation_updated.emit(animation, play_position)
87 |
88 | if has_user_signal(format_usignal_updated(animation)):
89 | emit_signal(format_usignal_updated(animation), play_position)
90 |
91 |
92 | ## Returns the given [kbd]animation[/kbd] string formatted as a 'started' user signal.
93 | func format_usignal_started(animation: StringName) -> StringName:
94 | return "_%s_started" % animation
95 |
96 |
97 | ## Returns the given [kbd]animation[/kbd] string formatted as a 'finished' user signal.
98 | func format_usignal_finished(animation: StringName) -> StringName:
99 | return "_%s_finished" % animation
100 |
101 |
102 | ## Returns the given [kbd]animation[/kbd] string formatted as a 'updated' user signal.
103 | func format_usignal_updated(animation: StringName) -> StringName:
104 | return "_%s_updated" % animation
105 |
106 |
107 | ## [code]Abstract method[/code] used to return the list of animations beloning to the tracked animator.
108 | ## This list is used to initialize the per-animation user signals.
109 | func _get_animation_list_impl() -> PackedStringArray:
110 | assert(false, "Method not implemented")
111 | return []
112 |
113 |
114 | func _add_anim_signals(animation_list: Array[String]) -> void:
115 | for anim_name in animation_list:
116 | add_user_signal(format_usignal_started(anim_name))
117 | add_user_signal(format_usignal_finished(anim_name))
118 | add_user_signal(format_usignal_updated(anim_name))
119 |
--------------------------------------------------------------------------------
/src/state_mgmt/component/anim_tracker/animator_tracker_animated_sprite_2d.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | class_name FrayAnimatorTrackerAnimatedSprite2D
3 | extends FrayAnimatorTracker
4 | ## [AnimatedSprite2D] tracker
5 |
6 | @export_node_path("AnimatedSprite2D") var anim_sprite_path: NodePath
7 |
8 | var _anim_sprite: AnimatedSprite2D = null
9 |
10 | var _prev_frame: int = -1
11 |
12 |
13 | func _ready_impl() -> void:
14 | _anim_sprite = fn_get_node_or_null.call(anim_sprite_path)
15 |
16 | func _get_configuration_warnings_impl() -> PackedStringArray:
17 | var anim_sprite = fn_get_node_or_null.call(anim_sprite_path)
18 |
19 | if anim_sprite == null:
20 | return ["Path to animated sprite not set."]
21 |
22 | return []
23 |
24 |
25 | func _process_impl(delta: float) -> void:
26 | if _anim_sprite.is_playing():
27 | if _anim_sprite.frame != _prev_frame:
28 | var last_frame := _anim_sprite.sprite_frames.get_frame_count(_anim_sprite.animation) - 1
29 |
30 | if _anim_sprite.frame == 0:
31 | emit_anim_started(_anim_sprite.animation)
32 |
33 | emit_anim_updated(_anim_sprite.animation, _anim_sprite.frame)
34 |
35 | if _anim_sprite.frame == last_frame:
36 | emit_anim_finished(_anim_sprite.animation)
37 |
38 | _prev_frame = _anim_sprite.frame
39 |
--------------------------------------------------------------------------------
/src/state_mgmt/component/anim_tracker/animator_tracker_animated_sprite_3d.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | class_name FrayAnimatorTrackerAnimatedSprite3D
3 | extends FrayAnimatorTracker
4 | ## [AnimatedSprite3D] tracker
5 |
6 | @export_node_path("AnimatedSprite3D") var anim_sprite_path: NodePath
7 |
8 | var _anim_sprite: AnimatedSprite3D = null
9 |
10 | var _prev_frame: int = -1
11 |
12 |
13 | func _ready_impl() -> void:
14 | _anim_sprite = fn_get_node_or_null.call(anim_sprite_path)
15 |
16 |
17 | func _process_impl(delta: float) -> void:
18 | if _anim_sprite.is_playing():
19 | if _anim_sprite.frame != _prev_frame:
20 | var last_frame := _anim_sprite.sprite_frames.get_frame_count(_anim_sprite.animation) - 1
21 |
22 | if _anim_sprite.frame == 0:
23 | emit_anim_started(_anim_sprite.animation)
24 |
25 | emit_anim_updated(_anim_sprite.animation, _anim_sprite.frame)
26 |
27 | if _anim_sprite.frame == last_frame:
28 | emit_anim_finished(_anim_sprite.animation)
29 |
30 | _prev_frame = _anim_sprite.frame
31 |
32 |
33 | func _get_configuration_warnings_impl() -> PackedStringArray:
34 | var anim_sprite = fn_get_node_or_null.call(anim_sprite_path)
35 |
36 | if anim_sprite == null:
37 | return ["Path to animated sprite not set."]
38 |
39 | return []
40 |
--------------------------------------------------------------------------------
/src/state_mgmt/component/anim_tracker/animator_tracker_animation_player.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | class_name FrayAnimatorTrackerAnimationPlayer
3 | extends FrayAnimatorTracker
4 | ## [AnimationPlayer] tracker
5 |
6 | @export_node_path("AnimationPlayer") var anim_player_path: NodePath
7 |
8 | var _anim_player: AnimationPlayer
9 |
10 |
11 | func _get_animation_list_impl() -> PackedStringArray:
12 | return _anim_player.get_animation_list()
13 |
14 |
15 | func _get_configuration_warnings_impl() -> PackedStringArray:
16 | var anim_player := fn_get_node_or_null.call(anim_player_path)
17 |
18 | if anim_player == null:
19 | return ["Path to animation player not set."]
20 |
21 | return []
22 |
23 |
24 | func _ready_impl() -> void:
25 | _anim_player = fn_get_node_or_null.call(anim_player_path)
26 |
27 |
28 | func _process_impl(delta: float) -> void:
29 | if _anim_player.is_playing():
30 | if _anim_player.current_animation_position == 0:
31 | emit_anim_started(_anim_player.current_animation)
32 |
33 | emit_anim_updated(_anim_player.current_animation, _anim_player.current_animation_position)
34 |
35 | if _anim_player.current_animation_position + delta >= _anim_player.current_animation_length:
36 | emit_anim_finished(_anim_player.current_animation)
37 |
--------------------------------------------------------------------------------
/src/state_mgmt/component/anim_tracker/animator_tracker_animation_tree.gd:
--------------------------------------------------------------------------------
1 | @tool
2 |
3 | extends FrayAnimatorTracker
4 | ## EXPERIMENTAL [AnimationTree] tracker
5 |
6 | @export_node_path("AnimationTree") var anim_tree_path: NodePath
7 |
8 | var _anim_tree: AnimationTree = null
9 | var _playback_state := _PlaybackState.new()
10 |
11 |
12 | func _ready_impl() -> void:
13 | super()
14 |
15 | _anim_tree = fn_get_node_or_null.call(anim_tree_path)
16 |
17 |
18 | func _process_impl(delta: float) -> void:
19 | if not _anim_tree.tree_root is AnimationNodeStateMachine:
20 | push_warning(
21 | "Due to interface limitations only the AnimationNodeStateMachine tree root is supported for tracking."
22 | )
23 | return
24 |
25 | var playback_path := "parameters/playback"
26 | var anim_node_path := ""
27 | var current_playback_state := _playback_state
28 | var current_playback = _anim_tree.get(playback_path)
29 | var current_node: StringName = current_playback.get_current_node()
30 |
31 | while current_playback != null:
32 | anim_node_path += "%s" % current_node
33 |
34 | if current_playback_state.current_node != current_node:
35 | if not current_playback_state.current_node.is_empty():
36 | emit_anim_finished(anim_node_path)
37 |
38 | current_playback_state.child = null
39 | current_playback_state.current_node = current_node
40 |
41 | emit_anim_started(anim_node_path)
42 |
43 | emit_anim_updated(anim_node_path, current_playback.current_play_position)
44 |
45 | playback_path += "/%s/playback" % current_node
46 | current_playback = _anim_tree.get(playback_path)
47 |
48 | if current_playback:
49 | anim_node_path += "/"
50 | current_playback_state.child = _PlaybackState.new()
51 | current_playback_state = current_playback_state.child
52 | current_node = current_playback.get_current_node()
53 |
54 |
55 | func _get_animation_list_impl() -> PackedStringArray:
56 | var anim_player: AnimationPlayer = _anim_tree.get_node(_anim_tree.anim_player)
57 | return anim_player.get_animation_list()
58 |
59 |
60 | func _get_configuration_warnings_impl() -> PackedStringArray:
61 | var anim_tree := fn_get_node_or_null.call(anim_tree_path)
62 |
63 | if anim_tree == null:
64 | return ["Path to animation tree not set."]
65 |
66 | if not anim_tree.tree_root is AnimationNodeStateMachine:
67 | return [
68 | "Due to interface limitations only the AnimationNodeStateMachine tree root is supported for tracking."
69 | ]
70 |
71 | return []
72 |
73 |
74 | class _PlaybackState:
75 | extends RefCounted
76 |
77 | var current_node: StringName = ""
78 | var child: _PlaybackState = null
79 |
--------------------------------------------------------------------------------
/src/state_mgmt/component/anim_tracker/animator_tracker_tween.gd:
--------------------------------------------------------------------------------
1 | class_name FrayAnimatorTrackerTween
2 | extends FrayAnimatorTracker
3 | ## [Tween] tracker
4 |
5 | var _tween: Tween = null
6 |
7 |
8 | func _get_animation_list_impl() -> PackedStringArray:
9 | return ["tween"]
10 |
11 |
12 | func set_tween(tween: Tween) -> void:
13 | _tween = tween
14 |
15 | emit_anim_started("tween")
16 | tween.finished.connect(_on_Tween_finished)
17 | tween.step_finished.connect(_on_Tween_step_finished)
18 |
19 |
20 | func _on_Tween_finished() -> void:
21 | emit_anim_finished("tween")
22 |
23 |
24 | func _on_Tween_step_finished(idx: int) -> void:
25 | emit_anim_updated("tween", idx)
26 |
--------------------------------------------------------------------------------
/src/state_mgmt/component/animation_observer.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | class_name FrayAnimationObserver
3 | extends Node
4 | ## A node used to observe animation events
5 | ##
6 | ## The animation observer can observe animation events from animators provided by the [FrayAnimatorTracker] resource.
7 | ## These events can be subscribed to per-animation using the included 'usignal' methods.
8 | ## The tracker also has event signals which can be connected to in a way that isn't per-animation.
9 |
10 | ## Used to determine which animator to observe
11 | @export var tracker: FrayAnimatorTracker:
12 | set(value):
13 | tracker = value
14 | tracker.fn_get_path_from = _get_path_from
15 | tracker.fn_get_node_or_null = get_node_or_null
16 |
17 |
18 | func _ready() -> void:
19 | if Engine.is_editor_hint():
20 | return
21 |
22 | tracker.ready()
23 |
24 |
25 | func _process(delta: float) -> void:
26 | if Engine.is_editor_hint():
27 | return
28 |
29 | tracker.process(delta)
30 |
31 |
32 | func _physics_process(delta: float) -> void:
33 | if Engine.is_editor_hint():
34 | return
35 |
36 | tracker.physics_process(delta)
37 |
38 |
39 | func _get_configuration_warnings() -> PackedStringArray:
40 | var warnings: PackedStringArray = []
41 |
42 | if tracker:
43 | warnings += tracker._get_configuration_warnings_impl()
44 |
45 | return warnings
46 |
47 |
48 | ## Returns a user defined signal which is used to connect to the start event of a given animation.
49 | ## [br]
50 | ## Signal excepts a method of type [code]func() -> void[/code]
51 | func usignal_started(animation: String) -> Signal:
52 | return Signal(tracker, tracker.format_usignal_started(animation))
53 |
54 |
55 | ## Returns a user defined signal which is used to connect to the finish event of a given animation.
56 | ## [br]
57 | ## Signal excepts a method of type [code]func() -> void[/code]
58 | func usignal_finished(animation: String) -> Signal:
59 | return Signal(tracker, tracker.format_usignal_finished(animation))
60 |
61 |
62 | ## Returns a user defined signal which is used to connect to the update event of a given animation.
63 | ## [br]
64 | ## Signal excepts a method of type [code]func(play_position: float) -> void[/code]
65 | func usignal_updated(animation: String) -> Signal:
66 | return Signal(tracker, tracker.format_usignal_updated(animation))
67 |
68 |
69 | func _get_path_from(from_node: Node) -> NodePath:
70 | return NodePath(from_node.get_path_to(self))
71 |
--------------------------------------------------------------------------------
/src/state_mgmt/component/buffered_input_advancer.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | @icon("res://addons/fray/assets/icons/buffered_input_advancer.svg")
3 | class_name FrayBufferedInputAdvancer
4 | extends Node
5 | ## A node designed to advance a specified state machine using buffered inputs.
6 | ##
7 | ## This node automatically feeds buffered inputs to the designated state machine to trigger state transitions.
8 | ## When an input is accepted by the state machine, the advancer stops processing new inputs for the current frame.
9 | ## The advancer can be paused to control the timing of input feeding, but note that pausing doesn't affect the input buffer.
10 | ## Inputs can still be buffered during pauses, and they will expire if they exceed the maximum buffer time.
11 | ## This behavior allows you to define the timeframe during which user inputs can trigger state transitions.
12 |
13 | enum AdvanceMode {
14 | IDLE, ## Advance during the idle process
15 | PHYSICS, ## Advance during the physics process
16 | }
17 |
18 | ## If [code]true[/code], the buffer does not attempt to advance by feeding inputs to the state machine.
19 | ## Enabling or disabling this property allows control over when buffered inputs are consumed.
20 | ## This can be useful for managing when a player can 'cancel' an attack using their buffered inputs.
21 | @export var paused: bool = false
22 |
23 | ## The max time an input can exist in the buffer before it is ignored, in seconds.
24 | @export_range(0.0, 5.0, 0.01, "suffix:sec") var max_buffer_time: float = 1.0
25 |
26 | ## Determines the process during which the advancer machine can advance the state machine.
27 | @export var advance_mode: AdvanceMode
28 |
29 | var _state_machine: FrayStateMachine
30 | var _input_buffer: Array[BufferedInput]
31 | var _accepted_input_time_stamp: int
32 |
33 |
34 | func _ready() -> void:
35 | if Engine.is_editor_hint():
36 | return
37 |
38 | _state_machine = get_state_machine()
39 |
40 |
41 | func _process(delta: float) -> void:
42 | if Engine.is_editor_hint():
43 | return
44 |
45 | if advance_mode == AdvanceMode.IDLE:
46 | _advance()
47 |
48 |
49 | func _physics_process(delta: float) -> void:
50 | if Engine.is_editor_hint():
51 | return
52 |
53 | if advance_mode == AdvanceMode.PHYSICS:
54 | _advance()
55 |
56 |
57 | func _get_configuration_warnings() -> PackedStringArray:
58 | if get_state_machine() == null:
59 | return ["This node is expected to be the the child of a FrayStateMachine."]
60 | return []
61 |
62 |
63 | ## Buffers an input press to be processed by the state machine
64 | ##
65 | ## [kbd]input[/kbd] is the name of the input.
66 | ## This is just an identifier used in input transitions.
67 | ## It is not default associated with any actions in godot or inputs in fray.
68 | ##
69 | ## If [kbd]is_presse[/kbd] is true then a pressed input is buffered, else a released input is buffered.
70 | func buffer_press(input: StringName, is_pressed: bool = true) -> void:
71 | _input_buffer.append(BufferedInputPress.new(Time.get_ticks_msec(), input, is_pressed))
72 |
73 |
74 | ## Buffers an input sequence to be processed by the state machine
75 | #
76 | ## [kbd]sequence_name[/kbd] is the name of the sequence.
77 | ## This is just an identifier used in input transitions.
78 | ## It is not default associated with any actions in godot or inputs in fray.
79 | func buffer_sequence(sequence_name: StringName) -> void:
80 | _input_buffer.append(BufferedInputSequence.new(Time.get_ticks_msec(), sequence_name))
81 |
82 |
83 | ## Clears the input buffer
84 | func clear_buffer() -> void:
85 | _input_buffer.clear()
86 |
87 |
88 | ## Returns a shallow copy of the current buffer.
89 | func get_buffer() -> Array[BufferedInput]:
90 | return _input_buffer.duplicate()
91 |
92 |
93 | ## Returns the state machine this component belongs to if it exists.
94 | func get_state_machine() -> FrayStateMachine:
95 | return get_parent() as FrayStateMachine
96 |
97 |
98 | func _advance() -> void:
99 | while not _input_buffer.is_empty() and not paused:
100 | var buffered_input: BufferedInput = _input_buffer.pop_front()
101 | var is_input_within_buffer := (
102 | buffered_input.calc_elapsed_time_msec() <= Fray.sec_to_msec(max_buffer_time)
103 | )
104 | var accepted_input_age_sec := Fray.msec_to_sec(
105 | Time.get_ticks_msec() - _accepted_input_time_stamp
106 | )
107 | var state_machine_input := _create_state_machine_input(
108 | buffered_input, accepted_input_age_sec
109 | )
110 |
111 | if is_input_within_buffer and _state_machine.advance(state_machine_input):
112 | _accepted_input_time_stamp = Time.get_ticks_msec()
113 | break
114 |
115 |
116 | func _create_state_machine_input(
117 | buffered_input: BufferedInput, time_since_last_input: float
118 | ) -> Dictionary:
119 | if buffered_input is BufferedInputPress:
120 | return {
121 | input = buffered_input.input,
122 | is_pressed = buffered_input.is_pressed,
123 | time_since_last_input = time_since_last_input,
124 | time_held = buffered_input.calc_elapsed_time_msec()
125 | }
126 | elif buffered_input is BufferedInputSequence:
127 | return {
128 | sequence = buffered_input.sequence_name,
129 | time_since_last_input = time_since_last_input,
130 | }
131 | return {}
132 |
133 |
134 | class BufferedInput:
135 | extends RefCounted
136 |
137 | var time_stamp: int
138 |
139 | func _init(input_time_stamp: int = 0) -> void:
140 | time_stamp = input_time_stamp
141 |
142 | func calc_elapsed_time_msec() -> int:
143 | return Time.get_ticks_msec() - time_stamp
144 |
145 |
146 | class BufferedInputPress:
147 | extends BufferedInput
148 |
149 | var input: StringName
150 | var is_pressed: bool
151 |
152 | func _init(
153 | input_time_stamp: int = 0, input_name: StringName = "", input_is_pressed: bool = true
154 | ) -> void:
155 | super(input_time_stamp)
156 | input = input_name
157 | is_pressed = input_is_pressed
158 |
159 |
160 | class BufferedInputSequence:
161 | extends BufferedInput
162 |
163 | var sequence_name: StringName
164 |
165 | func _init(input_time_stamp: int = 0, input_sequence_name: StringName = "") -> void:
166 | super(input_time_stamp)
167 | sequence_name = input_sequence_name
168 |
--------------------------------------------------------------------------------
/src/state_mgmt/state/a_star_graph.gd:
--------------------------------------------------------------------------------
1 | extends RefCounted
2 | ## Simple wrapper around [AStar2D] class that stores points as string names.
3 |
4 | # Type: Dictionary
5 | # Hint:
6 | var _point_id_by_node: Dictionary
7 |
8 | # Type: Dictionary
10 | var _node_by_point_id: Dictionary
11 |
12 | var _astar: _CustomAStar
13 | var _astar_point_id := 0
14 | var _travel_path: PackedStringArray
15 | var _travel_index: int
16 |
17 | # func_get_transition_cost: func(StringName, StringName) -> float
18 | func _init(func_get_transition_cost: Callable) -> void:
19 | _astar = _CustomAStar.new(func_get_transition_cost, _get_node_from_id)
20 |
21 | ## Adds point to graph.
22 | func add_point(state_name: StringName) -> void:
23 | _point_id_by_node[state_name] = _astar_point_id
24 | _node_by_point_id[_astar_point_id] = state_name
25 | _astar.add_point(_astar_point_id, Vector2.ZERO)
26 | _astar_point_id += 1
27 |
28 | ## Removes point from graph.
29 | func remove_point(state_name: StringName) -> void:
30 | var point_id: int = _point_id_by_node[state_name]
31 | _astar.remove_point(point_id)
32 | _point_id_by_node.erase(state_name)
33 | _node_by_point_id.erase(point_id)
34 |
35 | ## Renames point on graph.
36 | func rename_point(old_name: StringName, new_name: StringName) -> void:
37 | if _point_id_by_node.has(old_name) and not _point_id_by_node.has(new_name):
38 | var id: int = _point_id_by_node[old_name]
39 |
40 | _point_id_by_node.erase(old_name)
41 | _node_by_point_id.erase(id)
42 |
43 | _point_id_by_node[new_name] = id
44 | _node_by_point_id[id] = new_name
45 |
46 | ## Connects [kbd]from[/kbd] one point [kbd]to[/kbd] another point.
47 | ## [br]
48 | ## if [kbd]bidirectional[/kbd] is true then transitions are allowed in both directions.
49 | func connect_points(from: StringName, to: StringName, bidirectional: bool) -> void:
50 | _astar.connect_points(_point_id_by_node[from], _point_id_by_node[to], bidirectional)
51 |
52 | ## Disconnects [kbd]from[/kbd] one point [kbd]to[/kbd] another point.
53 | ## [br]
54 | ## if [kbd]bidirectional[/kbd] is true then transitions are disconnected in both directions.
55 | func disconnect_points(from: StringName, to: StringName, bidirectional: bool) -> void:
56 | _astar.disconnect_points(_point_id_by_node[from], _point_id_by_node[to], bidirectional)
57 |
58 | ## Determines the optimal travel path [kbd]from[/kbd] one point [kbd]to[/kbd] another point.
59 | func compute_travel_path(from: StringName, to: StringName) -> PackedStringArray:
60 | var id_path := _astar.get_id_path(_point_id_by_node[from], _point_id_by_node[to])
61 | var path := PackedStringArray()
62 |
63 | for id in id_path:
64 | path.append(_node_by_point_id[id])
65 |
66 | _travel_index = 0
67 | _travel_path = path
68 | return path
69 |
70 | ## Returns the cached path of a previous computation.
71 | func get_computed_travel_path() -> PackedStringArray:
72 | return _travel_path
73 |
74 | ## Clears the previously cached travel path.
75 | func clear_travel_path() -> void:
76 | _travel_path = PackedStringArray()
77 |
78 | ## Returns [code]true[/code] if there is a next point to fetch using [member get_next_travel_node].
79 | func has_next_travel_point() -> bool:
80 | return _travel_index < _travel_path.size()
81 |
82 | ## Returns the name of the next node on the calculated travel path.
83 | func get_next_travel_point() -> StringName:
84 | if not has_next_travel_point():
85 | return ""
86 | var next_state := _travel_path[_travel_index]
87 | _travel_index += 1
88 | return next_state
89 |
90 |
91 | func _get_node_from_id(id: int) -> StringName:
92 | return _node_by_point_id[id]
93 |
94 |
95 | class _CustomAStar:
96 | extends AStar2D
97 |
98 | # Type: func(StringName, StringName) -> int
99 | var _func_get_transition_cost: Callable
100 |
101 | # Type: func(int) -> StringName
102 | var _func_get_node_from_id: Callable
103 |
104 | func _init(func_get_transition_cost: Callable, func_get_node_from_id: Callable) -> void:
105 | _func_get_transition_cost = func_get_transition_cost
106 | _func_get_node_from_id = func_get_node_from_id
107 |
108 | func _compute_cost(from_id: int, to_id: int) -> float:
109 | var from: StringName = _func_get_node_from_id.call(from_id)
110 | var to: StringName = _func_get_node_from_id.call(to_id)
111 | return _func_get_transition_cost.call(from, to)
112 |
--------------------------------------------------------------------------------
/src/state_mgmt/state/state.gd:
--------------------------------------------------------------------------------
1 | class_name FrayState
2 | extends Resource
3 | ## Base state class
4 |
5 | # Type: WeakRef
6 | var _root_ref: WeakRef
7 |
8 | # Type: WeakRef
9 | var _parent_ref: WeakRef
10 |
11 | # Type: Dictionary
12 | var _callables_by_signal: Dictionary
13 |
14 |
15 | ## Returns [code]true[/code] if this state is child of another state.
16 | func has_parent() -> bool:
17 | return _parent_ref != null
18 |
19 |
20 | ## Returns the parent of this state if it exists.
21 | func get_parent() -> FrayCompoundState:
22 | return _parent_ref.get_ref() if has_parent() else null
23 |
24 |
25 | ## Returns the root of of this state's hierarchy.
26 | func get_root() -> FrayCompoundState:
27 | return _root_ref.get_ref() if _root_ref != null else null
28 |
29 |
30 | ## Returns [code]true[/code] if the state is considered to be done processing.
31 | func is_done_processing() -> bool:
32 | return _is_done_processing_impl()
33 |
34 |
35 | ## Returns the node located at the given [kbd]path[/kbd] relative to the state machine.
36 | func get_node(path: NodePath) -> Node:
37 | return get_root().get_node(path)
38 |
39 |
40 | ## Returns the first node of a given [kbd]type[/kbd] attached to this state machine.
41 | func get_node_of_type(type: Variant) -> Node:
42 | return get_root().get_node_of_type(type)
43 |
44 |
45 | ## Returns all nodes of a given [kbd]type[/kbd] attached to this state machine.
46 | func get_nodes_of_type(type: Variant) -> Array[Node]:
47 | return get_root().get_nodes_of_type(type)
48 |
49 |
50 | ## Process child states then this state.
51 | ## This method is intended to only be used by the [FrayCompoundState] when exiting sub-states.
52 | func exit() -> void:
53 | for sig in _callables_by_signal:
54 | for callable in _callables_by_signal[sig]:
55 | if sig.is_connected(callable):
56 | sig.disconnect(callable)
57 |
58 | _callables_by_signal.clear()
59 | _exit_impl()
60 |
61 |
62 | ## Connects a signal which disconnects when this state is no longer active.
63 | func connect_while_active(sig: Signal, callable: Callable, flags: int = 0) -> Error:
64 | if not sig.is_connected(callable):
65 | if not _callables_by_signal.has(sig):
66 | _callables_by_signal[sig] = []
67 |
68 | _callables_by_signal[sig].append(callable)
69 |
70 | return sig.connect(callable, flags)
71 |
72 |
73 | ## [code]Virtual method[/code] used to implement [method is_done_processing].
74 | func _is_done_processing_impl() -> bool:
75 | return true
76 |
77 |
78 | ## [code]Virtual method[/code] invoked when the state machine is initialized. Child states are readied before parent states.
79 | ## [br]
80 | ## [kbd]context[/kbd] is read-only dictionary which provides a way to pass data which is available to all states within a hierachy.
81 | func _ready_impl(context: Dictionary) -> void:
82 | pass
83 |
84 |
85 | ## [code]Virtual method[/code] invoked when the state is entered.
86 | ## [br]
87 | ## [kbd]args[/kbd] is user-defined data which is passed to the advanced state on enter.
88 | func _enter_impl(args: Dictionary) -> void:
89 | pass
90 |
91 |
92 | ## [code]Virtual method[/code] invoked when the state is existed.
93 | func _exit_impl() -> void:
94 | pass
95 |
96 |
97 | ## [code]Virtual method[/code] invokved when the state is added to the state machine hierarchy.
98 | func _enter_tree_impl() -> void:
99 | pass
100 |
101 |
102 | ## [code]Virtual method[/code] invoked when the state is being processed. [kbd]delta[/kbd] is in seconds.
103 | func _process_impl(delta: float) -> void:
104 | pass
105 |
106 |
107 | ## [code]Virtual method[/code] invoked when the state is being physics processed. [kbd]delta[/kbd] is in seconds.
108 | func _physics_process_impl(delta: float) -> void:
109 | pass
110 |
111 |
112 | ## [code]Virtual method[/code] invoked when there is a godot input event.
113 | func _input_impl(event: InputEvent) -> void:
114 | pass
115 |
116 |
117 | ## [code]Virtual method[/code] invoked when there is a godot input event that has not been consumed by [method Node._input].
118 | func _unhandled_input_impl(event: InputEvent) -> void:
119 | pass
120 |
--------------------------------------------------------------------------------
/src/state_mgmt/state_machine.gd:
--------------------------------------------------------------------------------
1 | @icon("res://addons/fray/assets/icons/state_machine.svg")
2 | class_name FrayStateMachine
3 | extends Node
4 | ## General purpose hierarchical state machine
5 | ##
6 | ## This class wraps around the [FrayCompoundState] and uses the [SceneTree] to
7 | ## process state nodes.
8 |
9 | ## Emitted when the current state within the root changes.
10 | signal state_changed(from: StringName, to: StringName)
11 |
12 | enum AdvanceMode {
13 | IDLE, ## Advance during the idle process
14 | PHYSICS, ## Advance during the physics process
15 | MANUAL, ## Advance manually
16 | }
17 |
18 | ## If true the [FrayStateMachine] will be processing.
19 | @export var active: bool = false
20 |
21 | ## Determines the process during which the state machine can advance.
22 | ## Advancing only relates to transitions.
23 | ## If the state machine is active then the current state is still processed
24 | ## during both idle and physics frames regardless of advance mode.
25 | @export var advance_mode: AdvanceMode = AdvanceMode.IDLE
26 |
27 | var _root: FrayCompoundState
28 |
29 |
30 | func _input(event: InputEvent) -> void:
31 | if _can_process():
32 | _root.input(event)
33 |
34 |
35 | func _unhandled_input(event: InputEvent):
36 | if _can_process():
37 | _root.unhandled_input(event)
38 |
39 |
40 | func _process(delta: float) -> void:
41 | if _can_process():
42 | _root.process(delta)
43 |
44 | if advance_mode == AdvanceMode.IDLE:
45 | advance()
46 |
47 |
48 | func _physics_process(delta: float) -> void:
49 | if _can_process():
50 | _root.physics_process(delta)
51 |
52 | if advance_mode == AdvanceMode.PHYSICS:
53 | advance()
54 |
55 |
56 | ## Used to initialize the root of the state machine.
57 | ## [br]
58 | ## [kbd]context[/kbd] is an optional dictionary which can pass read-only data to all states within the hierarchy.
59 | ## This data is accessible within a state's [method FrayState._ready_impl] method when it is invoked.
60 | ## [br]
61 | ## [b]WARN:[/b] The dictionary provided to the context argument will be made read-only.
62 | func initialize(context: Dictionary, root: FrayCompoundState) -> void:
63 | if root.has_parent():
64 | push_error("Failed to initialize statemachine. Provided root has parent state.")
65 | return
66 |
67 | _root = root
68 | _root._fn_get_node = get_node
69 | _root._fn_get_node_of_type = get_node_of_type
70 | _root._fn_get_nodes_of_type = get_nodes_of_type
71 | _root.ready(context)
72 | _root._enter_impl({})
73 | _root.transitioned.connect(_on_RootState_transitioned)
74 |
75 |
76 | ## Returns the state machine root.
77 | func get_root() -> FrayCompoundState:
78 | return _root
79 |
80 |
81 | ## Used to manually advance the state machine.
82 | func advance(input: Dictionary = {}, args: Dictionary = {}) -> bool:
83 | if _can_process():
84 | return _advance_impl(input, args)
85 | return false
86 |
87 |
88 | ## Returns the first node of a given [kbd]type[/kbd] attached to this state machine. Types can either be scripts or native classes.
89 | func get_node_of_type(type: Variant) -> Node:
90 | for child in get_children():
91 | if is_instance_of(child, type):
92 | return child
93 |
94 | return null
95 |
96 |
97 | ## Returns all nodes of a given [kbd]type[/kbd] attached to this state machine. Types can either be scripts or native classes.
98 | func get_nodes_of_type(type: Variant) -> Array[Node]:
99 | var nodes: Array[Node] = []
100 |
101 | for child in get_children():
102 | if is_instance_of(child, type):
103 | nodes.append(child)
104 |
105 | return nodes
106 |
107 |
108 | ## Returns the name of the root's current state.
109 | ## [br]
110 | ## Shorthand for root.get_current_state_name()
111 | func get_current_state_name() -> StringName:
112 | if _ERR_ROOT_NOT_SET("Failed to travel"):
113 | return ""
114 | return _root.get_current_state_name()
115 |
116 |
117 | ## Transitions from the current state to another one, following the shortest path.
118 | ## Transitions will ignore prerequisites and advance conditions, but will wait until a state is done processing.
119 | ## If no travel path can be formed then the [kbd]to[/kbd] state will be visted directly.
120 | ## [br]
121 | ## Shorthand for root.travel(input, args)
122 | func travel(to: StringName, args: Dictionary = {}) -> void:
123 | if _ERR_ROOT_NOT_SET("Failed to travel"):
124 | return
125 |
126 | _root.travel(to, args)
127 |
128 |
129 | ## Goes directly to the given state if it exists.
130 | ## If a travel is being performed it will be interupted.
131 | ## [br]
132 | ## Shorthand for root.goto(path, args)
133 | func goto(path: StringName, args: Dictionary = {}) -> void:
134 | if _ERR_ROOT_NOT_SET("Failed to go to state"):
135 | return
136 |
137 | _root.goto(path, args)
138 |
139 |
140 | ## Goes directly to the start state.
141 | ## [br]
142 | ## Shorthand for root.goto_start()
143 | func goto_start(args: Dictionary = {}) -> void:
144 | if _ERR_ROOT_NOT_SET("Failed to go to start state"):
145 | return
146 |
147 | _root.goto_start(args)
148 |
149 |
150 | ## Goes directly to the end state.
151 | ## [br]
152 | ## Shorthand for root.goto_start()
153 | func goto_end(args: Dictionary = {}) -> void:
154 | if _ERR_ROOT_NOT_SET("Failed to go to end state"):
155 | return
156 |
157 | _root.goto_end(args)
158 |
159 |
160 | ## [code]Virtual method[/code] used to implement [method advance] method.
161 | func _advance_impl(input: Dictionary = {}, args: Dictionary = {}) -> bool:
162 | if get_current_state_name().is_empty():
163 | push_warning("Failed to advance. Current state not set.")
164 | return false
165 |
166 | return _root.advance(input, args)
167 |
168 |
169 | func _can_process() -> bool:
170 | return _root != null and active
171 |
172 |
173 | func _ERR_ROOT_NOT_SET(msg: String = "") -> bool:
174 | if _root == null:
175 | push_error("%s. Current state not set." % msg)
176 | return true
177 |
178 | return false
179 |
180 |
181 | func _on_RootState_transitioned(from: StringName, to: StringName) -> void:
182 | state_changed.emit(from, to)
183 |
--------------------------------------------------------------------------------
/src/state_mgmt/transition/input_transition.gd:
--------------------------------------------------------------------------------
1 | class_name FrayInputTransition
2 | extends FrayStateMachineTransition
3 | ## Abstract input based transition class.
4 | ##
5 | ## Accepts input dictionary that contains these entires:
6 | ## [br] [br]
7 | ## - [code]time_since_last_input[/code] is the time in seconds since the last input was provided, as a [float].
8 |
9 | ## Minimum time that must have elapsed since the last input, in seconds. If negative then this check is ignored.
10 | @export var min_input_delay: float = -1.0
11 |
12 |
13 | func _accepts_impl(input: Dictionary) -> bool:
14 | return _can_ignore_min_input_delay() or input.get("time_since_last_input", 0) >= min_input_delay
15 |
16 |
17 | func _can_ignore_min_input_delay() -> bool:
18 | return sign(min_input_delay) == -1
19 |
--------------------------------------------------------------------------------
/src/state_mgmt/transition/input_transition_press.gd:
--------------------------------------------------------------------------------
1 | class_name FrayInputTransitionPress
2 | extends FrayInputTransition
3 | ## Input transition representing an atomic press input such as a key or button.
4 | ##
5 | ## Accepts input dictionary that contains these entires:
6 | ## [br] [br]
7 | ## - [code]input[/code] is the name of the input, as a [StringName];
8 | ## [br] [br]
9 | ## - [code]is_pressed[/code] is the state of the input, as a [bool];
10 | ## [br] [br]
11 | ## - [code]time_held[/code] is the time in seconds that the input was held for, as a [float].
12 |
13 | ## Input name.
14 | @export var input: StringName = ""
15 |
16 | ## If [code]true[/code] the input is only accepted on release.
17 | @export var is_triggered_on_release: bool = false
18 |
19 | ## Minimum time the input must be held in seconds. If negative then this check is ignored.
20 | @export var min_time_held: float = -1.0
21 |
22 | ## Maximum time the input is allowed to be held in seconds. If negative then this check is ignored.
23 | @export var max_time_held: float = -1.0
24 |
25 |
26 | func _accepts_impl(sm_input: Dictionary) -> bool:
27 | return (
28 | super(sm_input)
29 | and sm_input.get("input", null) == input
30 | and sm_input.get("is_pressed", false) != is_triggered_on_release
31 | and (_can_ignore_min_time_held() or sm_input.get("time_held", 0.0) >= min_time_held)
32 | and (_can_ignore_max_time_held() or sm_input.get("time_held", 0.0) <= max_time_held)
33 | )
34 |
35 |
36 | func _can_ignore_min_time_held() -> bool:
37 | return min_time_held < 0
38 |
39 |
40 | func _can_ignore_max_time_held() -> bool:
41 | return max_time_held < 0
42 |
--------------------------------------------------------------------------------
/src/state_mgmt/transition/input_transition_sequence.gd:
--------------------------------------------------------------------------------
1 | class_name FrayInputTransitionSequence
2 | extends FrayInputTransition
3 | ## Input transition representing a sequence input.
4 | ##
5 | ## Accepts input dictionary that contains these entires:
6 | ## [br] [br]
7 | ## - [code]sequence[/code] is the name of the input, as a [StringName];
8 |
9 | ## Name of the sequence.
10 | @export var sequence: StringName = ""
11 |
12 | func _accepts_impl(sm_input: Dictionary) -> bool:
13 | return (
14 | super(sm_input)
15 | and sm_input.get("sequence", null) == sequence
16 | )
17 |
--------------------------------------------------------------------------------
/src/state_mgmt/transition/state_machine_transition.gd:
--------------------------------------------------------------------------------
1 | class_name FrayStateMachineTransition
2 | extends Resource
3 | ## Represents transition from one state to another.
4 |
5 | enum SwitchMode{
6 | IMMEDIATE, ## Switch to the next state immediately.
7 | AT_END, ## Wait for the current state to finish processing, then switch to the beginning of the next state.
8 | }
9 |
10 | ## If [member auto_advance] is enabled then the transition will occur automatically when all advance conditions are true.
11 | @export var advance_conditions: PackedStringArray
12 |
13 | ## Prevents transition from occuring unless all prerequisite conditions are true.
14 | @export var prereqs: PackedStringArray
15 |
16 | ## If true then the transition can advance automatically.
17 | @export var auto_advance: bool = false
18 |
19 | ## Lower priority transitions are be preffered when determining next transitions.
20 | @export var priority: int = 0
21 |
22 | ## The transition type.
23 | @export var switch_mode: SwitchMode = SwitchMode.AT_END
24 |
25 | ## Returns true if the transition accepts the given input.
26 | func accepts(input: Dictionary) -> bool:
27 | return _accepts_impl(input)
28 |
29 | ## [code]Virtual method[/code] used to implement [method accepts] method.
30 | func _accepts_impl(input: Dictionary) -> bool:
31 | return true
32 |
--------------------------------------------------------------------------------