diff --git a/design/design-document.svg b/design/design-document.svg
index 91f41fa..ea79e93 100644
--- a/design/design-document.svg
+++ b/design/design-document.svg
@@ -24,10 +24,11 @@
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.845329"
- inkscape:cx="7581.6634"
- inkscape:cy="458.99289"
+ inkscape:cx="5903.0271"
+ inkscape:cy="437.6994"
inkscape:current-layer="layer1"
- showgrid="false">
+ showgrid="false"
+ inkscape:export-bgcolor="#ffffffff">
+ bleed="0"
+ inkscape:export-filename="../../../../Downloads/design-document.pdf"
+ inkscape:export-xdpi="96"
+ inkscape:export-ydpi="96" />
Modern civilisation has been wiped out. Most methods of long-distance communication have been destroyed or become unavailable. Factories have Modern civilisation has been wiped out. Most methods of long-distance communication have been destroyed or become unavailable. Factories have stopped running, and there is no government to take the lead. Those that remain have formed communes and towns in the ruins.
+ id="tspan4">stopped running, and there is no government to take the lead. Those that remain have formed communes and towns in the ruins.
+ id="tspan6">
In this particular city, people survived by sheltering in the underground subways. Now, several years after the end, they've become braver, and In this particular city, people survived by sheltering in the underground subways. Now, several years after the end, they've become braver, and started rebuilding. Their first order of business: reconnect the stations. By enabling the flow of resources and people between the various groups, started rebuilding. Their first order of business: reconnect the stations. By enabling the flow of resources and people between the various groups, they hope to improve the chances survivors have of making it through the end.
+ id="tspan24">they hope to improve the chances survivors have of making it through the end.
+ id="tspan26">
Player:Player:
+ id="tspan29">
A "combat railway engineer" who can maintain, drive, and repair trains and railway materiel. Because of the dangers lurking in the underground, A "combat railway engineer" who can maintain, drive, and repair trains and railway materiel. Because of the dangers lurking in the underground, these engineers are supported and trained as and by military personell.
+ id="tspan38">these engineers are supported and trained as and by military personell.
+ id="tspan40">
Objectives:Objectives:
+ id="tspan43">
The re-opening of the underground has various obstacles. The combat railway engineers are tasked with solving these. No matter what kind. The re-opening of the underground has various obstacles. The combat railway engineers are tasked with solving these. No matter what kind. Obstacles include: monsters in the tunnels, trains broken down, no trains available on a given line, broken rails, broken signals, and many more. Obstacles include: monsters in the tunnels, trains broken down, no trains available on a given line, broken rails, broken signals, and many more. Solving these problems will always require some combination of on-site problem solving and combat against the creatures that have spilled in from Solving these problems will always require some combination of on-site problem solving and combat against the creatures that have spilled in from the city surface.
+ id="tspan51">the city surface.
+ id="tspan53">
Resources:Resources:
+ id="tspan56">
Railway engineers and soldiers need extensive training, when one goes KIA that means one less person to fight in the next operation. Since the Railway engineers and soldiers need extensive training, when one goes KIA that means one less person to fight in the next operation. Since the apocalypse, weapons and spare parts are in short supply, the player will have to decide where to use what.
+ id="tspan60">apocalypse, weapons and spare parts are in short supply, the player will have to decide where to use what.
+ id="tspan62">
Mechanics:Mechanics:
+ id="tspan65">
Engineers are sent into the tunnels in squads, they'll have to communicate well and manage risks. The player and squad-members will each be Engineers are sent into the tunnels in squads, they'll have to communicate well and manage risks. The player and squad-members will each be carrying different tools, parts, and weapons. The player can shoot, and interact with things themselves, or order AI teammates to do those things carrying different tools, parts, and weapons. The player can shoot, and interact with things themselves, or order AI teammates to do those things instead, as long as the teammate has the right tools.
+ id="tspan71">instead, as long as the teammate has the right tools.
+ id="tspan73">
Dynamics:Dynamics:
+ id="tspan76">
During missions, the player will have to manage their time and resources well. You don't want to run out of ammunition while in the tunnels. This During missions, the player will have to manage their time and resources well. You don't want to run out of ammunition while in the tunnels. This means that the player will have to decide which teammember is the best fit for any given task. Whether to it themselves or to order someone else. means that the player will have to decide which teammember is the best fit for any given task. Whether to it themselves or to order someone else. Who can be missed, who has the right tools. Who has the right skills.
+ id="tspan82">Who can be missed, who has the right tools. Who has the right skills.
+ id="tspan84">
Conflict:
+ id="tspan86">Conflict:
In the tunnels, there are many obstacles for the trains to start or continue running. There may be monsters, broken trains, broken rails, signals, or In the tunnels, there are many obstacles for the trains to start or continue running. There may be monsters, broken trains, broken rails, signals, or perhaps there are people who pull heists that need to be stopped.
+ id="tspan90">perhaps there are people who pull heists that need to be stopped.
+ id="tspan92">
Boundaries:
+ id="tspan94">Boundaries:
The player is limited in their ability to explore the tunnels by resources and damage output. Deeper into the network the danger increases. And any The player is limited in their ability to explore the tunnels by resources and damage output. Deeper into the network the danger increases. And any lost team members are lost forever.
+ id="tspan98">lost team members are lost forever.
+ id="tspan100">
Outcome:
+ id="tspan102">Outcome:
The player's team will be able to open up new routes, expanding the reach of the railway, as well as being able to find new recruits at newly The player's team will be able to open up new routes, expanding the reach of the railway, as well as being able to find new recruits at newly connected settlements. If the player dies in a tunnel, they will wake up at the nearest station, having been rescued. The tunnel they were in will connected settlements. If the player dies in a tunnel, they will wake up at the nearest station, having been rescued. The tunnel they were in will have been weakened and they will be able to retry. Though their own resources will have been lessened as well.
+ id="tspan113">have been weakened and they will be able to retry. Though their own resources will have been lessened as well.
Mechanics
+ id="tspan115">Mechanics
+ id="tspan117">
The metro
+ id="tspan119">The metro
As the metro expands, and trains start running, the player will have to navigate it. As the metro expands, and trains start running, the player will have to navigate it. Using a railway map annotated with connected, discovered, and unavailable Using a railway map annotated with connected, discovered, and unavailable tunnels and stations. The player can fast travel only along active lines.
+ id="tspan125">tunnels and stations. The player can fast travel only along active lines.
+ id="tspan127">
While travelling on the 'outer' lines (those close to not-yet-cleared tunnels). The While travelling on the 'outer' lines (those close to not-yet-cleared tunnels). The train may be attacked by monsters.
+ id="tspan131">train may be attacked by monsters.
+ id="tspan133">
Combat
+ id="tspan135">Combat
The player and team are equipped with small arms and optionally explosives. The player and team are equipped with small arms and optionally explosives. Combat is on the slow side, with moving forward being dangerous and ill-Combat is on the slow side, with moving forward being dangerous and ill-adviced. The player use stations, side-tunnels and engineering tunnels to their adviced. The player use stations, side-tunnels and engineering tunnels to their advantage to take up a position. The players can use scarce consumable advantage to take up a position. The players can use scarce consumable equipment to create opportunities from such positions.
+ id="tspan145">equipment to create opportunities from such positions.
+ id="tspan147">
Excursions
+ id="tspan149">Excursions
When moving into new territory, the player will first have to find out what might When moving into new territory, the player will first have to find out what might stop the trains from running through there. To do this, an engineering vehicle can stop the trains from running through there. To do this, an engineering vehicle can be taken into the tunnel to serve as a base of operations. The player will have to be taken into the tunnel to serve as a base of operations. The player will have to clear obstacles stopping the engineering vehicle from advancing.
+ id="tspan157">clear obstacles stopping the engineering vehicle from advancing.
Dynamics
+ id="tspan159">Dynamics
+ id="tspan160">
The player will have to become familiar with the metro system. Always keeping The player will have to become familiar with the metro system. Always keeping track of where the active lines, outer lines and edge stations are.
+ id="tspan163">track of where the active lines, outer lines and edge stations are.
+ id="tspan164">
The player's ability to navigate the metro quickly is vital to their ability to both The player's ability to navigate the metro quickly is vital to their ability to both expand the network, and respond to incidents quickly.
+ id="tspan166">expand the network, and respond to incidents quickly.
+ id="tspan167">
Excursions are fights of endurance, monsters will be more numerous, and the Excursions are fights of endurance, monsters will be more numerous, and the advance slow. The engineering vehicle enables regular resupplies and allows the advance slow. The engineering vehicle enables regular resupplies and allows the player to specialize their squard more by letting them switch equipment more often.
+ id="tspan170">player to specialize their squard more by letting them switch equipment more often.
+ id="tspan177">
Scouting encourages speed and minimizing combat. Once trains have been brought Scouting encourages speed and minimizing combat. Once trains have been brought to the closest active station, the player will be able to prepare an excursion to clear to the closest active station, the player will be able to prepare an excursion to clear out the tunnel. Equipment for these missions will have to be general, as it is out the tunnel. Equipment for these missions will have to be general, as it is unknown what kinds of challenges may block the player's advance, with no way of unknown what kinds of challenges may block the player's advance, with no way of swapping equipment midway through.
+ id="tspan185">swapping equipment midway through.
+ id="tspan186">
By limiting outpost range based on transfers, the player is encouraged to place By limiting outpost range based on transfers, the player is encouraged to place them at intersections and transfers. Simultaneously the player will want to place them at intersections and transfers. Simultaneously the player will want to place them close to service stations (where the engineering train can stop). This tension them close to service stations (where the engineering train can stop). This tension can be exploited through level design.
+ id="tspan190">can be exploited through level design.
+ id="tspan191">
Early on, incidents will force the player to spread their own attention around. When Early on, incidents will force the player to spread their own attention around. When the player gains the ability to place more outposts, they will have to focus less of the player gains the ability to place more outposts, they will have to focus less of their own attention to this. Moving it from a micro task to a macro task.
+ id="tspan194">their own attention to this. Moving it from a micro task to a macro task.
Such as broken rails or monsters. When some waypoint is reached (such as a station) the regular train's line extends. Allowing fast travel from other Such as broken rails or monsters. When some waypoint is reached (such as a station) the regular train's line extends. Allowing fast travel from other stations on the same line to that station.
+ id="tspan197">stations on the same line to that station.
+ id="tspan198">
Scouting
+ id="tspan199">Scouting
When a new line is discovered, often the trains are not available. The player will then have to go into the tunnels on-foot to find a service station, When a new line is discovered, often the trains are not available. The player will then have to go into the tunnels on-foot to find a service station, repair a train there, and bring it back to an 'active' station.
+ id="tspan201">repair a train there, and bring it back to an 'active' station.
+ id="tspan202">
Incidents
+ id="tspan203">Incidents
On the 'outer' lines, incidents can occur. Forcing a certain line out of service until the player can restore it. If the player cannot restore it, the line may On the 'outer' lines, incidents can occur. Forcing a certain line out of service until the player can restore it. If the player cannot restore it, the line may be lost again.
+ id="tspan205">be lost again.
+ id="tspan206">
Outposts
+ id="tspan207">Outposts
Each line can have one outpost. The player can pick which station to put it on. An outpost's range is defined in number of transfers. An outpost can Each line can have one outpost. The player can pick which station to put it on. An outpost's range is defined in number of transfers. An outpost can respond to incidents within it's range. Outposts are also where the player can find new recruits.
+ id="tspan210">respond to incidents within it's range. Outposts are also where the player can find new recruits.
+ id="tspan212">
Engineering train
+ id="tspan213">Engineering train
The train used for excursions. This can be driven by the player and will always be given right of way by other trains on the network. The player can The train used for excursions. This can be driven by the player and will always be given right of way by other trains on the network. The player can equip it and store materials in it. The engineering train can only be exited while stopped in un-cleared tunnels and service stations.
+ id="tspan220">equip it and store materials in it. The engineering train can only be exited while stopped in un-cleared tunnels and service stations.
+ id="tspan221">
Repair crews and trains
+ id="tspan222">Repair crews and trains
Some places on the network are so severely damaged that the player's squad cannot fix it. To fix these the player gains the ability to dispatch repair Some places on the network are so severely damaged that the player's squad cannot fix it. To fix these the player gains the ability to dispatch repair crews to certain locations to repair extreme damage. These trains will however interupt service on the line they're repairing for a while.
+ id="tspan224">crews to certain locations to repair extreme damage. These trains will however interupt service on the line they're repairing for a while.
Resources
+ id="tspan225">Resources
+ id="tspan226">
+ id="tspan228">
+ id="tspan230">
+ id="tspan231">
+ id="tspan232">
+ id="tspan234">
+ id="tspan239">
+ id="tspan240">
+ id="tspan243">
The number of different resource types the player has to manage increase over the course of the game.
+ id="tspan244">The number of different resource types the player has to manage increase over the course of the game.
+ id="tspan245">
To begin with, the player has to manage their squad's health (and consumables) and ammunition for various weapons. This includes having to To begin with, the player has to manage their squad's health (and consumables) and ammunition for various weapons. This includes having to manage squad members when they are injured or die.
+ id="tspan247">manage squad members when they are injured or die.
+ id="tspan248">
Next the player will gain the ability to assign engineers to outposts. The engineer's skills will impact how many recruits the outpost attracts, how Next the player will gain the ability to assign engineers to outposts. The engineer's skills will impact how many recruits the outpost attracts, how well trained they will be when they join, the range of the outpost, and the response speed.
+ id="tspan250">well trained they will be when they join, the range of the outpost, and the response speed.
+ id="tspan251">
While expanding, the player will come across a service station with maintainance trains. Which the player will then be able to assign to clear rubble While expanding, the player will come across a service station with maintainance trains. Which the player will then be able to assign to clear rubble and repair more extensive damage to the metro's systems.
+ id="tspan253">and repair more extensive damage to the metro's systems.
+ id="tspan254">
The player's primary method of acquiring new equipment and resources is through scrapping and crafting. The player can scrap specific parts from The player's primary method of acquiring new equipment and resources is through scrapping and crafting. The player can scrap specific parts from broken items, and if they can find the right parts they can combine them into some new item. The items are specific to the level of "assault rifle broken items, and if they can find the right parts they can combine them into some new item. The items are specific to the level of "assault rifle firing mechanism". So not "AK47 firing mechanism" nor "gun part".
+ id="tspan257">firing mechanism". So not "AK47 firing mechanism" nor "gun part".
+ id="tspan258">
Trains will inevitably break down. And without a consistent source of replacement parts, the player will have to venture into the tunnels to find Trains will inevitably break down. And without a consistent source of replacement parts, the player will have to venture into the tunnels to find them instead.
+ id="tspan260">them instead.
+ id="tspan261">
List of resources:
+ id="tspan262">List of resources:
- Health and ammo: these are mainly important for on-the-ground combat. They are both ticking down to an excursion's failure.
+ id="tspan263">- Health and ammo: these are mainly important for on-the-ground combat. They are both ticking down to an excursion's failure.
- People: this is the big one the player needs to balance. Assigning engineers to increasingly varied posts.
+ id="tspan264">- People: this is the big one the player needs to balance. Assigning engineers to increasingly varied posts.
- Maintainance trains: these will be in short supply, only ever ticking up to at most 3 in late game.
+ id="tspan265">- Maintainance trains: these will be in short supply, only ever ticking up to at most 3 in late game.
- Regular trains: these will be unlocked shortly after finding a new line (see: scouting)
+ id="tspan266">- Regular trains: these will be unlocked shortly after finding a new line (see: scouting)
- Weapon/Equipment parts.
+ id="tspan267">- Weapon/Equipment parts.
- Train repair parts.
+ id="tspan268">- Train repair parts.
Objectives
+ id="tspan269">Objectives
+ id="tspan270">
The player's and settlement's resources are always running out. And there's no way of producing more at the rate required for self-The player's and settlement's resources are always running out. And there's no way of producing more at the rate required for self-sustainability. The player needs to expand the network to survive. But as the network grows, the resource pressure grows as well.
+ id="tspan272">sustainability. The player needs to expand the network to survive. But as the network grows, the resource pressure grows as well.
+ id="tspan273">
To get more resources, the player will have to explore the tunnels and increase the number of stations connected. The player can also go into To get more resources, the player will have to explore the tunnels and increase the number of stations connected. The player can also go into "dark zones" areas of the network that cannot be connected to the main network due to large caveins and failed tracks. These areas will be "dark zones" areas of the network that cannot be connected to the main network due to large caveins and failed tracks. These areas will be more dangerous, but also have more resources.
+ id="tspan276">more dangerous, but also have more resources.
+ id="tspan277">
+ id="tspan278">
Conflict
+ id="tspan279">Conflict
+ id="tspan280">
In the tunnels, managing resources is vital. The primary danger to those living there is running out of vital supplies like food, medicine, and In the tunnels, managing resources is vital. The primary danger to those living there is running out of vital supplies like food, medicine, and water. Without trains to run the metro, or tracks to carry them, the people of the metro are left with dwindling supplies and no fast way of water. Without trains to run the metro, or tracks to carry them, the people of the metro are left with dwindling supplies and no fast way of replenishing them.
+ id="tspan283">replenishing them.
+ id="tspan284">
Repairing the tracks is dangerous work, even just transporting replacement parts can bring unwanted attention. Attacks are common, and Repairing the tracks is dangerous work, even just transporting replacement parts can bring unwanted attention. Attacks are common, and repairing one line requires an extended battle of attrition to get to all the broken parts of the railway and repair them.
+ id="tspan286">repairing one line requires an extended battle of attrition to get to all the broken parts of the railway and repair them.
+ id="tspan287">
Some parts of the railway are rendered permanently inaccessible. This is where the most danger, and the majority of the remaining resources, Some parts of the railway are rendered permanently inaccessible. This is where the most danger, and the majority of the remaining resources, can be found. Because these parts of the metro are difficult to get to, very few people have managed to get into them yet, and thus a lot of can be found. Because these parts of the metro are difficult to get to, very few people have managed to get into them yet, and thus a lot of materials are left ungathered. This is also where it may be possible to find spare train parts, as the trains there are of no use to the metro materials are left ungathered. This is also where it may be possible to find spare train parts, as the trains there are of no use to the metro without connections.
+ id="tspan291">without connections.
Core Ideas and Pillars
+ id="tspan292">Core Ideas and Pillars
+ id="tspan294">
+ id="tspan296">Collectively making something from the scraps ...
+ id="tspan298"> Of a broken world, of the metro, of society, etc.
Collectively making something from the scraps ...
+ id="tspan302">
Of a broken world, of the metro, of society, etc.
+ id="tspan304">This phrase should be the driving force. People working together to build something new from what remains in the metro.
+ id="tspan306">
This phrase should be the driving force. People working together to build something new from what remains in the metro.
+ id="tspan308">Everyone working together to survive
+ id="tspan310">This means no economy, no human enemies. In general, all humans should be trying to help eachother survive.
Everyone working together to survive
-It does not mean there can't be distrust or conflict between humans, but it does mean that those should be storylines that can be resolved with This means no economy, no human enemies. In general, all humans should be trying to help eachother survive.
+ id="tspan314">collaboration and gaining trust
It does not mean there can't be distrust or conflict between humans, but it does mean that those should be storylines that can be resolved with In gameplay it means that fucking people over can *never* be the optimal solution to a problem.
+collaboration and gaining trust
+ id="tspan318">
In gameplay it means that fucking people over can *never* be the optimal solution to a problem.
+ id="tspan320">Something new
-Not just a return to form, but an attempt at something new. In early stages, when the player and people of the metro are not yet "safe" this should still Something new
+ id="tspan324">be far off. But it should be the "end goal" of the game, to not just repair what was lost, but make something new.
Not just a return to form, but an attempt at something new. In early stages, when the player and people of the metro are not yet "safe" this should still
+be far off. But it should be the "end goal" of the game, to not just repair what was lost, but make something new.
+ id="tspan328">Anarchy
-The aftermath of the apocalypse has no government, only a shared will to survive. The player will be a leader of sorts, but not in any official fashion. Anarchy
+ id="tspan332">The player character is a respected expert who's opinion is valued enough to let them make calls on the organization of the metro.
The aftermath of the apocalypse has no government, only a shared will to survive. The player will be a leader of sorts, but not in any official fashion.
+The player character is a respected expert who's opinion is valued enough to let them make calls on the organization of the metro.
+ id="tspan336">ludonarative progression
-ludonarative progression
-Surviving off what is left -> Repairing -> Rebuilding -> Trying the same thing in a changed environment fails -> Making something new
+ id="tspan338">Surviving off what is left -> Repairing -> Rebuilding -> Trying the same thing in a changed environment fails -> Making something new
Team member personality
+ id="tspan340">Team member personality
Engineers have their own personalities that inform their behaviour in combat situations. This informs how they react to stress, and how they Engineers have their own personalities that inform their behaviour in combat situations. This informs how they react to stress, and how they approach combat situations.
+ id="tspan342">approach combat situations.
+ id="tspan343">
Team member skills
+ id="tspan344">Team member skills
Some engineers are more adept at certain tasks than others. This might impact things like accuracy with a weapon type.
+ id="tspan345">Some engineers are more adept at certain tasks than others. This might impact things like accuracy with a weapon type.
+ id="tspan346">
Team member tension/stress
+ id="tspan347">Team member tension/stress
As a combat situation worsens your team will become more stressed. Different engineers will react to stress in different ways according to their As a combat situation worsens your team will become more stressed. Different engineers will react to stress in different ways according to their personality.
+ id="tspan349">personality.
diff --git a/godot/new_goal.tres b/godot/new_goal.tres
new file mode 100644
index 0000000..d54c01a
--- /dev/null
+++ b/godot/new_goal.tres
@@ -0,0 +1,6 @@
+[gd_resource type="Goal" format=3 uid="uid://ogtaubr23l5x"]
+
+[resource]
+goal_state = {
+"goal": true
+}
diff --git a/godot/player_character.tscn b/godot/player_character.tscn
index f4cd098..924af26 100644
--- a/godot/player_character.tscn
+++ b/godot/player_character.tscn
@@ -1,4 +1,4 @@
-[gd_scene load_steps=6 format=3 uid="uid://dpda341t6ipiv"]
+[gd_scene load_steps=9 format=3 uid="uid://dpda341t6ipiv"]
[sub_resource type="Curve" id="Curve_7rmf4"]
min_value = 0.2
@@ -18,7 +18,24 @@ albedo_color = Color(0.94902, 0.909804, 0, 1)
[sub_resource type="BoxMesh" id="BoxMesh_f5yvh"]
size = Vector3(0.125, 0.14, 0.94)
-[node name="PlayerCharacter" type="PlayerCharacter"]
+[sub_resource type="MoveStateArgs" id="MoveStateArgs_ibmkn"]
+argument_property = &"player_character"
+
+[sub_resource type="Action" id="Action_gtisq"]
+effects = {
+"is_near_player": true
+}
+apply_state = SubResource("MoveStateArgs_ibmkn")
+
+[sub_resource type="Goal" id="Goal_sqtwb"]
+goal_state = {
+"is_near_player": true
+}
+prerequisites = {
+"is_near_player": false
+}
+
+[node name="PlayerCharacter" type="CharacterActor"]
rotation_speed_curve = SubResource("Curve_7rmf4")
collision_layer = 7
@@ -47,3 +64,7 @@ surface_material_override/0 = SubResource("StandardMaterial3D_scmx3")
[node name="WeaponMuzzle" type="WeaponMuzzle" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.53551, 0.931313, 0)
+
+[node name="Planner" type="Planner" parent="."]
+actions = [SubResource("Action_gtisq")]
+goals = [SubResource("Goal_sqtwb")]
diff --git a/src/action.cpp b/src/action.cpp
new file mode 100644
index 0000000..909c3cc
--- /dev/null
+++ b/src/action.cpp
@@ -0,0 +1,78 @@
+#include "action.hpp"
+#include "character_actor.hpp"
+#include "global_world_state.hpp"
+#include "utils/godot_macros.h"
+#include
+#include
+
+namespace godot::goap {
+void Action::_bind_methods() {
+#define CLASSNAME Action
+ GDPROPERTY(context_prerequisites, Variant::DICTIONARY);
+ GDPROPERTY(prerequisites, Variant::DICTIONARY);
+ GDPROPERTY(effects, Variant::DICTIONARY);
+ GDPROPERTY_HINTED(apply_state, Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "StateArgs");
+}
+
+void Action::set_context_prerequisites(Dictionary dict) {
+ Action::dict_to_property_map(dict, this->context_prerequisites);
+}
+
+Dictionary Action::get_context_prerequisites() const {
+ return Action::property_map_to_dict(this->context_prerequisites);
+}
+
+void Action::set_prerequisites(Dictionary dict) {
+ Action::dict_to_property_map(dict, this->prerequisites);
+}
+
+Dictionary Action::get_prerequisites() const {
+ return Action::property_map_to_dict(this->prerequisites);
+}
+
+void Action::set_effects(Dictionary dict) {
+ Action::dict_to_property_map(dict, this->effects);
+}
+
+Dictionary Action::get_effects() const {
+ return Action::property_map_to_dict(this->effects);
+}
+
+void Action::set_apply_state(Ref state) {
+ this->apply_state = state;
+}
+
+Ref Action::get_apply_state() const {
+ return this->apply_state;
+}
+
+bool Action::get_is_completed(CharacterActor *context) {
+ GlobalWorldState *state = GlobalWorldState::get_singleton();
+ for(WorldProperty const &prop : this->effects) {
+ return (prop.key.begins_with("g_")
+ ? state->get_world_property(prop.key)
+ : context->call("get_" + prop.key)) == prop.value;
+ }
+ return true;
+}
+
+Dictionary Action::property_map_to_dict(WorldState const &props) {
+ Dictionary out{};
+ for(KeyValue const &prop : props) {
+ out[prop.key] = prop.value;
+ }
+ return out;
+}
+
+void Action::dict_to_property_map(Dictionary const &dict, WorldState &out) {
+ out.clear();
+ Array keys = dict.keys();
+ Array vals = dict.values();
+ for(size_t i{0}; i < keys.size(); ++i) {
+ if(keys[i].get_type() == Variant::STRING_NAME || keys[i].get_type() == Variant::STRING)
+ out.insert(keys[i], dict[keys[i]]);
+ else
+ UtilityFunctions::push_error("WorldProperty keys have to be string names (expected ", Variant::STRING_NAME, " is ", keys[i].get_type(), ")");
+ }
+}
+}
diff --git a/src/action.hpp b/src/action.hpp
new file mode 100644
index 0000000..582c342
--- /dev/null
+++ b/src/action.hpp
@@ -0,0 +1,43 @@
+#ifndef GOAP_ACTION_HPP
+#define GOAP_ACTION_HPP
+
+#include "state.hpp"
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace godot { class CharacterActor; }
+
+namespace godot::goap {
+typedef HashMap WorldState;
+typedef KeyValue WorldProperty;
+
+class Action : public Resource {
+ GDCLASS(Action, Resource);
+ static void _bind_methods();
+public:
+ void set_context_prerequisites(Dictionary dict);
+ Dictionary get_context_prerequisites() const;
+ void set_prerequisites(Dictionary dict);
+ Dictionary get_prerequisites() const;
+ void set_effects(Dictionary dict);
+ Dictionary get_effects() const;
+ void set_apply_state(Ref args);
+ Ref get_apply_state() const;
+
+ bool get_is_completed(CharacterActor *context);
+
+ static Dictionary property_map_to_dict(WorldState const &props);
+ static void dict_to_property_map(Dictionary const &dict, WorldState &out);
+public:
+ WorldState context_prerequisites{};
+ WorldState prerequisites{};
+ WorldState effects{};
+ Ref apply_state{};
+};
+}
+
+#endif //!GOAP_ACTION_HPP
diff --git a/src/character_actor.cpp b/src/character_actor.cpp
index e22fb60..525b294 100644
--- a/src/character_actor.cpp
+++ b/src/character_actor.cpp
@@ -1,5 +1,9 @@
#include "character_actor.hpp"
+#include "planner.hpp"
#include "projectile_pool.hpp"
+#include "state.hpp"
+#include "tunnels_game_mode.hpp"
+#include "utils/game_root.hpp"
#include "utils/godot_macros.h"
#include
#include
@@ -12,6 +16,8 @@ void CharacterActor::_bind_methods() {
#define CLASSNAME CharacterActor
GDPROPERTY_HINTED(rotation_speed_curve, Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "Curve");
GDFUNCTION_ARGS(set_velocity_target, "value");
+ GDFUNCTION(get_is_near_player);
+ GDFUNCTION(get_player_character);
}
void CharacterActor::_enter_tree() { GDGAMEONLY();
@@ -20,23 +26,26 @@ void CharacterActor::_enter_tree() { GDGAMEONLY();
this->target_rotation = this->get_global_transform().get_basis().get_quaternion();
this->health = this->get_node("Health");
this->primary_weapon_pool = this->get_node("ProjectilePool");
+ this->planner = this->get_node("Planner");
}
void CharacterActor::_process(double delta_time) { GDGAMEONLY();
this->process_rotation(delta_time);
if(!this->mode_manual) {
- this->process_ai(delta_time);
+ this->process_behaviour(delta_time);
+ this->process_navigation(delta_time);
}
- if(this->firing)
+ if(this->firing) {
this->try_fire_weapon();
+ }
}
void CharacterActor::_physics_process(double delta_time) { GDGAMEONLY();
// accelerate towards velocity target
Vector3 const new_velocity = this->get_velocity().move_toward(this->velocity_target, delta_time * CharacterActor::ACCELERATION);
- // only apply velocity if not grounded
- Vector3 const gravity{this->is_on_floor() ? Vector3() : Vector3{0.f, this->get_velocity().y - 9.8f, 0.f}};
- this->set_velocity(new_velocity + gravity);
+ Vector3 const gravity{Vector3{0.f, this->get_velocity().y - 9.8f, 0.f}};
+ // apply either gravity or walking velocity depending on results
+ this->set_velocity(this->is_on_floor() ? new_velocity : this->get_velocity() + gravity);
// update position
this->move_and_slide();
}
@@ -65,9 +74,7 @@ void CharacterActor::aim_direction(Vector3 direction) {
void CharacterActor::move_to(Vector3 to, float target_distance) {
this->nav_agent->set_target_desired_distance(target_distance);
- this->nav_agent->set_target_position(this->get_global_position().distance_squared_to(to) < target_distance * target_distance
- ? this->get_global_position()
- : to);
+ this->nav_agent->set_target_position(to);
}
void CharacterActor::shoot_at(Vector3 at) {
@@ -84,6 +91,7 @@ void CharacterActor::set_manual_mode(bool value) {
ProcessMode const mode = value ? ProcessMode::PROCESS_MODE_DISABLED : ProcessMode::PROCESS_MODE_PAUSABLE;
//this->nav_agent->set_process_mode(mode);
this->nav_agent->set_avoidance_priority(value ? 1.f : 0.9f);
+ this->set_state(goap::State::new_invalid());
}
void CharacterActor::set_rotation_speed_curve(Ref curve) {
@@ -120,10 +128,54 @@ Vector3 CharacterActor::get_velocity_target() const {
return this->velocity_target;
}
-void CharacterActor::process_ai(double delta_time) {
- float const distance = this->nav_agent->get_target_position().distance_squared_to(this->get_global_position());
- float const target_distance_sqr = std::pow(this->nav_agent->get_target_desired_distance(), 2.f);
- if(!this->nav_agent->is_navigation_finished() && distance >= target_distance_sqr) {
+bool CharacterActor::get_is_near_player() const {
+ return this->get_player_character()->get_global_position().distance_to(this->get_global_position()) < 5.f;
+}
+
+CharacterActor *CharacterActor::get_player_character() const {
+ Ref game_mode = GameRoot::get_singleton()->get_game_mode();
+ return game_mode->get_player_instance()->get_character();
+}
+
+void CharacterActor::set_state(goap::State state) {
+ switch(this->current_state.type) {
+ default:
+ break;
+ case goap::State::STATE_MOVE_TO:
+ this->nav_agent->set_target_position(this->get_global_position());
+ break;
+ }
+ this->current_state = state;
+ switch(state.type) {
+ default:
+ break;
+ case goap::State::STATE_MOVE_TO:
+ this->move_to(state.move_to->get_global_position());
+ break;
+ }
+}
+
+void CharacterActor::process_behaviour(double delta_time) {
+ if(this->current_state.is_complete(this) || this->planner->is_action_complete())
+ this->set_state(this->planner->get_next_state());
+ switch(this->current_state.type) {
+ default:
+ break;
+ case goap::State::STATE_MOVE_TO:
+ if(this->nav_agent->get_target_position().distance_to(this->current_state.move_to->get_global_position()) > 2.f)
+ this->nav_agent->set_target_position(this->current_state.move_to->get_global_position());
+ break;
+ case goap::State::STATE_ACTIVATE:
+ break;
+ case goap::State::STATE_ANIMATE:
+ break;
+ }
+}
+
+void CharacterActor::process_navigation(double delta_time) {
+ float const distance_sqr = this->nav_agent->get_target_position().distance_squared_to(this->get_global_position());
+ float const distance_target_sqr = std::pow(this->nav_agent->get_target_desired_distance(), 2.f);
+ if(!this->nav_agent->is_navigation_finished() && distance_sqr >= distance_target_sqr) {
Vector3 const target_position = this->nav_agent->get_next_path_position();
Vector3 const direction = (target_position - this->get_global_position()).normalized();
if(this->nav_agent->get_avoidance_enabled())
diff --git a/src/character_actor.hpp b/src/character_actor.hpp
index 686964b..1d83bce 100644
--- a/src/character_actor.hpp
+++ b/src/character_actor.hpp
@@ -4,12 +4,17 @@
#include "character_data.hpp"
#include "health.hpp"
#include "projectile_pool.hpp"
+#include "state.hpp"
#include
#include
namespace godot {
class NavigationAgent3D;
class TunnelsPlayer;
+class AnimationPlayer;
+namespace goap {
+ class Planner;
+};
class CharacterActor : public CharacterBody3D,
public IHealthEntity {
@@ -19,44 +24,66 @@ public:
virtual void _enter_tree() override;
virtual void _process(double delta_time) override;
virtual void _physics_process(double delta_time) override;
+ // manually set target_velocity
void move(Vector3 world_vector);
+ // manually aim at a target position
+ // calls aim_direction with the flattened direction to 'at'
void aim(Vector3 at);
+ // manually set the forward vector of target_rotation
void aim_direction(Vector3 direction);
+ // set a movement target to navigate towards
void move_to(Vector3 to, float target_distance = 0.5f);
+ // fire weapon at a target position
+ // calls aim(at) and set_firing(true)
void shoot_at(Vector3 at);
+ // getter-setters
void set_firing(bool firing);
void set_manual_mode(bool value);
void set_rotation_speed_curve(Ref curve);
Ref get_rotation_speed_curve() const;
-
virtual Health *get_health() override;
virtual Health const *get_health() const override;
-
void set_character_data(Ref data);
-
void set_weapon_muzzle(Node3D *node);
-
void set_velocity_target(Vector3 value);
Vector3 get_velocity_target() const;
+ bool get_is_near_player() const;
+ CharacterActor *get_player_character() const;
+ void set_state(goap::State state);
protected:
- void process_ai(double delta_time);
+ void process_behaviour(double delta_time);
+ void process_navigation(double delta_time);
void process_rotation(double delta_time);
void try_fire_weapon();
private:
+ // desired velocity, accelerated towards each frame
Vector3 velocity_target{0.f,0.f,0.f};
+ // target rotation, slerped towards each frame
Basis target_rotation{};
- NavigationAgent3D *nav_agent{nullptr};
+ // ignore any ai planning or navigation
bool mode_manual{false};
+ // fire weapon at whatever we're aiming at
+ bool firing{false};
+ // the next timestamp at which a weapon can be fired
+ float fire_timer{0.f};
+ // the origin point for projectiles
+ Node3D *weapon_muzzle{nullptr};
+ // something that the AI wants to target
+ Node *target{nullptr};
+ // the current state of the actor
+ goap::State current_state{goap::State::new_invalid()};
+ AnimationPlayer *anim_player{nullptr};
+
Health *health{nullptr};
ProjectilePool *primary_weapon_pool{nullptr};
- Ref data;
- float fire_interval{0.f};
- bool firing{false};
- float fire_timer{0.f};
- Node3D *weapon_muzzle{nullptr};
+ NavigationAgent3D *nav_agent{nullptr};
+ goap::Planner *planner{nullptr};
Ref rotation_speed_curve{};
+ // character data assigned when spawned
+ Ref data;
+ float fire_interval{0.f}; // derived from 1 / the current weapon's rps
static float const ACCELERATION;
static float const WALK_SPEED;
diff --git a/src/global_world_state.cpp b/src/global_world_state.cpp
new file mode 100644
index 0000000..3152b07
--- /dev/null
+++ b/src/global_world_state.cpp
@@ -0,0 +1,60 @@
+#include "global_world_state.hpp"
+#include "character_actor.hpp"
+#include "utils/game_root.hpp"
+
+namespace godot::goap {
+void GlobalWorldState::_bind_methods() {
+#define CLASSNAME GlobalWorldState
+}
+
+bool GlobalWorldState::has_singleton() {
+ return GlobalWorldState::singleton_instance != nullptr;
+}
+
+GlobalWorldState *GlobalWorldState::get_singleton() {
+ return GlobalWorldState::singleton_instance;
+}
+
+void GlobalWorldState::_enter_tree() {
+ if(GlobalWorldState::singleton_instance == nullptr)
+ GlobalWorldState::singleton_instance = this;
+}
+
+void GlobalWorldState::_ready() {
+ this->game_mode = GameRoot::get_singleton()->get_game_mode();
+}
+
+void GlobalWorldState::_exit_tree() {
+ if(GlobalWorldState::singleton_instance == this)
+ GlobalWorldState::singleton_instance = nullptr;
+}
+
+void GlobalWorldState::_process(double delta_time) {
+ global_state_cache.clear(); // invalidate cache
+}
+
+Vector3 GlobalWorldState::get_player_position() {
+ return this->game_mode->get_player_instance()->get_character()->get_global_position();
+}
+
+Variant GlobalWorldState::get_world_property(StringName prop_key) {
+ // check if prop key corresponds to a global key
+ if(!prop_key.begins_with("g_"))
+ return nullptr;
+ // check if the key is cached for this frame
+ else if(global_state_cache.has(prop_key))
+ return global_state_cache.get(prop_key);
+ // fetch by function name
+ StringName const fn = "get_" + prop_key.right(prop_key.length() - 2);
+ if(this->has_method(fn)) {
+ Variant result = this->call(fn);
+ // cache and return
+ this->global_state_cache.insert(prop_key, result);
+ return result;
+ }
+ return nullptr;
+}
+
+GlobalWorldState *GlobalWorldState::singleton_instance{nullptr};
+}
+
diff --git a/src/global_world_state.hpp b/src/global_world_state.hpp
new file mode 100644
index 0000000..a8f1eed
--- /dev/null
+++ b/src/global_world_state.hpp
@@ -0,0 +1,31 @@
+#ifndef GOAP_GLOBAL_WORLD_STATE_HPP
+#define GOAP_GLOBAL_WORLD_STATE_HPP
+
+#include "action.hpp"
+#include "tunnels_game_mode.hpp"
+#include
+
+namespace godot::goap {
+class GlobalWorldState : public Node {
+ GDCLASS(GlobalWorldState, Node);
+ static void _bind_methods();
+public:
+ static bool has_singleton();
+ static GlobalWorldState *get_singleton();
+
+ virtual void _enter_tree() override;
+ virtual void _ready() override;
+ virtual void _exit_tree() override;
+ virtual void _process(double delta_time) override;
+
+ Vector3 get_player_position();
+
+ Variant get_world_property(StringName prop_key);
+private:
+ WorldState global_state_cache{};
+ Ref game_mode{};
+ static GlobalWorldState *singleton_instance;
+};
+}
+
+#endif // !GOAP_GLOBAL_WORLD_STATE_HPP
diff --git a/src/planner.cpp b/src/planner.cpp
new file mode 100644
index 0000000..cdd1235
--- /dev/null
+++ b/src/planner.cpp
@@ -0,0 +1,237 @@
+#include "planner.hpp"
+#include "action.hpp"
+#include "character_actor.hpp"
+#include "global_world_state.hpp"
+#include "state.hpp"
+#include "utils/godot_macros.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace godot::goap {
+typedef HashMap FromMap;
+typedef HashMap ScoreMap;
+typedef HashSet NodeSet;
+
+void Goal::_bind_methods() {
+#define CLASSNAME Goal
+ GDPROPERTY(goal_state, Variant::DICTIONARY);
+ GDPROPERTY(prerequisites, Variant::DICTIONARY);
+}
+
+void Goal::set_goal_state(Dictionary dict) {
+ Action::dict_to_property_map(dict, this->goal_state);
+}
+
+Dictionary Goal::get_goal_state() const {
+ return Action::property_map_to_dict(this->goal_state);
+}
+
+void Goal::set_prerequisites(Dictionary dict) {
+ Action::dict_to_property_map(dict, this->prerequisites);
+}
+
+Dictionary Goal::get_prerequisites() const {
+ return Action::property_map_to_dict(this->prerequisites);
+}
+
+#undef CLASSNAME // !Goal
+
+PlannerNode PlannerNode::goal_node(WorldState const &goal) {
+ return PlannerNode{goal};
+}
+
+PlannerNode PlannerNode::new_node_along(Ref action) const {
+ PlannerNode new_node{
+ action->prerequisites,
+ action->effects,
+ action,
+ };
+ for(WorldProperty const &prop : this->state)
+ new_node.state.insert(prop.key, prop.value);
+ for(WorldProperty const &prop : this->open_requirements)
+ new_node.open_requirements.insert(prop.key, prop.value);
+ return new_node;
+}
+
+void Planner::_bind_methods() {
+#define CLASSNAME Planner
+ GDPROPERTY_HINTED(actions, Variant::ARRAY, PROPERTY_HINT_ARRAY_TYPE, GDRESOURCETYPE(Action));
+ GDPROPERTY_HINTED(goals, Variant::ARRAY, PROPERTY_HINT_ARRAY_TYPE, GDRESOURCETYPE(Goal));
+}
+
+void Planner::_ready() {
+ this->global_world_state = GlobalWorldState::get_singleton();
+ this->actor = Object::cast_to(this->get_parent());
+}
+
+static Vector[> trace_path(FromMap &map, PlannerNode &end) {
+ Vector][> edges{};
+ PlannerNode node{end};
+ while(node.last_edge.is_valid()) {
+ edges.push_back(node.last_edge);
+ node = map.get(node);
+ }
+ return edges;
+}
+
+Vector][> Planner::make_plan() {
+ // clear cache every planning phase
+ this->cached_world_state.clear();
+ Ref goal = this->select_goal();
+ if(!goal.is_valid())
+ return {};
+ // ordered list of all nodes still being considered
+ Vector open{PlannerNode::goal_node(goal->goal_state)};
+ PlannerNode first = open.get(0);
+ FromMap from{}; // mapping states to the previous in the path
+ ScoreMap dist_traveled{}; // mapping states to the shortest found distance from start
+ dist_traveled.insert(first, 0);
+ ScoreMap best_guess{}; // mapping states to the best guess of the distance to the goal
+ best_guess.insert(first, first.open_requirements.size());
+ PlannerNode current{}; // state we're checking for neighbours or completion
+ while(!open.is_empty()) {
+ // current is the top of the ordered list
+ current = open.get(0);
+ // check if we've reached the goal
+ if(current.open_requirements.is_empty())
+ return trace_path(from, current);
+ // current is no longer considered as it cannot be the end
+ open.erase(current);
+ // find all neighbours of this state
+ Vector neighbours = this->find_neighbours_of(current);
+ for(PlannerNode const& node : neighbours) {
+ float const new_dist = dist_traveled.get(current) + 1.f; // unweighed distance traveled to neighbour
+ if(!dist_traveled.has(node) || new_dist < dist_traveled.get(node)) {
+ // store distances
+ dist_traveled[node] = new_dist;
+ best_guess[node] = new_dist + node.open_requirements.size();
+ from[node] = current;
+ int i = open.find(node);
+ if(i != -1)
+ open.remove_at(i);
+ open.ordered_insert(node);
+ }
+ }
+ }
+ return {};
+}
+
+Ref Planner::select_goal() {
+ for(Ref const &goal : this->goals) {
+ bool can_try{true};
+ for(WorldProperty const &prop : goal->prerequisites) {
+ if(prop.value != this->get_world_property(prop.key)) {
+ can_try = false;
+ break;
+ }
+ }
+ if(can_try) return goal;
+ }
+ return {};
+}
+
+Variant Planner::get_world_property(StringName prop_key) {
+ if(prop_key.begins_with("g_")) {
+ return this->global_world_state->get_world_property(prop_key);
+ } else if(this->cached_world_state.has(prop_key)) {
+ return this->cached_world_state.get(prop_key);
+ } else if(this->actor->has_method("get_" + prop_key)) {
+ Variant val = this->actor->call("get_" + prop_key);
+ this->cached_world_state.insert(prop_key, val);
+ return val;
+ } else return nullptr;
+}
+
+bool Planner::can_do(Ref action) {
+ for(WorldProperty &prop : action->context_prerequisites) {
+ if(this->get_world_property(prop.key) != prop.value)
+ return false;
+ }
+ return true;
+}
+
+Vector Planner::find_neighbours_of(PlannerNode &node) {
+ Vector neighbours{};
+ for(Ref const &action : this->find_actions_satisfying(node.open_requirements)) {
+ PlannerNode new_node = node.new_node_along(action);
+ // remove all satisfied requirements
+ for(WorldProperty const &delta : action->effects) {
+ if(new_node.open_requirements.has(delta.key)
+ && new_node.open_requirements.get(delta.key) == delta.value) {
+ new_node.open_requirements.erase(delta.key);
+ }
+ }
+ neighbours.push_back(new_node);
+ }
+ return neighbours;
+}
+
+Vector][> Planner::find_actions_satisfying(WorldState requirements) {
+ Vector][> found_actions{};
+ for(Ref &act : this->actions) {
+ for(WorldProperty &prop : requirements) {
+ if(act->effects.has(prop.key)
+ && act->effects.get(prop.key) == prop.value
+ && this->can_do(act)) {
+ found_actions.push_back(act);
+ }
+ }
+ }
+ return found_actions;
+}
+
+bool Planner::is_action_complete() {
+ return this->plan.get(0)->get_is_completed(this->actor);
+}
+
+State Planner::get_next_state() {
+ if(!this->plan.is_empty())
+ this->plan.remove_at(0);
+ if(this->plan.is_empty())
+ this->plan = this->make_plan();
+ if(this->plan.is_empty())
+ return State::new_invalid();
+ return this->plan.get(0)->apply_state->construct(this->actor);
+}
+
+void Planner::set_actions(Array value) {
+ this->actions.clear();
+ this->actions.resize(value.size());
+ for(size_t i{0}; i < value.size(); ++i) {
+ Ref act = value[i];
+ if(act.is_valid())
+ this->actions.set(i, act);
+ }
+}
+
+Array Planner::get_actions() const {
+ Array array{};
+ for(Ref const &act : this->actions) {
+ array.push_back(act);
+ }
+ return array;
+}
+
+void Planner::set_goals(Array value) {
+ this->goals.clear();
+ this->goals.resize(value.size());
+ for(size_t i{0}; i < value.size(); ++i) {
+ Ref goal = value[i];
+ if(goal.is_valid())
+ this->goals.set(i, goal);
+ }
+}
+
+Array Planner::get_goals() const {
+ Array array{};
+ for(Ref const &goal : this->goals) {
+ array.push_back(goal);
+ }
+ return array;
+}
+}
diff --git a/src/planner.hpp b/src/planner.hpp
new file mode 100644
index 0000000..9bf9e73
--- /dev/null
+++ b/src/planner.hpp
@@ -0,0 +1,109 @@
+#ifndef GOAP_PLANNER_HPP
+#define GOAP_PLANNER_HPP
+
+#include "action.hpp"
+#include "godot_cpp/variant/variant.hpp"
+#include
+#include
+#include
+#include
+
+namespace godot {
+class CharacterActor;
+
+namespace goap {
+class GlobalWorldState;
+
+class Goal : public Resource {
+ GDCLASS(Goal, Resource);
+ static void _bind_methods();
+
+ void set_goal_state(Dictionary dict);
+ Dictionary get_goal_state() const;
+ void set_prerequisites(Dictionary dict);
+ Dictionary get_prerequisites() const;
+public:
+ WorldState goal_state{};
+ WorldState prerequisites{};
+};
+
+struct PlannerNode {
+ WorldState open_requirements{};
+ WorldState state{};
+ Ref last_edge{};
+
+ PlannerNode() = default;
+ PlannerNode(PlannerNode const &src) = default;
+
+ static PlannerNode goal_node(WorldState const &goal);
+
+ PlannerNode new_node_along(Ref action) const;
+};
+
+class Planner : public Node {
+ GDCLASS(Planner, Node);
+ static void _bind_methods();
+public:
+ virtual void _ready() override;
+
+ Vector][> make_plan();
+ Ref select_goal();
+
+ Variant get_world_property(StringName prop_key);
+
+ bool can_do(Ref action);
+ Vector find_neighbours_of(PlannerNode &node);
+ Vector][> find_actions_satisfying(WorldState requirements);
+
+ bool is_action_complete();
+ State get_next_state();
+
+ void set_actions(Array actions);
+ Array get_actions() const;
+ void set_goals(Array goals);
+ Array get_goals() const;
+private:
+ CharacterActor *actor{nullptr}; // the parent actor of this planner
+ WorldState cached_world_state{}; // the cached worldstate, cleared for every make_plan call
+ GlobalWorldState *global_world_state{nullptr}; // cached singleton instance
+ // configured settings
+ Vector][> actions{}; // available actions
+ Vector][> goals{}; // available goals
+ Vector][> plan{};
+};
+
+struct PlannerNodeHasher {
+ static _FORCE_INLINE_
+ uint32_t hash(godot::goap::PlannerNode const &node) {
+ uint32_t hash{1};
+ for(KeyValue const &kvp : node.state) {
+ hash = hash_murmur3_one_32(kvp.key.hash(), hash);
+ hash = hash_murmur3_one_32(kvp.value.hash(), hash);
+ }
+ return hash_fmix32(hash);
+ }
+};
+
+static _FORCE_INLINE_ bool operator==(PlannerNode const &lhs, PlannerNode const &rhs) {
+ return PlannerNodeHasher::hash(lhs) == PlannerNodeHasher::hash(rhs);
+}
+static _FORCE_INLINE_ bool operator!=(PlannerNode const &lhs, PlannerNode const &rhs) {
+ return !(lhs == rhs);
+}
+static _FORCE_INLINE_ bool operator<(PlannerNode const &lhs, PlannerNode const &rhs) {
+ return lhs.open_requirements.size() < rhs.open_requirements.size();
+}
+static _FORCE_INLINE_ bool operator>=(PlannerNode const &lhs, PlannerNode const &rhs) {
+ return !(lhs < rhs);
+}
+static _FORCE_INLINE_ bool operator>(PlannerNode const &lhs, PlannerNode const &rhs) {
+ return lhs.open_requirements.size() > rhs.open_requirements.size();
+}
+static _FORCE_INLINE_ bool operator<=(PlannerNode const &lhs, PlannerNode const &rhs) {
+ return !(lhs > rhs);
+}
+}
+}
+
+
+#endif // !GOAP_PLANNER_HPP
diff --git a/src/register_types.cpp b/src/register_types.cpp
index d2ce3f3..8a05f53 100644
--- a/src/register_types.cpp
+++ b/src/register_types.cpp
@@ -1,10 +1,14 @@
#include "register_types.h"
-#include "character_data.hpp"
+#include "action.hpp"
#include "character_actor.hpp"
+#include "character_data.hpp"
#include "enemy.hpp"
+#include "global_world_state.hpp"
#include "health.hpp"
#include "pellet_projectile.hpp"
+#include "planner.hpp"
#include "projectile_pool.hpp"
+#include "state.hpp"
#include "tunnels_game_mode.hpp"
#include "tunnels_game_state.hpp"
#include "tunnels_player.hpp"
@@ -51,6 +55,15 @@ void initialize_gdextension_types(ModuleInitializationLevel p_level)
ClassDB::register_class();
ClassDB::register_class();
+
+ ClassDB::register_class();
+ ClassDB::register_class();
+ ClassDB::register_abstract_class();
+ ClassDB::register_class();
+ ClassDB::register_class();
+ ClassDB::register_class();
+ ClassDB::register_class();
+ ClassDB::register_class();
}
extern "C"
diff --git a/src/state.cpp b/src/state.cpp
new file mode 100644
index 0000000..bdb1900
--- /dev/null
+++ b/src/state.cpp
@@ -0,0 +1,80 @@
+#include "state.hpp"
+#include "character_actor.hpp"
+#include "utils/godot_macros.h"
+
+namespace godot::goap {
+State::~State() {
+ if(unlikely(this->type == STATE_ANIMATE))
+ delete this->animate;
+}
+
+State State::new_move_to(Node3D *node) {
+ return {
+ .type = State::Type::STATE_MOVE_TO,
+ .move_to = node
+ };
+}
+
+State State::new_animate(StringName animation) {
+ return {
+ .type = State::Type::STATE_ANIMATE,
+ .animate = new StringName(animation)
+ };
+}
+
+State State::new_activate(Node *node) {
+ return {
+ .type = State::Type::STATE_ACTIVATE,
+ .activate = node
+ };
+}
+
+State State::new_invalid() {
+ return { .type = State::Type::STATE_TYPE_MAX };
+}
+
+bool State::is_complete(CharacterActor *context) const {
+ switch(this->type) {
+ default:
+ return true;
+ case STATE_MOVE_TO:
+ return context->get_global_position().is_equal_approx(this->move_to->get_global_position());
+ case STATE_ANIMATE:
+ return false; // TODO: replace this with checks for animation completion
+ case STATE_ACTIVATE:
+ return false; // TODO: replace this with checks for object activation
+ }
+}
+
+void StateArgs::_bind_methods() {
+#define CLASSNAME StateArgs
+ GDPROPERTY(argument_property, Variant::STRING_NAME);
+}
+
+State StateArgs::construct(Node *context) const {
+ return { .type = State::STATE_TYPE_MAX };
+}
+
+void StateArgs::set_argument_property(StringName var) { this->argument_property = var; }
+StringName StateArgs::get_argument_property() const { return this->argument_property; }
+
+void MoveStateArgs::_bind_methods() {}
+
+State MoveStateArgs::construct(Node *context) const {
+ Node3D *node = Object::cast_to(context->call("get_" + this->argument_property));
+ return State::new_move_to(node);
+}
+
+void AnimateStateArgs::_bind_methods() {}
+
+State AnimateStateArgs::construct(Node *context) const {
+ return State::new_animate(context->call("get_" + this->argument_property));
+}
+
+void ActivateStateArgs::_bind_methods() {}
+
+State ActivateStateArgs::construct(Node *context) const {
+ Node *node = Object::cast_to(context->call("get_" + this->argument_property));
+ return State::new_activate(node);
+}
+}
diff --git a/src/state.hpp b/src/state.hpp
new file mode 100644
index 0000000..1e9986c
--- /dev/null
+++ b/src/state.hpp
@@ -0,0 +1,66 @@
+#ifndef GOAP_STATE_HPP
+#define GOAP_STATE_HPP
+
+#include
+#include
+#include
+#include
+#include
+
+namespace godot { class CharacterActor; }
+namespace godot::goap {
+struct State {
+ ~State();
+ static State new_move_to(Node3D *location);
+ static State new_animate(StringName animation);
+ static State new_activate(Node *node);
+ static State new_invalid();
+
+ bool is_complete(CharacterActor *context) const;
+
+ enum Type {
+ STATE_MOVE_TO,
+ STATE_ANIMATE,
+ STATE_ACTIVATE,
+ STATE_TYPE_MAX,
+ };
+ State::Type type{STATE_TYPE_MAX};
+ union {
+ Node3D* move_to;
+ StringName *animate;
+ Node *activate;
+ };
+};
+
+class StateArgs : public Resource {
+ GDCLASS(StateArgs, Resource);
+ static void _bind_methods();
+public:
+ virtual State construct(Node *context) const;
+ void set_argument_property(StringName name);
+ StringName get_argument_property() const;
+ StringName argument_property;
+};
+
+class MoveStateArgs : public StateArgs {
+ GDCLASS(MoveStateArgs, StateArgs);
+ static void _bind_methods();
+ virtual State construct(Node *context) const override;
+};
+
+class AnimateStateArgs : public StateArgs {
+ GDCLASS(AnimateStateArgs, StateArgs);
+ static void _bind_methods();
+public:
+ virtual State construct(Node *context) const override;
+};
+
+class ActivateStateArgs : public StateArgs {
+ GDCLASS(ActivateStateArgs, StateArgs);
+ static void _bind_methods();
+public:
+ virtual State construct(Node *context) const override;
+};
+};
+
+#endif // !GOAP_STATE_HPP
]