From c4d87f0f8a9a700cbd3936ca1b7b83a24b17f236 Mon Sep 17 00:00:00 2001
From: Sara <sara@saragerretsen.nl>
Date: Mon, 9 Sep 2024 11:13:59 +0200
Subject: [PATCH] feat: implemented utility item locks

---
 godot/Animation/bean_characters.res | Bin 1546 -> 1774 bytes
 godot/GameObjects/player_unit.tscn  |   2 +-
 godot/Levels/test_level.tscn        |  21 +++++++------
 godot/project.godot                 |  10 +++++++
 src/register_types.cpp              |   1 +
 src/rts_actions.cpp                 |  44 ++++++++++++++++++++++++----
 src/rts_actions.hpp                 |   8 +++++
 src/rts_player.cpp                  |  15 ++++++++++
 src/rts_player.hpp                  |  10 ++++++-
 src/rts_states.cpp                  |   8 +++--
 src/rts_states.hpp                  |   4 +--
 src/unit_world_state.hpp            |   1 -
 src/utility_lock.cpp                |  10 +++++++
 src/utility_lock.hpp                |   3 ++
 14 files changed, 116 insertions(+), 21 deletions(-)

diff --git a/godot/Animation/bean_characters.res b/godot/Animation/bean_characters.res
index 122437412da1b051ca9ccfb8b8095a254b3e7b36..0dfb73971aacf8ec9a61428cff9a46020fbc1403 100644
GIT binary patch
delta 1729
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
XLoGlr$4kuQAVTX{EIqsaQd2`itt09+

delta 1507
zcmV<91swYB4T=nZQd2`i0ssI201yBGIu-x`y8{3KGXekrD77#BU;qz|9RPNrc}Vci
zHXY1}I?{1ctkxQzxxsqzy74)>NcM^B#N7R??S4Vq?b?)4(pG%Hi>XoK8Ce@7>zas+
zwx-5U&JOUzz)a|j&FQE#odQ<?UI1+XP4et`&;G1OuQ&34M@O%}%A&SO9^R3=LF<Zl
z(){N*3}0GB7cDLS5BSD_>u>V4(?Mwa!GDkcsN0N)07WP%mH*gVl`D=|GpTr)6z78f
zAOAJ}U;i`yr<_2il@W9u6vsQ>=@i44LGM3+uIsw4>uxF5&vG2caU8d8+qP|+rfHg{
zX|~lXt~?-rFKgc!UM|5Nad<Y^x6+anvv=9LC)CNVKCZXyYa67bS5mt1@aD`O{|9)b
z7M2wiQVPn6Nr|e5WHfS#gn(py87x^T7ZO&eHkdf@GXg>j(ygjzat*QEHYr{ckB_`r
zBkvISj}e0*qraiCp^5OXTx93AD~zpuKdu~p-sHxAIhisR*ICi3pd;w398`r>sKdXI
zr=$Wb1fdTPQ3pq8_3^#zb&pND<W!!t<mU0(vJhZkU?2@|lQpB3lxTv1nUac)%al}X
zbQLLlyttTNYGGL^adLA)mEB~J@d3dBQz50GoS2lTP>{HAT^Gsd5(xpxel#?gQXfK$
zQZGDz_+Z!!5Ks#2YLHam#l;kW`KW<aG9)4+1t~}&jREii5g{Sb0}^1P7)nA2)d>g^
zNC85qM#522QczHkWDHhM8FFEaip<mHx&%!?K1qg?t)B580hb}k<YUXZHk0k9UHp5u
z;P5AqnuGPl6&R{tcwviE5!PUaO-gvfenGK+k=Fc0uu(!FZ9!U31{&Csq68@yVfG0V
zF>0`HOvY1zgQhZeh0(|({}HP)JH8vKolKnC1T>5lP<=c(hEzoHm43vfY;S;q=mBes
zCXY_z1h5WepdjY3(CiQXX~XM2KZbrT!r#A_YWQ6?ji9Bp<@Hx!Jpl`b??%CERA%IV
zkUX?qaEttv1^CDqc4HbOGTCjwW#uc8-SLQlNvt{+6oU01{%{k*ro^De|41b9G(0M;
zS6_^;=1sNt&e$-6*><@B;U|fEtM&Ok$Wi4XEuh0}^|2vGh?sm6lral{ZMLB(v|ruV
z3tH*n+Fs&viaj}=<Ki62N+!!&9NA!h`4jM2>eB@({5k2Enne~{<z%^nKTjrXZ(YuT
zl-@_dWFN!w#z!=^;DbHAK{tDNkD7jfA%OMr+gK3b3(Vge{^QTin(~0^d`jb;jbBUZ
za=b7VcM7j6fjlU+Fa2OT1x*nE^b%h)WB?NY000626afGL02KfL02TrR7Z?DOGXyAq
z82|tP1poj53;+NCaBp*IbZKvHUo&I?8XFuP9v>6{0Dv^@zjr@R>~X$8Z-77a4H+OI
zA|oUvChSkTzAM3=KiXLFKIBijzHptQKhkwdJ|zK@KYpi;C~|LfVJRvrEG;e=Ckg-n
z06}hPZ3lB@Y-V3*WnpXp3k(eo4-gR;001EWHoncwOcE0m6&4p5;3#}O2LJ#7lg|Vg
ze{*GIUt)P-a%Tb&Ole|rVRCsd1yV&*MN|M7WMyG=XaQzva%Eq4Wnpk{ZUSv(Y-MF%
zVRUq1V`~EhQd3exfQhKV-9Sb(Nm|(q-~y2$F?kXIbQn-&#8x5|6cm?7jWh?tH8d(C
zt3{E+n8c!VQ`TvD3|epwImz-@F4GHie^#hOwjnHIhXEh?8_4AcMT9j6++Jb_z4$YM
z5n@bJS;HLFj<|guBG5Q3u`}9w*(!Grm>+%$7a!L*X}!+F-+aa!bn2xqWDMAOx_uiH
zic7Et?)C&lBPf6mufL#h|8{0Ld@oss1bNlhzG(Gkac*6OLXD#nzuwyue}!+KJxhBO
zR)oLBu_UeJ_Rb%R-S5)<%MB4cmr$(m4|aEqAK>O45QNce?k7|;W_6fgRhc?zv!`&W
J@&i&+Lqm4_o{Rth

diff --git a/godot/GameObjects/player_unit.tscn b/godot/GameObjects/player_unit.tscn
index 7e4d1b7..a115082 100644
--- a/godot/GameObjects/player_unit.tscn
+++ b/godot/GameObjects/player_unit.tscn
@@ -27,7 +27,7 @@ collision_mask = 0
 unique_name_in_owner = true
 
 [node name="Planner" type="Planner" parent="."]
-actions_inspector = [0, 1, 2, 3]
+actions_inspector = [0, 1, 2, 3, 6]
 unique_name_in_owner = true
 
 [node name="EntityHealth" type="EntityHealth" parent="."]
diff --git a/godot/Levels/test_level.tscn b/godot/Levels/test_level.tscn
index 8e39291..0eba452 100644
--- a/godot/Levels/test_level.tscn
+++ b/godot/Levels/test_level.tscn
@@ -206,39 +206,40 @@ size = Vector3(39.7404, 10.5501, 8.32035)
 
 [node name="NavigationObstacle3D" type="NavigationObstacle3D" parent="WorldEnvironment/NavigationRegion3D"]
 transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5.64773, 0)
+visible = false
 vertices = PackedVector3Array(-50, 0, 50, -50, 0, -50, 50, 0, -50, 50, 0, 50)
 affect_navigation_mesh = true
 carve_navigation_mesh = true
 
-[node name="UtilityLock" type="UtilityLock" parent="WorldEnvironment"]
+[node name="UtilityLock" type="UtilityLock" parent="WorldEnvironment/NavigationRegion3D"]
 allowed_items = [3]
+animation_name = "activate_crouched"
 goal = SubResource("Goal_yju55")
 transform = Transform3D(0.0142588, 0, 0.999898, 0, 1, 0, -0.999898, 0, 0.0142588, -4.68514, 1.8999e-07, 2.96901)
 collision_layer = 4
 collision_mask = 0
 script = SubResource("GDScript_2bv87")
 
-[node name="CollisionShape3D" type="CollisionShape3D" parent="WorldEnvironment/UtilityLock"]
+[node name="CollisionShape3D" type="CollisionShape3D" parent="WorldEnvironment/NavigationRegion3D/UtilityLock"]
 shape = SubResource("SphereShape3D_pptgx")
 
-[node name="StaticBody3D" type="StaticBody3D" parent="WorldEnvironment/UtilityLock"]
+[node name="StaticBody3D" type="StaticBody3D" parent="WorldEnvironment/NavigationRegion3D/UtilityLock"]
 
-[node name="CollisionShape3D" type="CollisionShape3D" parent="WorldEnvironment/UtilityLock/StaticBody3D"]
+[node name="CollisionShape3D" type="CollisionShape3D" parent="WorldEnvironment/NavigationRegion3D/UtilityLock/StaticBody3D"]
 transform = Transform3D(1, 0, 3.1664e-08, 0, 1, 0, -3.1664e-08, 0, 1, 0.0699434, 0.984067, -2.11005)
 shape = SubResource("BoxShape3D_3h2p0")
 
-[node name="NavigationObstacle3D" type="NavigationObstacle3D" parent="WorldEnvironment/UtilityLock/StaticBody3D"]
+[node name="NavigationObstacle3D" type="NavigationObstacle3D" parent="WorldEnvironment/NavigationRegion3D/UtilityLock/StaticBody3D"]
 transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.0515832, 0.0826392, -2.12334)
-height = 2.0
+height = 4.0
 vertices = PackedVector3Array(-1, 0, 3, -1, 0, -3, 1, 0, -3, 1, 0, 3)
-carve_navigation_mesh = true
 
-[node name="MeshInstance3D" type="MeshInstance3D" parent="WorldEnvironment/UtilityLock"]
+[node name="MeshInstance3D" type="MeshInstance3D" parent="WorldEnvironment/NavigationRegion3D/UtilityLock"]
 transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.136569, 0)
 mesh = SubResource("CylinderMesh_r0j0v")
 surface_material_override/0 = SubResource("StandardMaterial3D_emsq8")
 
-[node name="MeshInstance3D2" type="MeshInstance3D" parent="WorldEnvironment/UtilityLock"]
+[node name="MeshInstance3D2" type="MeshInstance3D" parent="WorldEnvironment/NavigationRegion3D/UtilityLock"]
 transform = Transform3D(0.984294, -0.176535, 0, 0.176535, 0.984294, 0, 0, 0, 1, 0, 0.857907, -1.99146)
 mesh = SubResource("BoxMesh_bl5l6")
 
@@ -394,3 +395,5 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -11.0413, -7.63685e-07, 2.867
 
 [node name="Tank4" parent="." instance=ExtResource("4_0o33v")]
 transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -14.4329, -7.63685e-07, 6.82909)
+
+[connection signal="finish_activate" from="WorldEnvironment/NavigationRegion3D/UtilityLock" to="WorldEnvironment/NavigationRegion3D/UtilityLock" method="_on_finish_activate"]
diff --git a/godot/project.godot b/godot/project.godot
index fe35f0a..41b9b71 100644
--- a/godot/project.godot
+++ b/godot/project.godot
@@ -87,6 +87,16 @@ DEBUG_toggle_debug={
 "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":96,"key_label":0,"unicode":96,"location":0,"echo":false,"script":null)
 ]
 }
