From 844b905174563a63e5e0c71fc5684211230ccc43 Mon Sep 17 00:00:00 2001
From: Sara <sara@saragerretsen.nl>
Date: Fri, 31 May 2024 16:06:15 +0200
Subject: [PATCH] feat: Started defining GOAP concepts

---
 src/goap/action.cpp            | 37 +++++++++++++++++++
 src/goap/action.hpp            | 65 ++++++++++++++++++++++++++++++++++
 src/goap/action_db.cpp         | 36 +++++++++++++++++++
 src/goap/action_db.hpp         | 55 ++++++++++++++++++++++++++++
 src/goap/actor_world_state.cpp |  7 ++++
 src/goap/actor_world_state.hpp | 21 +++++++++++
 src/goap/goal.hpp              | 24 +++++++++++++
 src/goap/planner.cpp           | 34 ++++++++++++++++++
 src/goap/planner.hpp           | 32 +++++++++++++++++
 src/goap/state.cpp             | 32 +++++++++++++++++
 src/goap/state.hpp             | 48 +++++++++++++++++++++++++
 11 files changed, 391 insertions(+)
 create mode 100644 src/goap/action.cpp
 create mode 100644 src/goap/action.hpp
 create mode 100644 src/goap/action_db.cpp
 create mode 100644 src/goap/action_db.hpp
 create mode 100644 src/goap/actor_world_state.cpp
 create mode 100644 src/goap/actor_world_state.hpp
 create mode 100644 src/goap/goal.hpp
 create mode 100644 src/goap/planner.cpp
 create mode 100644 src/goap/planner.hpp
 create mode 100644 src/goap/state.cpp
 create mode 100644 src/goap/state.hpp

