From a78bbfd3b57d5c0cc19c54f3cf044e2abfe8f36f Mon Sep 17 00:00:00 2001 From: Sara Date: Tue, 16 Jul 2024 19:35:55 +0200 Subject: [PATCH] feat: implemented melee attacks for Tank enemy --- .../{new_goal.tres => defeat_enemy_unit.tres} | 7 +- godot/Animation/bean_characters.res | Bin 0 -> 1361 bytes godot/GameObjects/enemy_unit.tscn | 170 +++--------------- godot/GameObjects/player_unit.tscn | 74 ++------ src/enemy_world_state.cpp | 105 +++++++++++ src/enemy_world_state.hpp | 44 +++++ src/entity_health.cpp | 47 +++-- src/entity_health.hpp | 18 +- src/register_types.cpp | 4 + src/rts_actions.cpp | 44 ++++- src/rts_actions.hpp | 14 ++ src/tank_unit.cpp | 19 ++ src/tank_unit.hpp | 13 ++ src/unit.cpp | 33 ++-- src/unit.hpp | 11 +- src/unit_world_state.cpp | 28 ++- src/unit_world_state.hpp | 6 +- 17 files changed, 369 insertions(+), 268 deletions(-) rename godot/AI/{new_goal.tres => defeat_enemy_unit.tres} (57%) create mode 100644 godot/Animation/bean_characters.res create mode 100644 src/enemy_world_state.cpp create mode 100644 src/enemy_world_state.hpp create mode 100644 src/tank_unit.cpp create mode 100644 src/tank_unit.hpp diff --git a/godot/AI/new_goal.tres b/godot/AI/defeat_enemy_unit.tres similarity index 57% rename from godot/AI/new_goal.tres rename to godot/AI/defeat_enemy_unit.tres index b16a917..a3a9c16 100644 --- a/godot/AI/new_goal.tres +++ b/godot/AI/defeat_enemy_unit.tres @@ -1,6 +1,9 @@ [gd_resource type="Goal" format=3 uid="uid://b4i4e34046n44"] [resource] -desired_state_dict = { -"goal1": true +requirements_dict = { +"is_target_enemy": true +} +desired_state_dict = { +"is_target_dead": true } diff --git a/godot/Animation/bean_characters.res b/godot/Animation/bean_characters.res new file mode 100644 index 0000000000000000000000000000000000000000..2b007e8b93b9269b20618f6a0f9d9acc6dc322bf GIT binary patch literal 1361 zcmV-X1+My1Q$s@n000005C8yG6953a0{{Se0RR9fwJ-f(01u5F0H)w}M*z(z9sK(Y z^xx5r+6)xYwm>PfYq0RYs@gF?2kh^=@4c)YZJ~sa;rxSDS*RsZhbS^3P>97D{EVBC z!3TF^|K=zF2Z>DqRRCT9YXI}})@8|l{SWXSS%%)SLHo`7y%?OYufYnNwl{vlcUI9w zJL&%c=M;1eO1@UMv}`ZQ!|y+Z=Xsvzd6$Y6v|ZPAUDt6O$8j9DZQHhO zn~k-LD-FhJ?JHZCY1pqAmL2x3la3!p2dhjaYs3?j4(QOE*R@#*kH zz@!94G9nUk0{QUh;MiEzm`WO12F9o_hZLsTaWG<40fd1M9T<=}-L7gRSJ239kK*kR zz4YIr;thN8j3;FRX~M9)ur&BZ{%5%DdSYu|kSn*qPjbhkN7M4}QsD6oRk`CJ91^XV!=7@3ryPBJ1A zf>T3BD-sL^6&no}v=hjOM+e8oDuf3KS(cef8d(MgX@oqn4Bq3suy3ZA z4eqB>B7@jZlo&CZ&lMXa1fnUS;bah#o|MQh&&9ZZ!bFU0@{N(}LEy1z4?8d#4dXvz zRffI9mG;R195DK!pA!Jp4j}Lpm59~6y`|KeEhDXu$7TJRR?6;_tvH1S@pZ^nQb5qt{%}L=qxoz z>=LZ=x?`RCuOo{rU(D+==Bd9Id9X5T#3zfb@-#Qbo+pF$pOkrXr19Rd8fUh=_`#el z_+VevFo->A&MiGQJ5VCa(Qd?+3MO42bdO4%4aLKiK`q@GG%f@=CGl2NuztR3C}b$L zFa2Ou0)-0zR1N?D02TlM00IC20096102crN03iSX02ly(KLCI~&CE~X$8Z-77a4H+LGAtECrCG1bSzAM3=KiXLFKIBijzHptQKhkwdJ|zK@ zKYpi;CvtCeVJImoD=aM-CJ_Jt06}hPZA@uma$$0LE(8Vu002@&Q$w&>cV%I4Z*Bq&0001OWo%_-Utx4~VPk7jQ&K}is8MBs%mg)QEE&eo0+Aun9TEU@ z6c&a8-At&kuw4H;r74lXhRidrKQL18S7`;L2*HwtaYA7unb$e$3z2WF=cN1myXCrJ z)3 + +void EnemyWorldState::_bind_methods() { +#define CLASSNAME EnemyWorldState + GDPROPERTY_HINTED(editor_available_goals, + gd::Variant::ARRAY, + gd::PROPERTY_HINT_ARRAY_TYPE, + GDRESOURCETYPE("Goal")); +} + +void EnemyWorldState::_ready() { + this->awareness_area = this->get_node("%AwarenessArea"); + this->awareness_area->connect("body_entered", callable_mp(this, &EnemyWorldState::on_awareness_entered)); + this->awareness_area->connect("body_exited", callable_mp(this, &EnemyWorldState::on_awareness_exited)); + this->health = this->get_node("%EntityHealth"); + if(this->health == nullptr) + this->health->connect("damage", callable_mp(this, &EnemyWorldState::on_damaged)); +} + +void EnemyWorldState::on_awareness_entered(gd::Node3D *node) { + gd::UtilityFunctions::print("1) object entered awareness"); + Unit *unit{gd::Object::cast_to(node)}; + if(unit == nullptr) return; + if(unit == this->parent_unit) return; + gd::UtilityFunctions::print("2) object was Unit"); + this->known_enemies.push_back(unit); + this->try_set_target(this->select_target_from_known()); +} + +void EnemyWorldState::on_awareness_exited(gd::Node3D *node) { + Unit *unit{gd::Object::cast_to(node)}; + this->known_enemies.erase(unit); +} + +void EnemyWorldState::on_damaged(int remaining, int delta, Unit *source) { + float highest_priority{0.f}; + Unit *highest_priority_unit{this->select_target_from_known_with_priority(&highest_priority)}; + float const priority{this->calculate_priority(source)}; + if(priority >= highest_priority) + this->try_set_target(source); +} + +Unit *EnemyWorldState::select_target_from_known_with_priority(float *out_priority) { + Unit *out{nullptr}; + *out_priority = -INFINITY; + for(Unit *unit : this->known_enemies) { + float const priority{this->calculate_priority(unit)}; + if(priority > *out_priority) { + out = unit; + *out_priority = priority; + } + } + return out; +} + +Unit *EnemyWorldState::select_target_from_known() { + float dummy{}; + return this->select_target_from_known_with_priority(&dummy); +} + +float EnemyWorldState::calculate_priority(Unit *target) { + return target->get_global_position().distance_squared_to(this->parent_unit->get_global_position()); +} + +gd::Ref EnemyWorldState::get_goal_for_target(Unit *unit) { + gd::Node3D *store{this->target_node}; + this->target_node = unit; + for(gd::Ref const &goal : this->available_goals) { + if(goal->check_requirements_met(this)) { + this->target_node = store; + return goal; + } + } + this->target_node = store; + return nullptr; +} + +void EnemyWorldState::try_set_target(Unit *unit) { + gd::UtilityFunctions::print("3) selecting goal"); + gd::Ref goal{this->get_goal_for_target(unit)}; + if(!goal.is_valid()) return; + gd::UtilityFunctions::print("4) selected goal ", goal->get_path()); + this->parent_unit->set_target_goal(unit, goal); +} + +void EnemyWorldState::set_editor_available_goals(gd::Array array) { + this->available_goals.clear(); + while(!array.is_empty()) { + gd::Ref goal{array.pop_back()}; + this->available_goals.push_back(goal); + } +} + +gd::Array EnemyWorldState::get_editor_available_goals() const { + gd::Array a{}; + for(gd::Ref const &goal : this->available_goals) + a.push_back(goal); + return a; +} diff --git a/src/enemy_world_state.hpp b/src/enemy_world_state.hpp new file mode 100644 index 0000000..c288e80 --- /dev/null +++ b/src/enemy_world_state.hpp @@ -0,0 +1,44 @@ +#ifndef ENEMY_WORLD_STATE_HPP +#define ENEMY_WORLD_STATE_HPP + +#include "unit_world_state.hpp" +#include "goap/goal.hpp" +#include +#include +#include + +namespace gd = godot; + +class EntityHealth; + +class EnemyWorldState : public UnitWorldState { + GDCLASS(EnemyWorldState, UnitWorldState); + static void _bind_methods(); +public: + virtual void _ready() override; + + void on_awareness_entered(gd::Node3D *node); + void on_awareness_exited(gd::Node3D *node); + void on_damaged(int remaining, int delta, Unit *source); + /*! Select the target with the highest priority and return it. + * Assigns the priority of found target to out_priority. + * \param out_priority Assigned to highest found priority, cannot be nullptr. + */ + Unit *select_target_from_known_with_priority(float *out_priority); + //! Shorthand for select_target_from_known_with_priority(nullptr) + Unit *select_target_from_known(); +private: + float calculate_priority(Unit *target); + gd::Ref get_goal_for_target(Unit *unit); + void try_set_target(Unit *target); + void set_editor_available_goals(gd::Array array); + gd::Array get_editor_available_goals() const; +private: + gd::Vector> available_goals{}; + gd::Vector known_enemies{}; + + gd::Area3D *awareness_area{nullptr}; + EntityHealth *health{nullptr}; +}; + +#endif // !ENEMY_WORLD_STATE_HPP diff --git a/src/entity_health.cpp b/src/entity_health.cpp index f578ee3..a88e240 100644 --- a/src/entity_health.cpp +++ b/src/entity_health.cpp @@ -4,9 +4,9 @@ void EntityHealth::_bind_methods() { #define CLASSNAME EntityHealth - GDPROPERTY(max_health, gd::Variant::INT); + GDPROPERTY(injury_max, gd::Variant::INT); + GDPROPERTY(wounds_max, gd::Variant::INT); - gd::PropertyInfo const source_arg{}; GDSIGNAL("damage", gd::PropertyInfo(gd::Variant::INT, "remaining"), gd::PropertyInfo(gd::Variant::INT, "change"), @@ -22,39 +22,48 @@ void EntityHealth::_bind_methods() { } void EntityHealth::_enter_tree() { - this->current_health = this->max_health; - this->emit_signal("damage", this->current_health, 0, nullptr); + this->injury_current = this->injury_max; + this->wounds_current = this->wounds_max; + this->emit_signal("damage", this->injury_current, 0, nullptr); } void EntityHealth::damaged_by(int amount, Unit *source) { amount = gd::Math::abs(amount); - this->current_health -= amount; - this->emit_signal("damage", this->current_health, amount, source); - this->emit_signal("health_changed", this->current_health, -amount); - if(this->current_health <= 0) { + this->injury_current -= amount; + this->emit_signal("damage", this->injury_current, amount, source); + this->emit_signal("health_changed", this->injury_current, -amount); + if(this->injury_current <= 0) { this->emit_signal("death", source); } } void EntityHealth::healed_by(int amount, Unit *source) { amount = gd::Math::abs(amount); - this->current_health = gd::Math::min(this->max_health, this->current_health + amount); - this->emit_signal("heal", this->current_health, amount, source); - this->emit_signal("health_changed", this->current_health, amount); + this->injury_current = gd::Math::min(this->injury_max, this->injury_current + amount); + this->emit_signal("heal", this->injury_current, amount, source); + this->emit_signal("health_changed", this->injury_current, amount); } -void EntityHealth::set_max_health(int max_health) { - this->max_health = max_health; +void EntityHealth::set_injury_max(int max_health) { + this->injury_max = max_health; } -int EntityHealth::get_max_health() const { - return this->max_health; +int EntityHealth::get_injury_max() const { + return this->injury_max; } -void EntityHealth::set_current_health(int current_health) { - this->current_health = current_health; +int EntityHealth::get_injury_current() const { + return this->injury_current; } -int EntityHealth::get_current_health() const { - return this->current_health; +void EntityHealth::set_wounds_max(int max_health) { + this->wounds_max = max_health; +} + +int EntityHealth::get_wounds_max() const { + return this->wounds_max; +} + +int EntityHealth::get_wounds_current() const { + return this->wounds_current; } diff --git a/src/entity_health.hpp b/src/entity_health.hpp index ed91fd8..5fc71b3 100644 --- a/src/entity_health.hpp +++ b/src/entity_health.hpp @@ -16,13 +16,19 @@ public: void damage(int amount); void healed_by(int amount, Unit *source); void heal(int amount); - void set_max_health(int max_health); - int get_max_health() const; - void set_current_health(int current_health); - int get_current_health() const; + void set_injury_max(int max_health); + int get_injury_max() const; + int get_injury_current() const; + void set_wounds_max(int max_health); + int get_wounds_max() const; + int get_wounds_current() const; private: - int max_health{100}; - int current_health{0}; + // long term health + int injury_max{10}; + int injury_current{0}; + // short term health + int wounds_max{10}; + int wounds_current{0}; }; #endif // !RTS_ENTITY_HEALTH_HPP diff --git a/src/register_types.cpp b/src/register_types.cpp index ba1de6b..28f6053 100644 --- a/src/register_types.cpp +++ b/src/register_types.cpp @@ -1,4 +1,5 @@ #include "register_types.h" +#include "enemy_world_state.hpp" #include "entity_health.hpp" #include "goap/state.hpp" #include "rts_actions.hpp" @@ -32,6 +33,8 @@ void initialize_gdextension_types(ModuleInitializationLevel p_level) goap::ActionDB::register_action(); goap::ActionDB::register_action(); goap::ActionDB::register_action(); + goap::ActionDB::register_action(); + goap::ActionDB::register_action(); ClassDB::register_class(); ClassDB::register_class(); @@ -43,6 +46,7 @@ void initialize_gdextension_types(ModuleInitializationLevel p_level) ClassDB::register_class(); ClassDB::register_class(); + ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); diff --git a/src/rts_actions.cpp b/src/rts_actions.cpp index 2de56b0..38a953f 100644 --- a/src/rts_actions.cpp +++ b/src/rts_actions.cpp @@ -7,16 +7,16 @@ MoveToTarget::MoveToTarget() : Action() { - effects.insert("is_at_target", true); + this->effects.insert("is_at_target", true); } goap::State *MoveToTarget::get_apply_state(goap::ActorWorldState *context) const { - gd::Node3D *target = gd::Object::cast_to(context->get_world_property("target_node")); + gd::Node3D *target{gd::Object::cast_to(context->get_world_property("target_node"))}; if(target == nullptr) { gd::UtilityFunctions::push_warning("Failed to get target node of action ", get_static_class()); return nullptr; } else { - MoveTo *state = this->create_state(); + MoveTo *state{this->create_state()}; state->target_node = target; return state; } @@ -24,24 +24,50 @@ goap::State *MoveToTarget::get_apply_state(goap::ActorWorldState *context) const FireAtTarget::FireAtTarget() : Action() { - effects.insert("is_target_dead", true); - required.insert("can_see_target", true); + this->effects.insert("is_target_dead", true); + this->required.insert("can_see_target", true); } goap::State *FireAtTarget::get_apply_state(goap::ActorWorldState *context) const { - Animate *state = this->create_state(); + Animate *state{this->create_state()}; state->animation = "fire_weapon"; return state; } FindTarget::FindTarget() : Action() { - effects.insert("can_see_target", true); + this->effects.insert("can_see_target", true); } goap::State *FindTarget::get_apply_state(goap::ActorWorldState *context) const { - gd::Node3D *target = gd::Object::cast_to(context->get_world_property("target_node")); - MoveTo *state = this->create_state(); + gd::Node3D *target{gd::Object::cast_to(context->get_world_property("target_node"))}; + MoveTo *state{this->create_state()}; state->target_node = target; return state; } + +GetInMeleeRange::GetInMeleeRange() +: Action() { + this->effects.insert("is_in_melee_range", true); + this->required.insert("can_see_target", true); +} + +goap::State *GetInMeleeRange::get_apply_state(goap::ActorWorldState *context) const { + gd::Node3D *target{gd::Object::cast_to(context->get_world_property("target_node"))}; + MoveTo *state{this->create_state()}; + state->target_node = target; + return state; +} + +MeleeAttack::MeleeAttack() +: Action() { + this->effects.insert("is_target_dead", true); + this->required.insert("can_see_target", true); + this->required.insert("is_in_melee_range", true); +} + +goap::State *MeleeAttack::get_apply_state(goap::ActorWorldState *context) const { + Animate *state{this->create_state()}; + state->animation = "melee_attack"; + return state; +} diff --git a/src/rts_actions.hpp b/src/rts_actions.hpp index 8238a8e..0a7119c 100644 --- a/src/rts_actions.hpp +++ b/src/rts_actions.hpp @@ -25,4 +25,18 @@ public: virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override; }; +class GetInMeleeRange : public goap::Action { + GOAP_ACTION(GetInMeleeRange); +public: + GetInMeleeRange(); + virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override; +}; + +class MeleeAttack : public goap::Action { + GOAP_ACTION(MeleeAttack); +public: + MeleeAttack(); + virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override; +}; + #endif // !RTS_ACTIONS_HPP diff --git a/src/tank_unit.cpp b/src/tank_unit.cpp new file mode 100644 index 0000000..fc7b902 --- /dev/null +++ b/src/tank_unit.cpp @@ -0,0 +1,19 @@ +#include "tank_unit.hpp" +#include "entity_health.hpp" + +void TankUnit::_bind_methods() {} + +void TankUnit::use_weapon() { + gd::Node3D *target{this->world_state->get_target_node()}; + if(target == nullptr) + return; + if(!this->world_state->get_can_see_target()) + return; + if(1.f < target->get_global_position().distance_squared_to(this->get_global_position())) + return; + this->aim_at(target); + EntityHealth *health = target->get_node("EntityHealth"); + if(health == nullptr) + return; + health->damaged_by(2, this); +} diff --git a/src/tank_unit.hpp b/src/tank_unit.hpp new file mode 100644 index 0000000..dd4e037 --- /dev/null +++ b/src/tank_unit.hpp @@ -0,0 +1,13 @@ +#ifndef TANK_UNIT_HPP +#define TANK_UNIT_HPP + +#include "unit.hpp" + +class TankUnit : public Unit { + GDCLASS(TankUnit, Unit); + static void _bind_methods(); +public: + virtual void use_weapon() override; +}; + +#endif // !TANK_UNIT_HPP diff --git a/src/unit.cpp b/src/unit.cpp index ecfad33..66e991c 100644 --- a/src/unit.cpp +++ b/src/unit.cpp @@ -9,18 +9,18 @@ void Unit::_bind_methods() { #define CLASSNAME Unit GDSIGNAL("goal_finished"); + GDSIGNAL("plan_failed"); GDPROPERTY_HINTED(configure_team, gd::Variant::INT, gd::PROPERTY_HINT_ENUM, UnitTeam::get_property_hint()); - GDFUNCTION(fire_at_target); + GDFUNCTION(use_weapon); } void Unit::_enter_tree() { GDGAMEONLY(); - this->agent = this->get_node("NavigationAgent3D"); - this->planner = this->get_node("Planner"); - this->world_state = this->get_node("ActorWorldState"); - this->eyes = this->get_node("%Eyes"); + this->agent = this->get_node("%NavigationAgent3D"); + this->planner = this->get_node("%Planner"); + this->world_state = this->get_node("%ActorWorldState"); this->world_state->connect("attention_changed", callable_mp(this, &Unit::stop_plan)); - this->anim_player = this->get_node("AnimationPlayer"); - EntityHealth *health{this->get_node("EntityHealth")}; + this->anim_player = this->get_node("%AnimationPlayer"); + EntityHealth *health{this->get_node("%EntityHealth")}; health->connect("death", callable_mp(this, &Unit::on_death)); } @@ -30,11 +30,13 @@ void Unit::stop_plan() { if(this->state && !this->state->is_queued_for_deletion()) this->destroy_state(); this->state = nullptr; + this->emit_signal("plan_failed"); } void Unit::begin_marker_temporary(GoalMarker *marker) { this->destroy_state(); this->world_state->set_target_node(marker); + this->destroy_state(); this->set_goal_and_plan(marker->get_goal()); // destroy temporary marker if goal is already achieved or failed // connect observers if a plan was formed @@ -58,15 +60,15 @@ void Unit::begin_goal(gd::Ref goal) { this->next_action(); } -void Unit::fire_at_target() { +void Unit::use_weapon() { gd::Node3D *target{this->world_state->get_target_node()}; - if(!target) + if(target == nullptr) return; this->aim_at(target); - EntityHealth *health = target->get_node("EntityHealth"); - if(!health) + EntityHealth *health{target->get_node("EntityHealth")}; + if(health == nullptr) return; - health->damaged_by(10, this); + health->damaged_by(1, this); } void Unit::aim_at(gd::Node3D *target) { @@ -86,10 +88,6 @@ UnitWorldState *Unit::get_world_state() const { return this->world_state; } -gd::Transform3D Unit::get_eye_transform() const { - return this->eyes->get_global_transform(); -} - UnitTeam Unit::get_team() const { return this->team; } @@ -105,6 +103,9 @@ int Unit::get_configure_team() const { void Unit::set_goal_and_plan(gd::Ref goal) { this->current_goal = goal; this->current_plan = this->planner->plan_for_goal(goal); + if(this->current_plan.is_empty()) { + this->current_goal.unref(); + } } void Unit::destroy_state() { diff --git a/src/unit.hpp b/src/unit.hpp index 524aa85..bcad7c0 100644 --- a/src/unit.hpp +++ b/src/unit.hpp @@ -32,12 +32,11 @@ public: void begin_goal(gd::Ref goal); void set_target_goal(gd::Node3D *target, gd::Ref goal); - void fire_at_target(); + virtual void use_weapon(); void aim_at(gd::Node3D *node); void on_death(Unit *damage_source); UnitWorldState *get_world_state() const; - gd::Transform3D get_eye_transform() const; UnitTeam get_team() const; void set_configure_team(int value); @@ -49,15 +48,13 @@ private: void next_action(); void replan_goal(); private: + gd::Callable on_state_finished{callable_mp(this, &Unit::state_finished)}; + gd::Callable on_plan_failed{callable_mp(this, &Unit::replan_goal)}; +protected: goap::Plan current_plan{}; gd::Ref current_goal{}; goap::State *state{nullptr}; - gd::Node3D *eyes{nullptr}; - - gd::Callable on_state_finished{callable_mp(this, &Unit::state_finished)}; - gd::Callable on_plan_failed{callable_mp(this, &Unit::replan_goal)}; - UnitTeam team{UnitTeam::Neutral}; gd::NavigationAgent3D *agent{nullptr}; diff --git a/src/unit_world_state.cpp b/src/unit_world_state.cpp index a84e982..c910f66 100644 --- a/src/unit_world_state.cpp +++ b/src/unit_world_state.cpp @@ -15,17 +15,20 @@ void UnitWorldState::_bind_methods() { GDFUNCTION(get_is_target_dead); GDFUNCTION(get_is_at_target); GDFUNCTION(get_target_node); + GDFUNCTION(get_is_target_enemy); + GDFUNCTION(get_is_in_melee_range); } void UnitWorldState::_enter_tree() { GDGAMEONLY(); this->parent_unit = gd::Object::cast_to(this->get_parent()); - this->agent = this->get_node("../NavigationAgent3D"); + this->agent = this->get_node("%NavigationAgent3D"); + this->eye_location = this->get_node("%Eyes"); if(this->parent_unit == nullptr) gd::UtilityFunctions::push_warning("UnitWorldState needs to be a child node of a Unit"); } bool UnitWorldState::get_can_see_target() const { - gd::Transform3D const eyes{this->parent_unit->get_eye_transform()}; + gd::Transform3D const eyes{this->eye_location->get_global_transform()}; // check if the target has a separate vision target, or is it's own vision target gd::Node3D *vision_target{this->target_node->get_node("VisionTarget")}; gd::Vector3 target_position{ @@ -56,13 +59,28 @@ bool UnitWorldState::get_is_at_target() const { bool UnitWorldState::get_is_target_dead() const { if(EntityHealth *health{this->target_node->get_node("EntityHealth")}) - return health->get_current_health() <= 0.f; + return health->get_injury_current() <= 0.f; return false; } +bool UnitWorldState::get_is_target_unit() const { + return gd::ClassDB::is_parent_class(this->target_node->get_class(), "Unit"); +} + +bool UnitWorldState::get_is_target_enemy() const { + Unit *unit{gd::Object::cast_to(this->target_node)}; + return unit != nullptr + && unit->get_team() != UnitTeam::Neutral + && unit->get_team() != this->parent_unit->get_team(); +} + +bool UnitWorldState::get_is_in_melee_range() const { + return this->target_node != nullptr + && this->target_node->get_global_position() + .distance_squared_to(this->parent_unit->get_global_position()) <= 4.f; +} + void UnitWorldState::set_target_node(gd::Node3D *node) { - if(node == this->target_node) - return; if(this->target_node != nullptr) this->target_node->disconnect("tree_exited", this->target_node_exited_tree.bind(this->target_node)); this->target_node = node; diff --git a/src/unit_world_state.hpp b/src/unit_world_state.hpp index a5bcca0..4970565 100644 --- a/src/unit_world_state.hpp +++ b/src/unit_world_state.hpp @@ -15,6 +15,9 @@ public: bool get_can_see_target() const; bool get_is_at_target() const; bool get_is_target_dead() const; + bool get_is_target_unit() const; + bool get_is_target_enemy() const; + bool get_is_in_melee_range() const; void set_target_node(gd::Node3D *node); gd::Node3D *get_target_node() const; @@ -22,10 +25,11 @@ private: void target_destroyed(gd::Node3D *target); private: gd::Callable const target_node_exited_tree{callable_mp(this, &UnitWorldState::target_destroyed)}; - +protected: Unit *parent_unit{nullptr}; gd::NavigationAgent3D *agent{nullptr}; gd::Node3D *target_node{nullptr}; + gd::Node3D *eye_location{nullptr}; }; #endif // !UNIT_WORLD_STATE_HPP