+zoom_in={
+"deadzone": 0.5,
+"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":8,"position":Vector2(179, 12),"global_position":Vector2(188, 58),"factor":1.0,"button_index":4,"canceled":false,"pressed":true,"double_click":false,"script":null)
+]
+}
+zoom_out={
+"deadzone": 0.5,
+"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":16,"position":Vector2(227, 19),"global_position":Vector2(236, 65),"factor":1.0,"button_index":5,"canceled":false,"pressed":true,"double_click":false,"script":null)
+]
+}
 
 [layer_names]
 
diff --git a/src/register_types.cpp b/src/register_types.cpp
index c1c18fc..b5dd65f 100644
--- a/src/register_types.cpp
+++ b/src/register_types.cpp
@@ -45,6 +45,7 @@ void initialize_gdextension_types(gd::ModuleInitializationLevel p_level)
     goap::ActionDB::register_action<GetInRange>();
     goap::ActionDB::register_action<TankSelfHeal>();
     goap::ActionDB::register_action<TakeCover>();
+    goap::ActionDB::register_action<ActivateLock>();
 
     // same for items,
     // make sure ItemDB::get_enum_hint is fully populated out before _bind_methods.
diff --git a/src/rts_actions.cpp b/src/rts_actions.cpp
index 4a90b51..660bb24 100644
--- a/src/rts_actions.cpp
+++ b/src/rts_actions.cpp
@@ -2,6 +2,7 @@
 #include "nav_marker.hpp"
 #include "nav_room.hpp"
 #include "rts_states.hpp"
