diff --git a/godot/GameObjects/enemy_unit.tscn b/godot/GameObjects/enemy_unit.tscn index 549fce6..4523747 100644 --- a/godot/GameObjects/enemy_unit.tscn +++ b/godot/GameObjects/enemy_unit.tscn @@ -21,7 +21,7 @@ rings = 3 [sub_resource type="BoxMesh" id="BoxMesh_p8wvo"] size = Vector3(0.2, 0.2, 0.5) -[node name="Tank" type="Unit"] +[node name="Tank" type="TankUnit"] configure_team = 3 movement_speed = 1.5 collision_layer = 6 diff --git a/godot/GameObjects/player_unit.tscn b/godot/GameObjects/player_unit.tscn index 068d37f..2b4fb02 100644 --- a/godot/GameObjects/player_unit.tscn +++ b/godot/GameObjects/player_unit.tscn @@ -18,12 +18,12 @@ rings = 3 [sub_resource type="BoxMesh" id="BoxMesh_p8wvo"] size = Vector3(0.2, 0.2, 0.5) -[node name="Player" type="Unit"] +[node name="Player" type="CharacterUnit"] configure_team = 1 collision_layer = 6 collision_mask = 0 -[node name="ActorWorldState" type="UnitWorldState" parent="."] +[node name="ActorWorldState" type="CharacterWorldState" parent="."] unique_name_in_owner = true [node name="Planner" type="Planner" parent="."] diff --git a/src/character_unit.cpp b/src/character_unit.cpp new file mode 100644 index 0000000..0e3004a --- /dev/null +++ b/src/character_unit.cpp @@ -0,0 +1,21 @@ +#include "character_unit.hpp" + +void CharacterUnit::_bind_methods() { +#define CLASSNAME CharacterUnit +} + +void CharacterUnit::_ready() { + this->parent_type::_ready(); + this->inventory = this->get_node("%Inventory"); +} + +void CharacterUnit::use_weapon() { + gd::Node3D *target{this->world_state->get_target_node()}; + Item const *weapon{this->inventory->get_weapon()}; + if(weapon == nullptr) { + this->parent_type::use_weapon(); // use the default unarmed attack instead + } else { + this->inventory->get_weapon()->try_use_on(this, target); + } +} + diff --git a/src/character_unit.hpp b/src/character_unit.hpp new file mode 100644 index 0000000..fad79f9 --- /dev/null +++ b/src/character_unit.hpp @@ -0,0 +1,17 @@ +#ifndef CHARACTER_UNIT_HPP +#define CHARACTER_UNIT_HPP + +#include "unit.hpp" +#include "inventory.hpp" + +class CharacterUnit : public Unit { + GDCLASS(CharacterUnit, Unit); + static void _bind_methods(); +public: + virtual void _ready() override; + virtual void use_weapon() override; +private: + Inventory *inventory{nullptr}; +}; + +#endif // !CHARACTER_UNIT_HPP diff --git a/src/character_world_state.cpp b/src/character_world_state.cpp new file mode 100644 index 0000000..b97296a --- /dev/null +++ b/src/character_world_state.cpp @@ -0,0 +1,16 @@ +#include "character_world_state.hpp" +#include "utils/godot_macros.hpp" + +void CharacterWorldState::_bind_methods() { +#define CLASSNAME CharacterWorldState +} + +void CharacterWorldState::_enter_tree() { + this->parent_type::_enter_tree(); + this->inventory = this->get_node("%Inventory"); +} + +bool CharacterWorldState::get_is_weapon_ranged() const { + return this->inventory->get_weapon()->has_capability(ItemCapability::WeaponRanged); +} + diff --git a/src/character_world_state.hpp b/src/character_world_state.hpp new file mode 100644 index 0000000..bd35b81 --- /dev/null +++ b/src/character_world_state.hpp @@ -0,0 +1,17 @@ +#ifndef CHARACTER_WORLD_STATE_HPP +#define CHARACTER_WORLD_STATE_HPP + +#include "inventory.hpp" +#include "unit_world_state.hpp" + +class CharacterWorldState : public UnitWorldState { + GDCLASS(CharacterWorldState, UnitWorldState); + static void _bind_methods(); +public: + virtual void _enter_tree() override; + virtual bool get_is_weapon_ranged() const override; +private: + Inventory *inventory{nullptr}; +}; + +#endif // !CHARACTER_WORLD_STATE_HPP diff --git a/src/goap/state.hpp b/src/goap/state.hpp index b9de3c8..e44e536 100644 --- a/src/goap/state.hpp +++ b/src/goap/state.hpp @@ -2,6 +2,8 @@ #define GOAP_STATE_HPP #include "actor_world_state.hpp" +#include +#include #include #include diff --git a/src/inventory.cpp b/src/inventory.cpp index 1de9ebb..06e5ba1 100644 --- a/src/inventory.cpp +++ b/src/inventory.cpp @@ -11,8 +11,8 @@ void Inventory::_bind_methods() { } void Inventory::_ready() { - if(gd::Engine::get_singleton()->is_editor_hint()) - return; + if(gd::Engine::get_singleton()->is_editor_hint()) + return; this->parent_unit = gd::Object::cast_to(this->get_parent()); } diff --git a/src/nav_marker.cpp b/src/nav_marker.cpp index f33120f..e7e57e0 100644 --- a/src/nav_marker.cpp +++ b/src/nav_marker.cpp @@ -14,7 +14,7 @@ void NavMarker::_bind_methods() { void NavMarker::_process(double) { #ifdef DEBUG_ENABLED - // only run in editor while selected + // only run in editor while selected if(gd::Engine::get_singleton()->is_editor_hint() && gd::EditorInterface::get_singleton()->get_selection()->get_selected_nodes().has(this)) { gd::RID const map_id{this->get_world_3d()->get_navigation_map()}; gd::Vector3 const new_point{gd::NavigationServer3D::get_singleton()->map_get_closest_point(map_id, this->get_global_position())}; diff --git a/src/register_types.cpp b/src/register_types.cpp index e00dd6e..5e87596 100644 --- a/src/register_types.cpp +++ b/src/register_types.cpp @@ -1,4 +1,6 @@ #include "register_types.h" +#include "character_unit.hpp" +#include "character_world_state.hpp" #include "enemy_world_state.hpp" #include "entity_health.hpp" #include "inventory.hpp" @@ -10,6 +12,7 @@ #include "rts_items.hpp" #include "rts_player.hpp" #include "rts_states.hpp" +#include "tank_unit.hpp" #include "unit.hpp" #include "unit_world_state.hpp" #include "goap/action.hpp" @@ -60,8 +63,11 @@ void initialize_gdextension_types(gd::ModuleInitializationLevel p_level) GDREGISTER_RUNTIME_CLASS(Activate); GDREGISTER_RUNTIME_CLASS(UnitWorldState); GDREGISTER_RUNTIME_CLASS(EnemyWorldState); + GDREGISTER_RUNTIME_CLASS(CharacterWorldState); GDREGISTER_RUNTIME_CLASS(GoalMarker); GDREGISTER_RUNTIME_CLASS(Unit); + GDREGISTER_RUNTIME_CLASS(TankUnit) + GDREGISTER_RUNTIME_CLASS(CharacterUnit); GDREGISTER_RUNTIME_CLASS(EntityHealth); GDREGISTER_CLASS(NavMarker); GDREGISTER_RUNTIME_CLASS(NavRoom); diff --git a/src/rts_actions.cpp b/src/rts_actions.cpp index f3e2537..ac057d6 100644 --- a/src/rts_actions.cpp +++ b/src/rts_actions.cpp @@ -29,6 +29,7 @@ goap::State *MoveToTarget::get_apply_state(goap::ActorWorldState *context) const FireAtTarget::FireAtTarget() : Action() { this->required.insert("can_see_target", true); + this->required.insert("is_weapon_ranged", true); this->effects.insert("is_target_dead", true); } @@ -96,7 +97,7 @@ TakeCover::TakeCover() } bool TakeCover::procedural_is_possible(goap::ActorWorldState *context) const { - // positions of the context and the target + // positions of the context and the target gd::Vector3 const context_position{context->get_world_property("parent_global_position")}; gd::Vector3 const target_position{context->get_world_property("target_global_position")}; // if there is no available navigation room, it is not possible to find a cover marker diff --git a/src/rts_items.cpp b/src/rts_items.cpp index c95e85e..6d6f00c 100644 --- a/src/rts_items.cpp +++ b/src/rts_items.cpp @@ -33,6 +33,9 @@ void Handgun::use_on(Unit* used_by, gd::Object* used_on) const { used_by->aim_at(target); EntityHealth *health{target->get_entity_health()}; health->damaged_by(4, used_by); + gd::Vector3 const owner_position{used_by->get_global_position()}; + gd::Vector3 const target_position{target->get_global_position()}; + used_by->look_at(owner_position-(target_position-owner_position)); } Lasercutter::Lasercutter() diff --git a/src/rts_player.cpp b/src/rts_player.cpp index 739d17b..8a0b774 100644 --- a/src/rts_player.cpp +++ b/src/rts_player.cpp @@ -43,7 +43,6 @@ void RTSPlayer::setup_player_input(utils::PlayerInput *input) { input->listen_to("_mouse_down", "_mouse_up", callable_mp(this, &RTSPlayer::on_mouse_vertical)); #ifdef DEBUG_ENABLED input->listen_to("DEBUG_toggle_debug", callable_mp(this, &RTSPlayer::DEBUG_enable_debug)); - gd::UtilityFunctions::print("!!! DEBUG_ENABLED"); #endif } diff --git a/src/unit.cpp b/src/unit.cpp index 62a1dbe..8301e0e 100644 --- a/src/unit.cpp +++ b/src/unit.cpp @@ -29,7 +29,6 @@ void Unit::_enter_tree() { this->anim_player = this->get_node("%AnimationPlayer"); this->health = this->get_node("%EntityHealth"); this->health->connect("death", callable_mp(this, &Unit::on_death)); - this->inventory = this->get_node("%Inventory"); } void Unit::_physics_process(double) { @@ -46,9 +45,7 @@ void Unit::_physics_process(double) { void Unit::stop_plan() { this->current_goal.unref(); this->current_plan.clear(); - if(this->state && !this->state->is_queued_for_deletion()) - this->destroy_state(); - this->state = nullptr; + this->destroy_state(); this->call_deferred("emit_signal", "plan_failed"); } @@ -78,16 +75,9 @@ void Unit::begin_goal(gd::Ref goal) { } void Unit::use_weapon() { - gd::Node3D *target{this->world_state->get_target_node()}; - Item const *weapon{this->inventory->get_weapon()}; - if(weapon == nullptr) { - // try and do a melee attack instead when no weapon is equipped - Unit *target_unit{gd::Object::cast_to(target)}; - if(target_unit != nullptr && this->world_state->get_can_see_target() && this->world_state->get_is_in_melee_range()) - target_unit->get_entity_health()->damaged_by(1, this); - } else { - this->inventory->get_weapon()->try_use_on(this, target); - } + Unit *target_unit{gd::Object::cast_to(this->world_state->get_target_node())}; + if(target_unit != nullptr && this->world_state->get_can_see_target() && this->world_state->get_is_in_melee_range()) + target_unit->get_entity_health()->damaged_by(1, this); } void Unit::aim_at(gd::Node3D *target) { @@ -119,9 +109,10 @@ void Unit::on_velocity_computed(gd::Vector3 vel) { } void Unit::destroy_state() { - if(this->state == nullptr || this->state->is_queued_for_deletion() || !this->state->is_inside_tree()) + if(this->state == nullptr) return; - this->state->interrupt_state(); + if(!this->state->is_queued_for_deletion() && this->state->is_inside_tree()) + this->state->interrupt_state(); this->state = nullptr; } @@ -132,30 +123,30 @@ void Unit::state_finished() { } void Unit::next_action() { + // destroy active state if relevant + this->destroy_state(); // cannot perform actions while dead if(!this->health->is_conscious()) return; - // destroy active state if relevant - if(this->state != nullptr && !this->state->is_queued_for_deletion()) - this->destroy_state(); - this->state = nullptr; if(this->current_plan.is_empty()) return; goap::Action const *action{this->current_plan.get(0)}; - if(!action->is_possible(this->world_state)) { + if(action == nullptr || !action->is_possible(this->world_state)) { this->emit_signal("plan_interrupted"); return; } - // pop next action and apply state + // get next action and apply state this->state = action->get_apply_state(this->world_state); if(state == nullptr) { this->emit_signal("plan_interrupted"); return; } + // pop action from plan this->current_plan.remove_at(0); - this->add_child(this->state); + // register new state and start this->state->connect("state_finished", this->on_state_finished); this->state->connect("state_failed", this->on_plan_failed); + this->add_child(this->state); } void Unit::replan_goal() { @@ -168,9 +159,8 @@ void Unit::set_goal_and_plan(gd::Ref goal) { this->current_plan.clear(); } else { this->current_plan = this->planner->plan_for_goal(goal); - if(this->current_plan.is_empty()) { + if(this->current_plan.is_empty()) this->current_goal.unref(); - } } } diff --git a/src/unit.hpp b/src/unit.hpp index cc154d0..6f46824 100644 --- a/src/unit.hpp +++ b/src/unit.hpp @@ -2,7 +2,6 @@ #define RTS_UNIT_HPP #include "goal_marker.hpp" -#include "inventory.hpp" #include "unit_world_state.hpp" #include "goap/goal.hpp" #include "goap/planner.hpp" @@ -70,7 +69,6 @@ protected: goap::Planner *planner{nullptr}; EntityHealth *health{nullptr}; UnitWorldState *world_state{nullptr}; - Inventory *inventory{nullptr}; #ifdef DEBUG_ENABLED public: gd::String DEBUG_print_debug_info(); diff --git a/src/unit_world_state.cpp b/src/unit_world_state.cpp index 8a5312d..40635b6 100644 --- a/src/unit_world_state.cpp +++ b/src/unit_world_state.cpp @@ -12,6 +12,7 @@ void UnitWorldState::_bind_methods() { #define CLASSNAME UnitWorldState GDSIGNAL("attention_changed"); + GDFUNCTION(get_is_weapon_ranged); GDFUNCTION(get_can_see_target); GDFUNCTION(get_is_target_dead); GDFUNCTION(get_is_at_target); @@ -41,6 +42,10 @@ gd::Variant UnitWorldState::get_world_property(gd::String property) { return ActorWorldState::get_world_property(property); } +bool UnitWorldState::get_is_weapon_ranged() const { + return false; +} + bool UnitWorldState::get_can_see_target() { bool value{this->get_can_see_node(this->target_node)}; this->cache_property("can_see_target", value, utils::time_seconds() + 0.5); diff --git a/src/unit_world_state.hpp b/src/unit_world_state.hpp index 44584e9..87f924a 100644 --- a/src/unit_world_state.hpp +++ b/src/unit_world_state.hpp @@ -2,6 +2,7 @@ #define UNIT_WORLD_STATE_HPP #include "entity_health.hpp" +#include "inventory.hpp" #include "goap/actor_world_state.hpp" #include #include @@ -21,6 +22,8 @@ class UnitWorldState : public goap::ActorWorldState { public: virtual void _enter_tree() override; virtual gd::Variant get_world_property(gd::String property) override; + + virtual bool get_is_weapon_ranged() const; bool get_can_see_target(); bool get_can_see_node(gd::Node3D *node) const; bool get_is_at_target() const;