diff --git a/src/goap/action.cpp b/src/goap/action.cpp
new file mode 100644
index 0000000..7011195
--- /dev/null
+++ b/src/goap/action.cpp
@@ -0,0 +1,37 @@
+#include "action.hpp"
+
+namespace goap {
+Action::Action(WorldState &requires, WorldState &effects)
+: requires{requires}, effects{effects} {}
+
+WorldState const &Action::get_required() const {
+    return this->requires;
+}
+
+WorldState const &Action::get_effects() const {
+    return this->effects;
+}
+
+ActionID Action::get_id() const {
+    return this->id;
+}
+
+ActionData::ActionData(Action *action)
+: data{action} {}
+
+ActionData::~ActionData() {
+    if(this->data != nullptr)
+        delete this->data;
+}
+}
+
+bool TestAction::is_done_for(goap::ActorWorldState *) {
+    return true;
+}
+bool TestAction::is_possible_for(goap::ActorWorldState *) {
+    return true;
+}
+goap::State *TestAction::get_apply_state() const {
+    return nullptr;
+}
+
diff --git a/src/goap/action.hpp b/src/goap/action.hpp
new file mode 100644
index 0000000..0c6913a
--- /dev/null
+++ b/src/goap/action.hpp
@@ -0,0 +1,65 @@
+#ifndef GOAP_ACTION_HPP
+#define GOAP_ACTION_HPP
+
+#include "actor_world_state.hpp"
+#include "state.hpp"
+#include <godot_cpp/variant/string.hpp>
+#include <godot_cpp/templates/vector.hpp>
+#include <godot_cpp/templates/hash_map.hpp>
+
+#define GOAP_ACTION(Name_) \
+public: \
+    _FORCE_INLINE_ static gd::String get_static_class() { return #Name_; }\
+    _FORCE_INLINE_ gd::String get_class() const { return #Name_; }\
+private:
+
+namespace goap {
+typedef int ActionID;
+
+/*! A goap action construct.
+ *
+ * Child classes an be registered with `ActionDB::register_action`
+ */
+class Action {
+    friend class ActionDB;
+    GOAP_ACTION(Action);
+public:
+    virtual ~Action() = default;
+
+    virtual bool is_possible_for(ActorWorldState *state) = 0;
+    virtual bool is_done_for(ActorWorldState *state) = 0;
+    virtual State *get_apply_state() const = 0;
+
+    WorldState const &get_required() const;
+    WorldState const &get_effects() const;
+
+    ActionID get_id() const;
+protected:
+    Action() = default;
+    Action(WorldState &requires, WorldState &effects);
+private:
+    WorldState requires{};
+    WorldState effects{};
+    ActionID id{-1};
+};
+
+/* Storage wrapper for managed action class.
+ */
+struct ActionData {
+    ActionData() = default;
+    ActionData(Action *action);
+    ~ActionData();
+    Action *data{nullptr};
+};
+}
+
+class TestAction : public goap::Action {
+    GOAP_ACTION(TestAction);
+public:
+    TestAction() = default;
+    virtual bool is_done_for(goap::ActorWorldState *) override;
+    virtual bool is_possible_for(goap::ActorWorldState *) override;
+    virtual goap::State *get_apply_state() const override;
+};
+
+#endif // !GOAP_ACTION_HPP
diff --git a/src/goap/action_db.cpp b/src/goap/action_db.cpp
new file mode 100644
index 0000000..70b27fd
--- /dev/null
+++ b/src/goap/action_db.cpp
@@ -0,0 +1,36 @@
+#include "action_db.hpp"
+
+namespace goap {
+StaticString::~StaticString() {
+    if(this->ptr != nullptr)
+        delete this->ptr;
+}
+
+gd::String &StaticString::get() {
+    if(this->ptr == nullptr)
+        this->ptr = new gd::String();
+    return *this->ptr;
+}
+
+ActionID ActionDB::register_action(Action *instance, gd::String name) {
+    instance->id = ActionDB::actions.size();
+    if(!ActionDB::hint.get().is_empty())
+        ActionDB::hint.get() += ",";
+    ActionDB::hint.get() += name;
+    ActionDB::actions.push_back(ActionData(instance));
+    return instance->get_id();
+}
+
+Action const *ActionDB::get_action(ActionID index) {
+    if(ActionDB::actions.size() >= index || index < 0)
+        return nullptr;
+    return ActionDB::actions[index].data;
+}
+
+gd::String const &ActionDB::get_enum_hint() {
+    return ActionDB::hint.get();
+}
+
+gd::Vector<ActionData> ActionDB::actions{gd::Vector<ActionData>()};
+StaticString ActionDB::hint{};
+}
diff --git a/src/goap/action_db.hpp b/src/goap/action_db.hpp
new file mode 100644
index 0000000..1db4012
--- /dev/null
+++ b/src/goap/action_db.hpp
@@ -0,0 +1,55 @@
+#ifndef GOAP_ACTION_DB_HPP
+#define GOAP_ACTION_DB_HPP
+
+#include "action.hpp"
+
+namespace goap {
+struct StaticString {
+private:
+    gd::String *ptr{nullptr};
+public:
+    StaticString() = default;
+    ~StaticString();
+    gd::String &get();
+};
+
+/*! Global access to all (registered) Action types.
+ *
+ * Register them with the `register_action` function from `initialize_gdextension_types()`.
+ * To represent an Action in the editor, the getter and setter should act as if the property is an int.
+ * The property should then be registered as an enum with `get_enum_hint()` as hint.
+ * Inside the getter, `Action->get_id()` can be returned. While the setter assigns the underlying value to `ActionDB::get_action(id)`.
+ * ```
+ * void Example::set_action(int id) {
+ *      this->action = ActionDB::get_action(id);
+ * }
+ * int Example::get_action() const {
+ *      return this->action->get_id();
+ * }
+ * ```
+ */
+class ActionDB {
+protected:
+    static ActionID register_action(Action *instance, gd::String name);
+public:
+    /*! Get action by ID.
+     */
+    static Action const *get_action(ActionID id);
+    /*! Get the csv godot enum hint.
+     */
+    static gd::String const &get_enum_hint();
+    /*! Add action to database.
+     *
+     * \returns Action's assigned ID.
+     */
+    template<class TAction>
+    _FORCE_INLINE_ static ActionID register_action() {
+        return ActionDB::register_action(new TAction(), TAction::get_static_class());
+    }
+private:
+    static gd::Vector<ActionData> actions;
+    static StaticString hint;
+};
+}
+
+#endif // !GOAP_ACTION_DB_HPP
diff --git a/src/goap/actor_world_state.cpp b/src/goap/actor_world_state.cpp
new file mode 100644
index 0000000..de2fba1
--- /dev/null
+++ b/src/goap/actor_world_state.cpp
@@ -0,0 +1,7 @@
+#include "actor_world_state.hpp"
+
+namespace goap {
+void ActorWorldState::_bind_methods() {
+#define CLASSNAME ActorWorldState
+}
+}
diff --git a/src/goap/actor_world_state.hpp b/src/goap/actor_world_state.hpp
new file mode 100644
index 0000000..3b1a21f
--- /dev/null
+++ b/src/goap/actor_world_state.hpp
@@ -0,0 +1,21 @@
+#ifndef GOAP_ACTOR_WORLD_STATE_HPP
+#define GOAP_ACTOR_WORLD_STATE_HPP
+
+#include <godot_cpp/classes/node.hpp>
+#include <godot_cpp/templates/hash_map.hpp>
+#include <godot_cpp/templates/pair.hpp>
+#include <godot_cpp/variant/string_name.hpp>
+
+namespace gd = godot;
+
+namespace goap {
+typedef gd::HashMap<gd::StringName, gd::Variant> WorldState;
+typedef gd::KeyValue<gd::StringName, gd::Variant> WorldProperty;
+
+class ActorWorldState : public gd::Node {
+    GDCLASS(ActorWorldState, gd::Node);
+    static void _bind_methods();
+};
+}
+
+#endif // !GOAP_ACTOR_WORLD_STATE_HPP
diff --git a/src/goap/goal.hpp b/src/goap/goal.hpp
new file mode 100644
index 0000000..3ca39e3
--- /dev/null
+++ b/src/goap/goal.hpp
@@ -0,0 +1,24 @@
+#ifndef GOAP_GOAL_HPP
+#define GOAP_GOAL_HPP
+
+#include "actor_world_state.hpp"
+#include <godot_cpp/classes/resource.hpp>
+
+namespace gd = godot;
+
+namespace goap {
+class Goal : public gd::Resource {
+    GDCLASS(Goal, gd::Resource);
+    static void _bind_methods();
+public:
+    gd::Dictionary get_desired_state() const;
+    void set_desired_state(gd::Dictionary dict);
+private:
+    //! Do not select this goal unless all requirements are met.
+    WorldState requirements;
+    //! The state to find a path to.
+    WorldState desired_state;
+};
+}
+
+#endif // !GOAP_GOAL_HPP
diff --git a/src/goap/planner.cpp b/src/goap/planner.cpp
new file mode 100644
index 0000000..4bc61b6
--- /dev/null
+++ b/src/goap/planner.cpp
@@ -0,0 +1,34 @@
+#include "planner.hpp"
+#include "goap/action_db.hpp"
+#include "utils/godot_macros.hpp"
+
+namespace goap {
+void Planner::_bind_methods() {
+#define CLASSNAME Planner
+    GDPROPERTY_HINTED(actions, gd::Variant::PACKED_INT32_ARRAY, gd::PROPERTY_HINT_ENUM, ActionDB::get_enum_hint());
+}
+
+void Planner::_enter_tree() {
+    this->world_state = this->get_node<ActorWorldState>("../ActorWorldState");
+}
+
+Plan Planner::plan_for_goal(gd::Ref<Goal> goal) {
+    return {};
+}
+
+void Planner::set_actions(gd::PackedInt32Array array) {
+    this->actions.clear();
+    for(int id : array) {
+        Action const *action = ActionDB::get_action(id);
+        if(action != nullptr && !this->actions.has(action))
+            this->actions.push_back(action);
+    }
+}
+
+gd::PackedInt32Array Planner::get_actions() const {
+    gd::PackedInt32Array array{};
+    for(Action const *action : this->actions)
+        action->get_id();
+    return array;
+}
+}
diff --git a/src/goap/planner.hpp b/src/goap/planner.hpp
new file mode 100644
index 0000000..41378ed
--- /dev/null
+++ b/src/goap/planner.hpp
@@ -0,0 +1,32 @@
+#ifndef GOAP_PLANNER_HPP
+#define GOAP_PLANNER_HPP
+
+#include "action.hpp"
+#include "actor_world_state.hpp"
+#include "goal.hpp"
+#include "godot_cpp/variant/packed_int32_array.hpp"
+#include <godot_cpp/templates/vector.hpp>
+#include <godot_cpp/classes/node.hpp>
+
+namespace gd = godot;
+
+namespace goap {
+typedef gd::Vector<Action> Plan;
+
+class Planner : public gd::Node {
+    GDCLASS(Planner, gd::Node);
+    static void _bind_methods();
+public:
+    virtual void _enter_tree() override;
+
+    Plan plan_for_goal(gd::Ref<Goal> goal);
+
+    void set_actions(gd::PackedInt32Array array);
+    gd::PackedInt32Array get_actions() const;
+private:
+    ActorWorldState *world_state{nullptr};
+    gd::Vector<Action const *> actions;
+};
+}
+
+#endif // !GOAP_PLANNER_HPP
diff --git a/src/goap/state.cpp b/src/goap/state.cpp
new file mode 100644
index 0000000..65df71d
--- /dev/null
+++ b/src/goap/state.cpp
@@ -0,0 +1,32 @@
+#include "state.hpp"
+
+namespace goap {
+void State::_bind_methods() {}
+
+void MoveTo::_bind_methods() {}
+
+void MoveTo::_enter_tree() {
+    this->agent = this->get_node<gd::NavigationAgent3D>("../NavigationAgent3D");
+    this->agent->set_target_position(this->target_node->get_global_position());
+    this->parent_node3d = Object::cast_to<gd::Node3D>(this->get_parent());
+}
+
+void MoveTo::_exit_tree() {
+    this->agent->set_target_position(this->parent_node3d->get_global_position());
+}
+
+void MoveTo::_process(double delta_time) {
+    gd::Vector3 const pos = this->parent_node3d->get_global_position();
+    gd::Vector3 const target = this->agent->get_next_path_position();
+    gd::Vector3 const direction = (target - pos).normalized();
+    this->parent_node3d->set_global_position(direction * delta_time);
+    this->parent_node3d->look_at(pos - gd::Vector3{direction.x, 0.f, direction.z});
+
+    if(this->agent->is_navigation_finished())
+        this->queue_free();
+}
+
+void Activate::_bind_methods() {}
+
+void Animate::_bind_methods() {}
+}
diff --git a/src/goap/state.hpp b/src/goap/state.hpp
new file mode 100644
index 0000000..2edb220
--- /dev/null
+++ b/src/goap/state.hpp
@@ -0,0 +1,48 @@
+#ifndef GOAP_STATE_HPP
+#define GOAP_STATE_HPP
+
+#include "godot_cpp/classes/navigation_agent3d.hpp"
+#include <godot_cpp/classes/node.hpp>
+#include <godot_cpp/classes/node3d.hpp>
+
+namespace gd = godot;
+
+namespace goap {
+class State : public gd::Node {
+    GDCLASS(State, gd::Node);
+    static void _bind_methods();
+};
+
+/*! Uses navigation to chase the desired target node.
+ */
+class MoveTo : public State {
+    GDCLASS(MoveTo, State);
+    static void _bind_methods();
+public:
+    virtual void _enter_tree() override;
+    virtual void _exit_tree() override;
+    virtual void _process(double delta_time) override;
+public:
+    gd::Node3D *target_node{nullptr};
+private:
+    gd::Node3D *parent_node3d{nullptr};
+    gd::NavigationAgent3D *agent{nullptr};
+};
+
+class Activate : public State {
+    GDCLASS(Activate, State);
+    static void _bind_methods();
+public:
+    gd::Node3D *target_node{nullptr};
+    gd::String animation{};
+};
+
+class Animate : public State {
+    GDCLASS(Animate, State);
+    static void _bind_methods();
+public:
+    gd::String animation{};
+};
+}
+
+#endif // !GOAP_STATE_HPP