+#include "utility_lock.hpp"
 #include "goap/actor_world_state.hpp"
 #include "goap/state.hpp"
 #include <godot_cpp/core/memory.hpp>
@@ -19,11 +20,10 @@ goap::State *MoveToTarget::get_apply_state(goap::ActorWorldState *context) const
     if(target == nullptr) {
         gd::UtilityFunctions::push_warning("Failed to get target node of ", context->get_path());
         return nullptr;
-    } else {
-        MoveTo *state{this->create_state<MoveTo>()};
-        state->target_node = target;
-        return state;
     }
+    MoveTo *state{this->create_state<MoveTo>()};
+    state->target_node = target;
+    return state;
 }
 
 UseWeapon::UseWeapon()
@@ -140,6 +140,40 @@ float TakeCover::score_cover_marker(class NavMarker* marker, const gd::Vector3&
     float const context_distance{marker_position.distance_to(context_position)};
     // score is a weighted comparison between distances to the target and context
     // marker distance from context grows faster than marker distance from target
-    return (target_distance) - (context_distance);
+    return target_distance - context_distance;
 }
 
+ActivateLock::ActivateLock() {
+    this->required.insert("is_at_target", true);
+    this->effects.insert("is_target_activated", true);
+}
+
+bool ActivateLock::procedural_is_possible(goap::ActorWorldState *context) const {
+    Unit *context_unit{gd::Object::cast_to<Unit>(context->get_parent())};
+    if(context_unit == nullptr)
+        return false;
+    UnitWorldState *unit_context{context_unit->get_world_state()};
+    if(unit_context == nullptr)
+        return false;
+    UtilityLock *lock{gd::Object::cast_to<UtilityLock>(unit_context->get_target_node())};
+    if(lock == nullptr)
+        return false;
+    return lock->can_use(context_unit->get_node<Inventory>("%Inventory")->get_utility(), context_unit);
+}
+
+goap::State *ActivateLock::get_apply_state(goap::ActorWorldState *context) const {
+    UnitWorldState *unit_context{gd::Object::cast_to<UnitWorldState>(context)};
+    if(unit_context == nullptr) {
+        gd::UtilityFunctions::push_error("ActivateLock: Context '", context->get_path(), "' is not a UnitWorldState");
+        return nullptr;
+    }
+    UtilityLock *lock{gd::Object::cast_to<UtilityLock>(unit_context->get_target_node())};
+    if(lock == nullptr) {
+        gd::UtilityFunctions::push_error("ActivateLock: Target for '", context->get_path(), "' is not a UtilityLock");
+    }
+    Activate *activate = this->create_state<Activate>();
+    activate->lock = lock;
+    if(Inventory *inventory{unit_context->get_node<Inventory>("%Inventory")})
+        activate->using_item = inventory->get_utility();
+    return activate;
+}
diff --git a/src/rts_actions.hpp b/src/rts_actions.hpp
index 8c9af95..23b8640 100644
--- a/src/rts_actions.hpp
+++ b/src/rts_actions.hpp
@@ -50,4 +50,12 @@ private:
     float score_cover_marker(class NavMarker *marker, gd::Vector3 const &target_position, gd::Vector3 const &context_position) const;
 };
 
