├── .gitignore
├── .idea
├── .gitignore
├── action-rpg-tutorial-rs.iml
├── modules.xml
└── vcs.xml
├── README.md
├── action-rpg-rs
├── .gitignore
├── .idea
│ ├── .gitignore
│ ├── action-rpg-rs.iml
│ ├── deployment.xml
│ ├── modules.xml
│ ├── runConfigurations
│ │ ├── Build_Debug.xml
│ │ ├── Build_Release.xml
│ │ ├── Build___Debug.xml
│ │ ├── Build___Debug2.xml
│ │ ├── Build___Release.xml
│ │ └── Build___Release2.xml
│ └── vcs.xml
├── Cargo.toml
├── build.ps1
├── build.sh
└── src
│ ├── bat.rs
│ ├── effect.rs
│ ├── grass.rs
│ ├── has_effect.rs
│ ├── health_ui.rs
│ ├── hurt_box.rs
│ ├── hurt_sound.rs
│ ├── lib.rs
│ ├── player.rs
│ ├── player_camera.rs
│ ├── player_detection.rs
│ ├── soft_collision.rs
│ ├── stats.rs
│ ├── sword.rs
│ ├── utils.rs
│ └── wander.rs
└── action-rpg
├── .gitignore
├── Colliders
├── HitBox.tscn
├── HurtBox.gdns
├── HurtBox.tscn
├── SoftCollision.gdns
└── SoftCollision.tscn
├── Effects
├── Effect.gdns
├── EnemyDeathEffect.gdns
├── EnemyDeathEffect.png
├── EnemyDeathEffect.png.import
├── EnemyDeathEffect.tscn
├── GrassEffect.png
├── GrassEffect.png.import
├── GrassEffect.tscn
├── HitEffect.gdns
├── HitEffect.png
├── HitEffect.png.import
└── HitEffect.tscn
├── Enemies
├── Bat.gdns
├── Bat.png
├── Bat.png.import
├── Bat.tscn
├── PlayerDetectionZone.gdns
├── PlayerDetectionZone.tscn
├── WanderController.gdns
└── WanderController.tscn
├── Music and Sounds
├── EnemyDie.wav
├── EnemyDie.wav.import
├── Evade.wav
├── Evade.wav.import
├── Hit.wav
├── Hit.wav.import
├── Hurt.wav
├── Hurt.wav.import
├── Menu Move.wav
├── Menu Move.wav.import
├── Menu Select.wav
├── Menu Select.wav.import
├── Music.mp3
├── Music.mp3.import
├── Pause.wav
├── Pause.wav.import
├── Swipe.wav
├── Swipe.wav.import
├── Unpause.wav
└── Unpause.wav.import
├── Player
├── Player.gdns
├── Player.png
├── Player.png.import
├── Player.tscn
├── PlayerHurtSound.gdns
├── PlayerHurtSound.tscn
├── PlayerStats.tscn
└── SwordHitBox.gdns
├── PlayerCamera.gdns
├── PlayerCamera.tscn
├── Shaders
└── WhiteColorShader.gdshader
├── Shadows
├── LargeShadow.png
├── LargeShadow.png.import
├── MediumShadow.png
├── MediumShadow.png.import
├── SmallShadow.png
└── SmallShadow.png.import
├── Stats
├── Stats.gdns
└── Stats.tscn
├── UI
├── HealthUI.gdns
├── HealthUI.tscn
├── HeartUIEmpty.png
├── HeartUIEmpty.png.import
├── HeartUIFull.png
└── HeartUIFull.png.import
├── World
├── Bush.png
├── Bush.png.import
├── Bush.tscn
├── CliffTileset.png
├── CliffTileset.png.import
├── DirtTileset.png
├── DirtTileset.png.import
├── Grass.gdns
├── Grass.png
├── Grass.png.import
├── Grass.tscn
├── GrassBackground.png
├── GrassBackground.png.import
├── Tree.png
├── Tree.png.import
└── Tree.tscn
├── action_rpg_tutorial_library.gdnlib
├── default_env.tres
├── icon.png
├── icon.png.import
├── project.godot
└── world.tscn
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | *.dll
3 | *.so
4 | *.dylib
5 | .godot/
6 |
7 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/.idea/action-rpg-tutorial-rs.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Godot Action RPG w/Rust
2 | A [GDNative](https://docs.godotengine.org/en/stable/tutorials/plugins/gdnative/index.html) implementation in [Rust
](https://www.rust-lang.org/) of youtuber [HeartBeast
](https://www.youtube.com/c/uheartbeast)'s great step-by-step turoial series creating a [Godot Action RPG](https://tinyurl.com/5t7rstyx), using the [godot-rust
](https://godot-rust.github.io/)
3 |
4 | ## Windows Build
5 | from within the ```action-rpg-rs``` folder
6 |
7 | _build release and copy dll to godot project_
8 |
9 | ```shell
10 | ~\...\action-rpg-rs > .\build.ps1 release
11 | ```
12 |
13 | _build debug and copy dll to godot project_
14 |
15 | ```shell
16 | ~\...\action-rpg-rs > .\build.ps1 debug
17 | ```
18 |
19 | ## MacOS / Linux Build
20 | from within the ```action-rpg-rs``` folder
21 |
22 | _build release and copy library so to godot project_
23 |
24 | ```shell
25 | ~/.../action-rpg-rs> ./build.sh release
26 | ```
27 |
28 | _build debug and copy library so to godot project_
29 |
30 | ```shell
31 | ~/.../action-rpg-rs> ./build.sh debug
32 | ```
33 |
34 | _* will require ```llvm``` tools, see_ [godot-rust](https://godot-rust.github.io/book/getting-started/setup.html) setup instructions
35 |
36 | _* this project is stuck on [Godot 3.4.n](https://godotengine.org/download/archive/#3.5-beta1) and [GDNative 3.9](https://crates.io/crates/gdnative/0.9.3), there are breaking changes beyond this point_
--------------------------------------------------------------------------------
/action-rpg-rs/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | debug/
4 | target/
5 |
6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
8 | Cargo.lock
9 |
10 | # These are backup files generated by rustfmt
11 | **/*.rs.bk
12 |
13 | # MSVC Windows builds of rustc generate these, which store debugging information
14 | *.pdb
--------------------------------------------------------------------------------
/action-rpg-rs/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/action-rpg-rs/.idea/action-rpg-rs.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/action-rpg-rs/.idea/deployment.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/action-rpg-rs/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/action-rpg-rs/.idea/runConfigurations/Build_Debug.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/action-rpg-rs/.idea/runConfigurations/Build_Release.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/action-rpg-rs/.idea/runConfigurations/Build___Debug.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/action-rpg-rs/.idea/runConfigurations/Build___Debug2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/action-rpg-rs/.idea/runConfigurations/Build___Release.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/action-rpg-rs/.idea/runConfigurations/Build___Release2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/action-rpg-rs/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/action-rpg-rs/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "action-rpg-rs"
3 | version = "0.1.0"
4 | edition = "2018"
5 |
6 | [lib]
7 | crate-type = ["cdylib"]
8 |
9 | [dependencies]
10 | gdnative = "0.9.3"
--------------------------------------------------------------------------------
/action-rpg-rs/build.ps1:
--------------------------------------------------------------------------------
1 | param(
2 | [Parameter(Mandatory=$true, Position=0)][ValidateSet('release', 'debug')]
3 | [string] $build
4 | )
5 |
6 | function exit_on_fail() {
7 | if ($lastExitCode -ne 0) {
8 | exit 1
9 | }
10 | }
11 |
12 | Write-Output ""
13 |
14 | if ($build -eq "release") {
15 | Write-Output "building release ..."
16 | Write-Output ""
17 | cargo build --release
18 | exit_on_fail
19 | } elseif ($build -eq "debug") {
20 | Write-Output "building debug ..."
21 | Write-Output ""
22 | cargo build
23 | exit_on_fail
24 | }
25 |
26 | Write-Output ""
27 | Write-Output "copying $build action_rpg_rs.dll to ../action-rpg/action_rpg_rs.dll"
28 | Write-Output ""
29 |
30 | Copy-Item .\target\$build\action_rpg_rs.dll ..\action-rpg\action-rpg.dll
31 | exit_on_fail
32 |
33 | Write-Output "done"
34 | Write-Output ""
35 |
--------------------------------------------------------------------------------
/action-rpg-rs/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ -z "$1" ]; then
4 | build="release"
5 | else
6 | build=$1
7 | fi
8 |
9 | echo
10 |
11 | if [[ "$build" == "release" ]]; then
12 | echo building release ...
13 | echo
14 | cargo build --release || { exit 1; }
15 | elif [[ "$build" == "debug" ]]; then
16 | echo building debug ...
17 | echo
18 | cargo build || { exit 1; }
19 | else
20 | echo "$1 is an invalid argument"
21 | echo
22 | echo "usage: ./build.sh [debug|release]"
23 | echo
24 | exit 1
25 | fi
26 |
27 | library="libaction_rpg_rs"
28 |
29 | case "$(uname -sr)" in
30 | Darwin*)
31 | library="$library.dynlib"
32 | ;;
33 |
34 | Linux*|Linux*Microsoft*|CYGWIN*|MINGW*|MINGW32*|MSYS*)
35 | library="$library.so"
36 | ;;
37 |
38 | *)
39 | library="$library.so"
40 | ;;
41 | esac
42 |
43 | echo
44 | echo "copying $build $library to ../action-rpg/$library"
45 | echo
46 |
47 | cp "./target/$build/$library" ../action-rpg/$library || { exit 1; }
48 |
49 | echo "done"
50 | echo
51 |
--------------------------------------------------------------------------------
/action-rpg-rs/src/bat.rs:
--------------------------------------------------------------------------------
1 | use std::f64::consts::FRAC_PI_4;
2 |
3 | use gdnative::api::*;
4 | use gdnative::prelude::*;
5 |
6 | use crate::{assume_safe, call, child_node, get_parameter, load_resource, set_parameter};
7 | use crate::has_effect::HasEffect;
8 | use crate::hurt_box::{METHOD_PLAY_HIT_EFFECT, METHOD_START_INVINCIBILITY};
9 | use crate::player_detection::{METHOD_CAN_SEE_PLAYER, METHOD_GET_PLAYER};
10 | use crate::soft_collision::{METHOD_GET_PUSH_VECTOR, METHOD_IS_COLLIDING};
11 | use crate::stats::PROPERTY_HEALTH;
12 | use crate::sword::{PROPERTY_DAMAGE, PROPERTY_KNOCK_BACK_VECTOR};
13 | use crate::wander::{METHOD_IS_TIMER_COMPLETE, METHOD_START_TIMER, PROPERTY_TARGET_POSITION};
14 |
15 | pub(crate) const PROPERTY_ACCELERATION: &str = "acceleration";
16 | pub(crate) const PROPERTY_FRICTION: &str = "friction";
17 | pub(crate) const PROPERTY_KNOCK_BACK_FORCE: &str = "knock_back_force";
18 | pub(crate) const PROPERTY_MAX_SPEED: &str = "max_speed";
19 | pub(crate) const PROPERTY_PUSH_VECTOR_FORCE: &str = "push_vector_force";
20 |
21 | // i choose this ratio of max speed to buffer the bat's approach to it's target
22 | const WANDER_BUFFER_RATIO: f32 = 0.08; // this value might be frame rate dependent
23 |
24 | const DEFAULT_ACCELERATION: f32 = 300.0;
25 | const DEFAULT_FRICTION: f32 = 200.0;
26 | const DEFAULT_KNOCK_BACK_FORCE: f32 = 120.0;
27 | const DEFAULT_MAX_SPEED: f32 = 50.0;
28 | const DEFAULT_PUSH_VECTOR_FORCE: f32 = 400.0;
29 | const DEFAULT_WANDER_BUFFER_ZONE: f32 = DEFAULT_MAX_SPEED * WANDER_BUFFER_RATIO;
30 |
31 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
32 | enum BatState {
33 | CHASE,
34 | IDLE,
35 | WANDER,
36 | }
37 |
38 | #[derive(NativeClass)]
39 | #[inherit(KinematicBody2D)]
40 | #[register_with(Self::register)]
41 | pub struct Bat {
42 | #[property]
43 | acceleration: f32,
44 | blink_animation: Option[>,
45 | effect: Option][>,
46 | #[property]
47 | friction: f32,
48 | hurt_box: Option][>,
49 | knock_back: Vector2,
50 | #[property]
51 | knock_back_force: f32,
52 | #[property]
53 | max_speed: f32,
54 | player_detection: Option][>,
55 | push_vector_force: f32,
56 | rand: Ref,
57 | soft_collision: Option][>,
58 | sprite: Option][>,
59 | state: BatState,
60 | stats: Option][>,
61 | velocity: Vector2,
62 | wander_buffer_zone: f32,
63 | wander_controller: Option][>,
64 | }
65 |
66 | impl HasEffect for Bat {
67 | fn effect_scene(&self) -> &Option][> {
68 | &self.effect
69 | }
70 | }
71 |
72 | impl Bat {
73 | fn new(_owner: &KinematicBody2D) -> Self {
74 | Bat {
75 | acceleration: DEFAULT_ACCELERATION,
76 | blink_animation: None,
77 | effect: None,
78 | friction: DEFAULT_FRICTION,
79 | hurt_box: None,
80 | knock_back: Vector2::zero(),
81 | knock_back_force: DEFAULT_KNOCK_BACK_FORCE,
82 | max_speed: DEFAULT_MAX_SPEED,
83 | player_detection: None,
84 | push_vector_force: DEFAULT_PUSH_VECTOR_FORCE,
85 | rand: RandomNumberGenerator::new(),
86 | soft_collision: None,
87 | sprite: None,
88 | state: BatState::IDLE,
89 | stats: None,
90 | velocity: Vector2::zero(),
91 | wander_buffer_zone: DEFAULT_WANDER_BUFFER_ZONE,
92 | wander_controller: None,
93 | }
94 | }
95 |
96 | //noinspection DuplicatedCode
97 | fn register(builder: &ClassBuilder) {
98 | builder
99 | .add_property::(PROPERTY_ACCELERATION)
100 | .with_getter(|s: &Self, _| s.acceleration)
101 | .with_setter(|s: &mut Self, _, value: f32| s.acceleration = value)
102 | .with_default(DEFAULT_ACCELERATION)
103 | .done();
104 |
105 | builder
106 | .add_property::(PROPERTY_FRICTION)
107 | .with_getter(|s: &Self, _| s.friction)
108 | .with_setter(|s: &mut Self, _, value: f32| s.friction = value)
109 | .with_default(DEFAULT_FRICTION)
110 | .done();
111 |
112 | builder
113 | .add_property::(PROPERTY_KNOCK_BACK_FORCE)
114 | .with_getter(|s: &Self, _| s.knock_back_force)
115 | .with_setter(|s: &mut Self, _, value: f32| s.knock_back_force = value)
116 | .with_default(DEFAULT_KNOCK_BACK_FORCE)
117 | .done();
118 |
119 | builder
120 | .add_property::(PROPERTY_MAX_SPEED)
121 | .with_getter(|s: &Self, _| s.max_speed)
122 | .with_setter(|s: &mut Self, _, value: f32| {
123 | s.max_speed = value;
124 | s.wander_buffer_zone = s.max_speed * WANDER_BUFFER_RATIO;
125 | })
126 | .with_default(DEFAULT_MAX_SPEED)
127 | .done();
128 |
129 | builder
130 | .add_property::(PROPERTY_PUSH_VECTOR_FORCE)
131 | .with_getter(|s: &Self, _| s.push_vector_force)
132 | .with_setter(|s: &mut Self, _, value: f32| s.push_vector_force = value)
133 | .with_default(DEFAULT_PUSH_VECTOR_FORCE)
134 | .done();
135 | }
136 | }
137 |
138 | #[methods]
139 | impl Bat {
140 | #[export]
141 | fn _ready(&mut self, owner: &KinematicBody2D) {
142 | load_resource! { scene: PackedScene = "Effects/EnemyDeathEffect.tscn" {
143 | self.effect = Some(scene.claim())
144 | } }
145 |
146 | self.blink_animation = Some(child_node!(claim owner["BlinkAnimationPlayer"]: AnimationPlayer));
147 | self.hurt_box = Some(child_node!(claim owner["HurtBox"]: Node2D));
148 | self.player_detection = Some(child_node!(claim owner["PlayerDetectionZone"]: Area2D));
149 | self.soft_collision = Some(child_node!(claim owner["SoftCollision"]: Area2D));
150 | self.sprite = Some(child_node!(claim owner["AnimatedSprite"]: AnimatedSprite));
151 | self.stats = Some(child_node!(owner["Stats"]));
152 | self.wander_controller = Some(child_node!(claim owner["WanderController"]: Node2D));
153 |
154 | self.state = self.pick_random_state();
155 | }
156 |
157 | #[export]
158 | fn _physics_process(&mut self, owner: &KinematicBody2D, delta: f32) {
159 | self.knock_back = owner.move_and_slide(
160 | self.knock_back.move_towards(Vector2::zero(), self.friction * delta),
161 | Vector2::zero(), false, 4, FRAC_PI_4, true,
162 | );
163 |
164 | match self.state {
165 | BatState::CHASE => {
166 | let player = call!(self.player_detection; METHOD_GET_PLAYER: KinematicBody2D);
167 |
168 | if let Some(player) = player {
169 | let direction = owner.global_position()
170 | .direction_to(unsafe { player.assume_safe() }.global_position());
171 |
172 | self.accelerate_towards(direction, delta);
173 | } else {
174 | self.state = BatState::IDLE
175 | }
176 | }
177 | BatState::IDLE => {
178 | self.seek_player(owner);
179 | self.next_state_on_finish(3.0);
180 |
181 | self.velocity = self.velocity.move_towards(Vector2::zero(), self.friction * delta);
182 | }
183 | BatState::WANDER => {
184 | self.seek_player(owner);
185 | self.next_state_on_finish(3.0);
186 |
187 | let target_position = get_parameter!(
188 | self.wander_controller.unwrap(); PROPERTY_TARGET_POSITION
189 | ).to_vector2();
190 |
191 | let direction = owner.global_position().direction_to(target_position);
192 |
193 | self.accelerate_towards(direction, delta);
194 |
195 | if owner.global_position().distance_to(target_position) <= self.wander_buffer_zone {
196 | self.next_state(3.0);
197 | }
198 | }
199 | }
200 |
201 | if call!(self.soft_collision; METHOD_IS_COLLIDING).to_bool() {
202 | self.velocity += call!(self.soft_collision; METHOD_GET_PUSH_VECTOR).to_vector2()
203 | * delta * self.push_vector_force;
204 | }
205 |
206 | // move flip logic here for all movement states
207 | // check for stopped bat to keep last direction
208 | if self.velocity != Vector2::zero() {
209 | assume_safe!(self.sprite).set_flip_h(self.velocity.x < 0.0);
210 | }
211 |
212 | owner.move_and_slide(self.velocity, Vector2::zero(), false, 4, FRAC_PI_4, true);
213 | }
214 |
215 | #[inline]
216 | fn accelerate_towards(&mut self, direction: Vector2, delta: f32) {
217 | self.velocity = self.velocity.move_towards(
218 | direction * self.max_speed,
219 | self.acceleration * delta,
220 | );
221 | }
222 |
223 | #[inline]
224 | fn next_state(&mut self, max_secs: f64) {
225 | self.state = self.pick_random_state();
226 |
227 | call!(
228 | self.wander_controller;
229 | METHOD_START_TIMER(self.rand.randf_range(1.0, max_secs).to_variant())
230 | );
231 | }
232 |
233 | #[inline]
234 | fn next_state_on_finish(&mut self, max_secs: f64) {
235 | let timer_complete = call!(self.wander_controller; METHOD_IS_TIMER_COMPLETE).to_bool();
236 |
237 | if timer_complete {
238 | self.next_state(max_secs);
239 | }
240 | }
241 |
242 | #[inline]
243 | fn seek_player(&mut self, _owner: &KinematicBody2D) {
244 | let can_see_player = call!(self.player_detection; METHOD_CAN_SEE_PLAYER).to_bool();
245 | if can_see_player {
246 | self.state = BatState::CHASE
247 | }
248 | }
249 |
250 | // this did not need the overhead of lists and list manipulation,
251 | // so this is my simplified solution
252 | #[inline]
253 | fn pick_random_state(&mut self) -> BatState {
254 | if self.rand.randi_range(1, 2) == 1 {
255 | BatState::IDLE
256 | } else {
257 | BatState::WANDER
258 | }
259 | }
260 |
261 | #[export]
262 | #[allow(non_snake_case)]
263 | fn _on_HurtBox_area_entered(&mut self, _owner: &KinematicBody2D, area: Ref) {
264 | let damage = get_parameter!(area[PROPERTY_DAMAGE]).to_i64();
265 | let stats = self.stats.unwrap();
266 | let health = get_parameter!(stats; PROPERTY_HEALTH).to_i64();
267 |
268 | set_parameter!(stats; PROPERTY_HEALTH = health - damage);
269 |
270 | self.knock_back = get_parameter!(area[PROPERTY_KNOCK_BACK_VECTOR]).to_vector2()
271 | * self.knock_back_force;
272 |
273 | call!(self.hurt_box; METHOD_START_INVINCIBILITY(0.4.to_variant()));
274 | call!(self.hurt_box; METHOD_PLAY_HIT_EFFECT);
275 | }
276 |
277 | #[export]
278 | #[allow(non_snake_case)]
279 | fn _on_HurtBox_invincibility_ended(&self, _owner: &KinematicBody2D) {
280 | assume_safe!(self.blink_animation).play("Stop", -1.0, 1.0, false);
281 | }
282 |
283 | #[export]
284 | #[allow(non_snake_case)]
285 | fn _on_HurtBox_invincibility_started(&self, _owner: &KinematicBody2D) {
286 | assume_safe!(self.blink_animation).play("Start", -1.0, 1.0, false);
287 | }
288 |
289 | // when connecting signal in the godot editor, click the "advanced" switch
290 | // and select the "deferred" option, otherwise an exception occurs
291 | // todo: figure out why this is necessary
292 | #[export]
293 | #[allow(non_snake_case)]
294 | fn _on_Stats_no_health(&self, owner: &KinematicBody2D) {
295 | self.play_effect_parent(owner);
296 | owner.queue_free();
297 | }
298 | }
299 |
--------------------------------------------------------------------------------
/action-rpg-rs/src/effect.rs:
--------------------------------------------------------------------------------
1 | use gdnative::api::*;
2 | use gdnative::prelude::*;
3 |
4 | #[derive(NativeClass)]
5 | #[inherit(AnimatedSprite)]
6 | pub struct Effect;
7 |
8 | impl Effect {
9 | fn new(_owner: &AnimatedSprite) -> Self {
10 | Effect
11 | }
12 | }
13 |
14 | #[methods]
15 | impl Effect {
16 | #[export]
17 | fn _ready(&mut self, owner: TRef) {
18 | owner
19 | .connect("animation_finished", owner, "_on_animation_finished", VariantArray::new_shared(), 1)
20 | .expect("_on_animation_finished to connect to effect instance");
21 |
22 | owner.set_frame(0);
23 | owner.play("Animate", false);
24 | }
25 |
26 | #[export]
27 | #[allow(non_snake_case)]
28 | fn _on_animation_finished(&mut self, owner: &AnimatedSprite) {
29 | owner.queue_free();
30 | }
31 | }
--------------------------------------------------------------------------------
/action-rpg-rs/src/grass.rs:
--------------------------------------------------------------------------------
1 | use gdnative::api::*;
2 | use gdnative::prelude::*;
3 |
4 | use crate::has_effect::HasEffect;
5 | use crate::load_resource;
6 |
7 | #[derive(NativeClass)]
8 | #[inherit(Node2D)]
9 | pub struct Grass {
10 | effect: Option][>,
11 | }
12 |
13 | impl HasEffect for Grass {
14 | fn effect_scene(&self) -> &Option][> {
15 | &self.effect
16 | }
17 | }
18 |
19 | impl Grass {
20 | fn new(_owner: &Node2D) -> Self {
21 | Grass { effect: None }
22 | }
23 | }
24 |
25 | #[methods]
26 | impl Grass {
27 | #[export]
28 | fn _ready(&mut self, _owner: &Node2D) {
29 | load_resource! { scene: PackedScene = "Effects/GrassEffect.tscn" {
30 | self.effect = Some(scene.claim())
31 | } }
32 | }
33 |
34 | #[export]
35 | #[allow(non_snake_case)]
36 | fn _on_HurtBox_area_entered(&mut self, owner: &Node2D, _area: Ref) {
37 | self.play_effect_parent(owner);
38 | owner.queue_free();
39 | }
40 | }
--------------------------------------------------------------------------------
/action-rpg-rs/src/has_effect.rs:
--------------------------------------------------------------------------------
1 | use gdnative::api::*;
2 | use gdnative::prelude::*;
3 |
4 | use crate::assume_safe;
5 |
6 | pub(crate) trait HasEffect {
7 | fn effect_scene(&self) -> &Option][>;
8 |
9 | #[inline]
10 | fn play_effect_parent(&self, owner: &Node2D) {
11 | let scene = assume_safe!(self.effect_scene());
12 |
13 | assume_safe! {
14 | let instance: Node2D = scene.instance(PackedScene::GEN_EDIT_STATE_DISABLED),
15 | let parent: Node = Node::get_parent(owner) => {
16 | parent.add_child(instance, false);
17 | instance.set_global_position(owner.global_position());
18 | }
19 | }
20 | }
21 |
22 | #[inline]
23 | fn play_effect_root(&self, owner: &Node2D) {
24 | let scene = assume_safe!(self.effect_scene());
25 |
26 | assume_safe! {
27 | let instance: Node2D = scene.instance(PackedScene::GEN_EDIT_STATE_DISABLED),
28 | let root: SceneTree = Node::get_tree(owner),
29 | let scene: Node = root.current_scene() => {
30 | scene.add_child(instance, false);
31 | instance.set_global_position(owner.global_position());
32 | }
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/action-rpg-rs/src/health_ui.rs:
--------------------------------------------------------------------------------
1 | use gdnative::api::*;
2 | use gdnative::prelude::*;
3 |
4 | use crate::{assume_safe, auto_load, child_node};
5 | use crate::stats::{PROPERTY_MAX_HEALTH, SIGNAL_HEALTH_CHANGED, SIGNAL_MAX_HEALTH_CHANGED};
6 |
7 | type Hearts = i64;
8 |
9 | pub(crate) const PROPERTY_HEARTS: &str = "hearts";
10 | pub(crate) const PROPERTY_MAX_HEARTS: &str = "max_hearts";
11 |
12 | const DEFAULT_MAX_HEARTS: Hearts = 4;
13 |
14 | const MINIMUM_HEARTS: Hearts = 0;
15 | const MINIMUM_MAX_HEARTS: Hearts = 1;
16 |
17 | const HEART_HEIGHT: f32 = 11.0;
18 | const HEART_WIDTH: f32 = 15.0;
19 |
20 | #[derive(NativeClass)]
21 | #[inherit(Control)]
22 | #[register_with(Self::register)]
23 | pub struct HealthUI {
24 | #[property]
25 | hearts: Hearts,
26 | hearts_empty: Option][>,
27 | hearts_full: Option][>,
28 | player_stats: Option][>,
29 | #[property]
30 | max_hearts: Hearts,
31 | }
32 |
33 | impl HealthUI {
34 | fn new(_owner: &Control) -> Self {
35 | HealthUI {
36 | hearts: DEFAULT_MAX_HEARTS,
37 | hearts_empty: None,
38 | hearts_full: None,
39 | player_stats: None,
40 | max_hearts: DEFAULT_MAX_HEARTS,
41 | }
42 | }
43 |
44 | fn register(builder: &ClassBuilder) {
45 | builder
46 | .add_property::(PROPERTY_HEARTS)
47 | .with_getter(|s: &Self, _| s.hearts)
48 | .with_setter(Self::set_hearts)
49 | .with_default(DEFAULT_MAX_HEARTS)
50 | .done();
51 |
52 | builder
53 | .add_property::(PROPERTY_MAX_HEARTS)
54 | .with_getter(|s: &Self, _| s.max_hearts)
55 | .with_setter(Self::set_max_hearts)
56 | .with_default(DEFAULT_MAX_HEARTS)
57 | .done();
58 | }
59 |
60 | #[inline]
61 | fn update_health_ui(&self) {
62 | assume_safe!(self.hearts_full)
63 | .set_size(Vector2::new(self.hearts as f32 * HEART_WIDTH, HEART_HEIGHT), false);
64 | }
65 |
66 | #[inline]
67 | fn update_max_health_ui(&self) {
68 | assume_safe!(self.hearts_empty)
69 | .set_size(Vector2::new(self.max_hearts as f32 * HEART_WIDTH, HEART_HEIGHT), false);
70 | }
71 | }
72 |
73 | #[methods]
74 | impl HealthUI {
75 | #[export]
76 | fn _ready(&mut self, owner: TRef) {
77 | let owner_ref = owner.as_ref();
78 |
79 | self.hearts_empty = Some(child_node!(claim owner_ref["HeartUIEmpty"]: TextureRect));
80 | self.hearts_full = Some(child_node!(claim owner_ref["HeartUIFull"]: TextureRect));
81 |
82 | let player_stats = auto_load!("PlayerStats": Node);
83 |
84 | player_stats
85 | .connect(SIGNAL_HEALTH_CHANGED, owner.clone(), "set_hearts", VariantArray::new_shared(), 1)
86 | .expect("set_hearts to connect to player stats");
87 |
88 | player_stats
89 | .connect(SIGNAL_MAX_HEALTH_CHANGED, owner.clone(), "set_max_hearts", VariantArray::new_shared(), 1)
90 | .expect("set_max_hearts to connect to player stats");
91 |
92 | self.set_max_hearts(owner, player_stats.get(PROPERTY_MAX_HEALTH).to_i64());
93 |
94 | self.player_stats = Some(player_stats.claim());
95 | }
96 |
97 | #[export]
98 | fn set_hearts(&mut self, _owner: TRef, hearts: Hearts) {
99 | self.hearts = Hearts::clamp(hearts, MINIMUM_HEARTS, self.max_hearts);
100 | self.update_health_ui();
101 | }
102 |
103 | #[export]
104 | fn set_max_hearts(&mut self, _owner: TRef, max_hearts: Hearts) {
105 | self.max_hearts = Hearts::max(max_hearts, MINIMUM_MAX_HEARTS);
106 | self.update_max_health_ui();
107 |
108 | self.hearts = self.max_hearts;
109 | self.update_health_ui();
110 | }
111 | }
--------------------------------------------------------------------------------
/action-rpg-rs/src/hurt_box.rs:
--------------------------------------------------------------------------------
1 | use gdnative::api::*;
2 | use gdnative::prelude::*;
3 |
4 | use crate::{assume_safe, child_node, load_resource};
5 | use crate::has_effect::HasEffect;
6 |
7 | type Duration = f64;
8 |
9 | pub(crate) const PROPERTY_INVINCIBLE: &str = "invincible";
10 |
11 | const DEFAULT_INVINCIBLE: bool = false;
12 |
13 | pub(crate) const METHOD_START_INVINCIBILITY: &str = "start_invincibility";
14 | pub(crate) const METHOD_PLAY_HIT_EFFECT: &str = "play_hit_effect";
15 |
16 | pub(crate) const SIGNAL_INVINCIBILITY_ENDED: &str = "invincibility_ended";
17 | pub(crate) const SIGNAL_INVINCIBILITY_STARTED: &str = "invincibility_started";
18 |
19 | #[derive(NativeClass)]
20 | #[inherit(Node2D)]
21 | #[register_with(Self::register)]
22 | pub struct HurtBox {
23 | collision_shape: Option][>,
24 | effect: Option][>,
25 | invincible: bool,
26 | timer: Option][>,
27 | }
28 |
29 | impl HasEffect for HurtBox {
30 | fn effect_scene(&self) -> &Option][> {
31 | &self.effect
32 | }
33 | }
34 |
35 | impl HurtBox {
36 | fn new(_owner: &Node2D) -> Self {
37 | HurtBox {
38 | collision_shape: None,
39 | effect: None,
40 | invincible: DEFAULT_INVINCIBLE,
41 | timer: None,
42 | }
43 | }
44 |
45 | fn register(builder: &ClassBuilder) {
46 | builder
47 | .add_property::(PROPERTY_INVINCIBLE)
48 | .with_getter(|s: &Self, _| s.invincible)
49 | .with_setter(Self::set_invincible)
50 | .with_default(DEFAULT_INVINCIBLE)
51 | .done();
52 |
53 | builder.add_signal(Signal { name: SIGNAL_INVINCIBILITY_ENDED, args: &[] });
54 |
55 | builder.add_signal(Signal { name: SIGNAL_INVINCIBILITY_STARTED, args: &[] });
56 | }
57 |
58 | fn set_invincible(&mut self, owner: TRef, invincible: bool) {
59 | self.invincible = invincible;
60 |
61 | if invincible {
62 | owner.emit_signal(SIGNAL_INVINCIBILITY_STARTED, &[]);
63 | } else {
64 | owner.emit_signal(SIGNAL_INVINCIBILITY_ENDED, &[]);
65 | }
66 | }
67 | }
68 |
69 | #[methods]
70 | impl HurtBox {
71 | #[export]
72 | fn _ready(&mut self, owner: &Node2D) {
73 | load_resource! { scene: PackedScene = "Effects/HitEffect.tscn" {
74 | self.effect = Some(scene.claim())
75 | } }
76 |
77 | self.collision_shape = Some(child_node!(claim owner["CollisionShape2D"]: CollisionShape2D));
78 | self.timer = Some(child_node!(claim owner["Timer"]: Timer));
79 | }
80 |
81 | #[export]
82 | fn play_hit_effect(&mut self, owner: &Node2D) {
83 | self.play_effect_root(owner);
84 | }
85 |
86 | #[export]
87 | fn start_invincibility(&mut self, owner: TRef, duration: Duration) {
88 | self.set_invincible(owner, true);
89 |
90 | assume_safe!(self.timer).start(duration);
91 | }
92 |
93 | // rust required these two signals to be connected "deferred" in godot
94 | #[export]
95 | #[allow(non_snake_case)]
96 | fn _on_HurtBox_invincibility_ended(&mut self, owner: &Node2D) {
97 | assume_safe!(self.collision_shape).set_disabled(false);
98 | owner.set("monitorable", true);
99 | }
100 |
101 | #[export]
102 | #[allow(non_snake_case)]
103 | fn _on_HurtBox_invincibility_started(&mut self, owner: &Node2D) {
104 | assume_safe!(self.collision_shape).set_disabled(true);
105 | owner.set("monitorable", false);
106 | }
107 |
108 | #[export]
109 | #[allow(non_snake_case)]
110 | fn _on_Timer_timeout(&mut self, owner: TRef) {
111 | self.set_invincible(owner, false);
112 | }
113 | }
--------------------------------------------------------------------------------
/action-rpg-rs/src/hurt_sound.rs:
--------------------------------------------------------------------------------
1 | use gdnative::api::*;
2 | use gdnative::prelude::*;
3 |
4 | #[derive(NativeClass)]
5 | #[inherit(AudioStreamPlayer)]
6 | pub struct PlayerHurtSound;
7 |
8 | impl PlayerHurtSound {
9 | fn new(_owner: &AudioStreamPlayer) -> Self {
10 | PlayerHurtSound
11 | }
12 | }
13 |
14 | #[methods]
15 | impl PlayerHurtSound {
16 | #[export]
17 | #[allow(non_snake_case)]
18 | fn _on_PlayerHurtSound_finished(&mut self, owner: &AudioStreamPlayer) {
19 | owner.queue_free();
20 | }
21 | }
--------------------------------------------------------------------------------
/action-rpg-rs/src/lib.rs:
--------------------------------------------------------------------------------
1 | use gdnative::prelude::*;
2 |
3 | mod bat;
4 | mod effect;
5 | mod grass;
6 | mod has_effect;
7 | mod health_ui;
8 | mod hurt_box;
9 | mod hurt_sound;
10 | mod player;
11 | mod player_camera;
12 | mod player_detection;
13 | mod soft_collision;
14 | mod stats;
15 | mod sword;
16 | mod wander;
17 |
18 | mod utils;
19 |
20 | fn init(handle: InitHandle) {
21 | handle.add_class::();
22 | handle.add_class::();
23 | handle.add_class::();
24 | handle.add_class::();
25 | handle.add_class::();
26 | handle.add_class::();
27 | handle.add_class::();
28 | handle.add_class::();
29 | handle.add_class::();
30 | handle.add_class::();
31 | handle.add_class::();
32 | handle.add_class::();
33 | handle.add_class::();
34 | }
35 |
36 | godot_init!(init);
--------------------------------------------------------------------------------
/action-rpg-rs/src/player.rs:
--------------------------------------------------------------------------------
1 | use std::f64::consts::FRAC_PI_4;
2 |
3 | use gdnative::api::*;
4 | use gdnative::prelude::*;
5 |
6 | use crate::{
7 | assume_safe, auto_load, blend_position, call,
8 | child_node, get_parameter, load_resource, set_parameter,
9 | };
10 | use crate::hurt_box::{METHOD_PLAY_HIT_EFFECT, METHOD_START_INVINCIBILITY};
11 | use crate::stats::{PROPERTY_HEALTH, SIGNAL_NO_HEALTH};
12 | use crate::sword::PROPERTY_KNOCK_BACK_VECTOR;
13 |
14 | type AnimationPlayback = AnimationNodeStateMachinePlayback;
15 |
16 | pub(crate) const PROPERTY_ACCELERATION: &str = "acceleration";
17 | pub(crate) const PROPERTY_FRICTION: &str = "friction";
18 | pub(crate) const PROPERTY_MAX_SPEED: &str = "max_speed";
19 | pub(crate) const PROPERTY_ROLL_SPEED: &str = "roll_speed";
20 |
21 | const DEFAULT_ACCELERATION: f32 = 500.0;
22 | const DEFAULT_FRICTION: f32 = 500.0;
23 | const DEFAULT_MAX_SPEED: f32 = 80.0;
24 | const DEFAULT_ROLL_SPEED: f32 = 120.0;
25 |
26 | const INPUT_ATTACK: &str = "ui_attack";
27 | const INPUT_DOWN: &str = "ui_down";
28 | const INPUT_LEFT: &str = "ui_left";
29 | const INPUT_RIGHT: &str = "ui_right";
30 | const INPUT_ROLL: &str = "ui_roll";
31 | const INPUT_UP: &str = "ui_up";
32 |
33 | const TRAVEL_ATTACK: &str = "Attack";
34 | const TRAVEL_IDLE: &str = "Idle";
35 | const TRAVEL_ROLL: &str = "Roll";
36 | const TRAVEL_RUN: &str = "Run";
37 |
38 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
39 | enum PlayerState {
40 | Attack,
41 | Move,
42 | Roll,
43 | }
44 |
45 | #[derive(NativeClass)]
46 | #[inherit(KinematicBody2D)]
47 | #[register_with(Self::register)]
48 | pub struct Player {
49 | #[property]
50 | acceleration: f32,
51 | // todo: when using Ref, the placeholder values in "fn new" cause the following warning in godot
52 | // todo: "WARNING: cleanup: ObjectDB instances leaked at exit"
53 | animation_state: Option][>,
54 | animation_tree: Option][>,
55 | blink_animation: Option][>,
56 | #[property]
57 | friction: f32,
58 | hurt_box: Option][>,
59 | hurt_sound: Option][>,
60 | #[property]
61 | max_speed: f32,
62 | player_stats: Option][>,
63 | #[property]
64 | roll_speed: f32,
65 | roll_vector: Vector2,
66 | state: PlayerState,
67 | sword: Option][>,
68 | velocity: Vector2,
69 | }
70 |
71 | impl Player {
72 | fn new(_owner: &KinematicBody2D) -> Self {
73 | Player {
74 | acceleration: DEFAULT_ACCELERATION,
75 | animation_state: None,
76 | animation_tree: None,
77 | blink_animation: None,
78 | friction: DEFAULT_FRICTION,
79 | hurt_box: None,
80 | hurt_sound: None,
81 | max_speed: DEFAULT_MAX_SPEED,
82 | roll_speed: DEFAULT_ROLL_SPEED,
83 | roll_vector: Vector2::new(0.0, 1.0), // DOWN
84 | player_stats: None,
85 | state: PlayerState::Move,
86 | sword: None,
87 | velocity: Vector2::zero(),
88 | }
89 | }
90 |
91 | //noinspection DuplicatedCode
92 | fn register(builder: &ClassBuilder) {
93 | builder
94 | .add_property::(PROPERTY_ACCELERATION)
95 | .with_getter(|s: &Self, _| s.acceleration)
96 | .with_setter(|s: &mut Self, _, value: f32| s.acceleration = value)
97 | .with_default(DEFAULT_ACCELERATION)
98 | .done();
99 |
100 | builder
101 | .add_property::(PROPERTY_FRICTION)
102 | .with_getter(|s: &Self, _| s.friction)
103 | .with_setter(|s: &mut Self, _, value: f32| s.friction = value)
104 | .with_default(DEFAULT_FRICTION)
105 | .done();
106 |
107 | builder
108 | .add_property::(PROPERTY_MAX_SPEED)
109 | .with_getter(|s: &Self, _| s.max_speed)
110 | .with_setter(|s: &mut Self, _, value: f32| s.max_speed = value)
111 | .with_default(DEFAULT_MAX_SPEED)
112 | .done();
113 |
114 | builder
115 | .add_property::(PROPERTY_ROLL_SPEED)
116 | .with_getter(|s: &Self, _| s.roll_speed)
117 | .with_setter(|s: &mut Self, _, value: f32| s.roll_speed = value)
118 | .with_default(DEFAULT_ROLL_SPEED)
119 | .done();
120 | }
121 | }
122 |
123 | // the additional values passed to godot functions, that are not mentioned in
124 | // the video, are listed in the api documentation and are defaults in gdscript
125 |
126 | #[methods]
127 | impl Player {
128 | #[export]
129 | fn _ready(&mut self, owner: TRef) {
130 | let owner_ref = owner.as_ref();
131 |
132 | child_node! { animation_tree: AnimationTree = owner_ref["AnimationTree"] }
133 | get_parameter! { animation_state: AnimationPlayback = animation_tree[@"playback"] }
134 |
135 | animation_tree.set_active(true);
136 |
137 | self.animation_tree = Some(animation_tree.claim());
138 | self.animation_state = Some(animation_state.claim());
139 | self.blink_animation = Some(child_node!(claim owner_ref["BlinkAnimationPlayer"]: AnimationPlayer));
140 | self.hurt_box = Some(child_node!(claim owner_ref["HurtBox"]: Node2D));
141 | self.sword = Some(child_node!(claim owner_ref["HitboxPivot/SwordHitbox"]: Area2D));
142 |
143 | load_resource! { scene: PackedScene = "Player/PlayerHurtSound.tscn" {
144 | self.hurt_sound = Some(scene.claim())
145 | } }
146 |
147 | let player_stats = auto_load!("PlayerStats": Node);
148 |
149 | player_stats
150 | .connect(SIGNAL_NO_HEALTH, owner, "_on_Stats_no_health", VariantArray::new_shared(), 1)
151 | .expect("_on_Stats_no_health to connect to player stats");
152 |
153 | self.player_stats = Some(player_stats.claim());
154 | }
155 |
156 | #[export]
157 | fn _physics_process(&mut self, owner: &KinematicBody2D, delta: f32) {
158 | match self.state {
159 | PlayerState::Move =>
160 | self.move_state(owner, delta),
161 | PlayerState::Attack =>
162 | self.attack_state(owner),
163 | PlayerState::Roll =>
164 | self.roll_state(owner)
165 | }
166 | }
167 |
168 | #[export]
169 | fn attack_animation_finished(&mut self, _owner: &KinematicBody2D) {
170 | self.state = PlayerState::Move
171 | }
172 |
173 | #[export]
174 | fn roll_animation_finished(&mut self, _owner: &KinematicBody2D) {
175 | self.velocity = self.velocity * 0.8; // ease sliding past roll animation
176 | self.state = PlayerState::Move
177 | }
178 |
179 | #[inline]
180 | fn attack_state(&mut self, _owner: &KinematicBody2D) {
181 | self.velocity = Vector2::zero();
182 |
183 | assume_safe!(self.animation_state).travel(TRAVEL_ATTACK);
184 | }
185 |
186 | #[inline]
187 | fn move_state(&mut self, owner: &KinematicBody2D, delta: f32) {
188 | let input = Input::godot_singleton();
189 | let mut input_vector = Vector2::zero();
190 |
191 | input_vector.x = (input.get_action_strength(INPUT_RIGHT) -
192 | input.get_action_strength(INPUT_LEFT)) as f32;
193 |
194 | input_vector.y = (input.get_action_strength(INPUT_DOWN) -
195 | input.get_action_strength(INPUT_UP)) as f32;
196 |
197 | if input_vector != Vector2::zero() {
198 | // in the video, the function "normalized" is used, which handles zero condition.
199 | // godot-rust does not have that function, instead there is a try_normalize.
200 | // since we only use the input_vector when it's none zero, I opted to use the
201 | // "normalize" function after the check for zero.
202 | input_vector = input_vector.normalize();
203 |
204 | self.roll_vector = input_vector;
205 |
206 | set_parameter! { ?self.sword; PROPERTY_KNOCK_BACK_VECTOR = input_vector }
207 |
208 | let animation_tree = assume_safe!(self.animation_tree);
209 |
210 | animation_tree.set(blend_position!("Idle"), input_vector);
211 | animation_tree.set(blend_position!("Run"), input_vector);
212 | animation_tree.set(blend_position!("Attack"), input_vector);
213 | animation_tree.set(blend_position!("Roll"), input_vector);
214 |
215 | assume_safe!(self.animation_state).travel(TRAVEL_RUN);
216 |
217 | self.velocity = self.velocity.move_towards(input_vector * self.max_speed, self.acceleration * delta);
218 | } else {
219 | assume_safe!(self.animation_state).travel(TRAVEL_IDLE);
220 |
221 | self.velocity = self.velocity.move_towards(Vector2::zero(), self.friction * delta);
222 | }
223 |
224 | self.move_player(owner);
225 |
226 | if input.is_action_just_pressed(INPUT_ROLL) {
227 | self.state = PlayerState::Roll
228 | }
229 |
230 | if input.is_action_just_pressed(INPUT_ATTACK) {
231 | self.state = PlayerState::Attack
232 | }
233 | }
234 |
235 | #[inline]
236 | fn roll_state(&mut self, owner: &KinematicBody2D) {
237 | self.velocity = self.roll_vector * self.roll_speed;
238 |
239 | assume_safe!(self.animation_state).travel(TRAVEL_ROLL);
240 |
241 | self.move_player(owner);
242 | }
243 |
244 | #[inline]
245 | fn move_player(&mut self, owner: &KinematicBody2D) {
246 | // FRAC_PI_4 was suggested by c-lion ide as an approximate constant of the
247 | // documented default value of 0.785398 for "floor_max_angle"
248 |
249 | self.velocity = owner.move_and_slide(self.velocity, Vector2::zero(), false, 4, FRAC_PI_4, true);
250 | }
251 |
252 | #[export]
253 | #[allow(non_snake_case)]
254 | fn _on_HurtBox_area_entered(&mut self, owner: &KinematicBody2D, _area: Ref) {
255 | // enemy hit box does not have damage, the video "fix" causes a bug
256 | // let damage = get_parameter!(area[PROPERTY_DAMAGE]).to_i64();
257 | let player_stats = self.player_stats.unwrap();
258 | let health = get_parameter!(player_stats; PROPERTY_HEALTH).to_i64();
259 |
260 | set_parameter!(player_stats; PROPERTY_HEALTH = health - 1);
261 |
262 | call!(self.hurt_box; METHOD_START_INVINCIBILITY(0.5.to_variant()));
263 | call!(self.hurt_box; METHOD_PLAY_HIT_EFFECT);
264 |
265 | let scene = assume_safe!(self.hurt_sound);
266 |
267 | assume_safe! {
268 | let instance: Node = scene.instance(PackedScene::GEN_EDIT_STATE_DISABLED),
269 | let root: SceneTree = Node::get_tree(owner),
270 | let scene: Node = root.current_scene() => {
271 | scene.add_child(instance, false);
272 | }
273 | }
274 | }
275 |
276 | #[export]
277 | #[allow(non_snake_case)]
278 | fn _on_HurtBox_invincibility_ended(&self, _owner: &KinematicBody2D) {
279 | assume_safe!(self.blink_animation).play("Stop", -1.0, 1.0, false);
280 | }
281 |
282 | #[export]
283 | #[allow(non_snake_case)]
284 | fn _on_HurtBox_invincibility_started(&self, _owner: &KinematicBody2D) {
285 | assume_safe!(self.blink_animation).play("Start", -1.0, 1.0, false);
286 | }
287 |
288 | #[export]
289 | #[allow(non_snake_case)]
290 | fn _on_Stats_no_health(&self, owner: &KinematicBody2D) {
291 | owner.queue_free();
292 | }
293 | }
294 |
--------------------------------------------------------------------------------
/action-rpg-rs/src/player_camera.rs:
--------------------------------------------------------------------------------
1 | use gdnative::api::*;
2 | use gdnative::prelude::*;
3 |
4 | use crate::child_node;
5 |
6 | #[derive(NativeClass)]
7 | #[inherit(Camera2D)]
8 | pub struct PlayerCamera;
9 |
10 | impl PlayerCamera {
11 | fn new(_owner: &Camera2D) -> Self {
12 | PlayerCamera
13 | }
14 | }
15 |
16 | #[methods]
17 | impl PlayerCamera {
18 | #[export]
19 | fn _ready(&mut self, owner: &Camera2D) {
20 | let top_left = child_node!(owner["Limits/TopLeft"]: Position2D).position();
21 |
22 | owner.set("limit_top", top_left.y);
23 | owner.set("limit_left", top_left.x);
24 |
25 | let bottom_right = child_node!(owner["Limits/BottomRight"]: Position2D).position();
26 |
27 | owner.set("limit_bottom", bottom_right.y);
28 | owner.set("limit_right", bottom_right.x);
29 | }
30 | }
--------------------------------------------------------------------------------
/action-rpg-rs/src/player_detection.rs:
--------------------------------------------------------------------------------
1 | use gdnative::api::*;
2 | use gdnative::prelude::*;
3 |
4 | pub(crate) const METHOD_CAN_SEE_PLAYER: &str = "can_see_player";
5 | pub(crate) const METHOD_GET_PLAYER: &str = "get_player";
6 |
7 | pub(crate) const PROPERTY_PLAYER: &str = "player";
8 |
9 | #[derive(NativeClass)]
10 | #[inherit(Area2D)]
11 | #[register_with(Self::register)]
12 | pub struct PlayerDetectionZone {
13 | #[property(no_editor)]
14 | player: Option][>,
15 | }
16 |
17 | impl PlayerDetectionZone {
18 | fn new(_owner: &Area2D) -> Self {
19 | PlayerDetectionZone {
20 | player: None
21 | }
22 | }
23 |
24 | fn register(builder: &ClassBuilder) {
25 | builder
26 | .add_property::]