+class ActivateLock : public goap::Action {
+    GOAP_ACTION(ActivateLock);
+public:
+    ActivateLock();
+    virtual bool procedural_is_possible(goap::ActorWorldState *context) const override;
+    virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override;
+};
+
 #endif // !RTS_ACTIONS_HPP
diff --git a/src/rts_player.cpp b/src/rts_player.cpp
index a123b57..5ec4d61 100644
--- a/src/rts_player.cpp
+++ b/src/rts_player.cpp
@@ -18,6 +18,9 @@ void RTSPlayer::_bind_methods() {
 
 void RTSPlayer::_ready() {
     this->camera = this->get_node<gd::Camera3D>("Camera3D");
+    this->local_zoom_direction = this->camera->get_position().normalized();
+    this->current_zoom_level = this->max_zoom_level = this->camera->get_position().length();
+    this->set_zoom_level(this->current_zoom_level);
 }
 
 void RTSPlayer::_process(double delta_time) {
@@ -41,6 +44,7 @@ void RTSPlayer::setup_player_input(utils::PlayerInput *input) {
     input->listen_to("rotate_left", "rotate_right", callable_mp(this, &RTSPlayer::on_rotate_horizontal));
     input->listen_to("_mouse_left", "_mouse_right", callable_mp(this, &RTSPlayer::on_mouse_horizontal));
     input->listen_to("_mouse_down", "_mouse_up", callable_mp(this, &RTSPlayer::on_mouse_vertical));
+    input->listen_to("zoom_in", "zoom_out", callable_mp(this, &RTSPlayer::on_zoom));
 #ifdef DEBUG_ENABLED
     input->listen_to("DEBUG_toggle_debug", callable_mp(this, &RTSPlayer::DEBUG_enable_debug));
 #endif
@@ -139,6 +143,14 @@ void RTSPlayer::select_unit(Unit *unit) {
     unit->connect("tree_exited", this->on_selected_unit_destroyed);
 }
 
+void RTSPlayer::set_zoom_level(float level) {
+    this->current_zoom_level = gd::Math::clamp(level, this->min_zoom_level, this->max_zoom_level);
+    this->camera->set_position(this->local_zoom_direction * this->current_zoom_level);
+    gd::Vector3 const forward_vector{(this->camera->get_global_position() - this->get_global_position()).normalized()};
+    gd::Vector3 const left_vector{gd::Vector3{0.f, 1.f, 0.f}.cross(forward_vector)};
+    this->camera->set_global_basis({left_vector, forward_vector.cross(left_vector), forward_vector});
+}
+
 void RTSPlayer::on_mouse_horizontal(gd::Ref<gd::InputEvent>, float value) {
     if(this->mmb_down)
         this->camera_mouse_motion.x = value;
@@ -182,6 +194,9 @@ void RTSPlayer::on_lclick(gd::Ref<gd::InputEvent>, float value) {
 void RTSPlayer::on_mclick(gd::Ref<gd::InputEvent>, float value) {
     this->mmb_down = value != 0.f;
 }
+void RTSPlayer::on_zoom(gd::Ref<gd::InputEvent>, float value) {
+    this->set_zoom_level(this->current_zoom_level + value * this->camera_zoom_speed);
+}
 
 gd::Vector3 RTSPlayer::get_forward_direction() const {
     gd::Vector3 forward = this->get_global_basis().get_column(2);
diff --git a/src/rts_player.hpp b/src/rts_player.hpp
index 44ed5c4..7ba9823 100644
--- a/src/rts_player.hpp
+++ b/src/rts_player.hpp
@@ -40,6 +40,7 @@ private:
     void spawn_movement_marker(gd::Vector3 at);
     void clear_selected_unit();
     void select_unit(Unit *unit);
+    void set_zoom_level(float level);
 
     // input functions
     void on_mouse_horizontal(gd::Ref<gd::InputEvent>, float value);
@@ -50,12 +51,15 @@ private:
     void on_lclick(gd::Ref<gd::InputEvent>, float value);
     void on_rclick(gd::Ref<gd::InputEvent>, float value);
     void on_mclick(gd::Ref<gd::InputEvent>, float value);
+    void on_zoom(gd::Ref<gd::InputEvent>, float value);
 
     // setters & getters
     gd::Vector3 get_forward_direction() const;
     gd::Vector3 get_left_direction() const;
     void set_ground_marker_scene(gd::Ref<gd::PackedScene> scene);
     gd::Ref<gd::PackedScene> get_ground_marker_scene() const;
+private:
+    gd::Callable const on_selected_unit_destroyed{callable_mp(this, &RTSPlayer::clear_selected_unit)};
 private:
     Unit* selected_unit{nullptr};
 
@@ -75,13 +79,17 @@ private:
     gd::Vector3 cursor_camera_normal{0.f, 0.f, 0.f};
     gd::Camera3D *camera{nullptr};
     gd::Ref<gd::PackedScene> ground_marker_scene{nullptr};
-    gd::Callable const on_selected_unit_destroyed{callable_mp(this, &RTSPlayer::clear_selected_unit)};
+    gd::Vector3 local_zoom_direction{0.f, 0.f, 0.f};
+    float current_zoom_level{1.f};
+    float max_zoom_level{1.f};
 
+    float const min_zoom_level{3.f};
     float const camera_keys_speed{10.f};
     float const camera_keys_rotation_speed{1.f};
     float const camera_mouse_speed{0.01f};
     float const camera_mouse_rotation_speed{-0.003f};
     double const time_to_held{0.1};
+    float const camera_zoom_speed{1.f};
 #ifdef DEBUG_ENABLED
 private:
     bool DEBUG_debug_enabled{false};
diff --git a/src/rts_states.cpp b/src/rts_states.cpp
index a955bd1..139b61b 100644
--- a/src/rts_states.cpp
+++ b/src/rts_states.cpp
@@ -82,10 +82,14 @@ void Activate::_ready() {
         return;
     }
     // start activating the lock
-    this->lock->begin_activate(this->using_item, this->parent_unit);
+    if(!this->lock->begin_activate(this->using_item, this->parent_unit)) {
+        this->end_state();
+        return;
+    }
+    this->animation = this->lock->get_animation_name();
     // play activate animation
     this->anim = this->parent_unit->get_node<gd::AnimationPlayer>("%AnimationPlayer");
-    if(!this->anim->has_animation(this->animation)) {
+    if(this->anim == nullptr || !this->anim->has_animation(this->animation)) {
         this->end_state();
         return;
     }
diff --git a/src/rts_states.hpp b/src/rts_states.hpp
index fd193d3..88d50cc 100644
--- a/src/rts_states.hpp
+++ b/src/rts_states.hpp
@@ -40,9 +40,9 @@ public:
     virtual void _process(double) override;
     virtual void _end_state() override;
     UtilityLock *lock{nullptr};
-    gd::String animation{};
-private:
     Item const *using_item{nullptr};
+private:
+    gd::String animation{};
     gd::AnimationPlayer *anim{nullptr};
     Unit *parent_unit{nullptr};
 };
diff --git a/src/unit_world_state.hpp b/src/unit_world_state.hpp
index c9f82dc..937ccf2 100644
--- a/src/unit_world_state.hpp
+++ b/src/unit_world_state.hpp
@@ -37,7 +37,6 @@ public:
     bool get_is_health_safe() const;
     gd::Vector3 get_parent_global_position() const;
     gd::Vector3 get_target_global_position() const;
-
     void set_target_node(gd::Node3D *node);
     gd::Node3D *get_target_node() const;
 private:
diff --git a/src/utility_lock.cpp b/src/utility_lock.cpp
index 82012e5..b971794 100644
--- a/src/utility_lock.cpp
+++ b/src/utility_lock.cpp
@@ -1,10 +1,12 @@
 #include "utility_lock.hpp"
 #include "item_db.hpp"
+#include "unit.hpp"
 #include "utils/godot_macros.hpp"
 
 void UtilityLock::_bind_methods() {
 #define CLASSNAME UtilityLock
     GDPROPERTY_HINTED(allowed_items, gd::Variant::ARRAY, gd::PROPERTY_HINT_ARRAY_TYPE, ItemDB::get_array_hint());
+    GDPROPERTY(animation_name, gd::Variant::STRING);
     GDSIGNAL("begin_activate", gd::PropertyInfo(gd::Variant::INT, "with_item"), gd::PropertyInfo(gd::Variant::OBJECT, "by_unit", gd::PROPERTY_HINT_NODE_TYPE, "Unit"));
     GDSIGNAL("finish_activate", gd::PropertyInfo(gd::Variant::INT, "with_item"), gd::PropertyInfo(gd::Variant::OBJECT, "by_unit", gd::PROPERTY_HINT_NODE_TYPE, "Unit"));
     GDSIGNAL("interrupt_activate");
@@ -60,3 +62,11 @@ gd::Array UtilityLock::get_allowed_items() const {
         array.push_back(item == nullptr ? 0 : item->get_id());
     return array;
 }
+
+void UtilityLock::set_animation_name(gd::String animation) {
+    this->animation_name = animation;
+}
+
+gd::String UtilityLock::get_animation_name() const {
+    return this->animation_name;
+}
diff --git a/src/utility_lock.hpp b/src/utility_lock.hpp
index 1811cb8..4d62acf 100644
--- a/src/utility_lock.hpp
+++ b/src/utility_lock.hpp
@@ -29,9 +29,12 @@ public:
 
     void set_allowed_items(gd::Array array);
     gd::Array get_allowed_items() const;
+    void set_animation_name(gd::String animation);
+    gd::String get_animation_name() const;
 private:
     gd::HashSet<Item const *> allowed_items{};
     Unit *user{nullptr};
+    gd::String animation_name{};
     ActivationState activation_state{ActivationState::Inactive};
 };