Enemies and now using the GOAP AI to decide when to attack the player, but it's very rough right now. I need to sort out how to store the AI states and use them in the System.
This commit is contained in:
parent
77f2e94515
commit
ad71631809
13 changed files with 61 additions and 30 deletions
4
Makefile
4
Makefile
|
@ -22,7 +22,7 @@ tracy_build:
|
||||||
meson compile -j 10 -C builddir
|
meson compile -j 10 -C builddir
|
||||||
|
|
||||||
test: build
|
test: build
|
||||||
./builddir/runtests "[ai]"
|
./builddir/runtests
|
||||||
|
|
||||||
run: build test
|
run: build test
|
||||||
powershell "cp ./builddir/zedcaster.exe ."
|
powershell "cp ./builddir/zedcaster.exe ."
|
||||||
|
@ -41,7 +41,7 @@ clean:
|
||||||
meson compile --clean -C builddir
|
meson compile --clean -C builddir
|
||||||
|
|
||||||
debug_test: build
|
debug_test: build
|
||||||
gdb --nx -x .gdbinit --ex run --args builddir/runtests.exe -e "[ai]"
|
gdb --nx -x .gdbinit --ex run --args builddir/runtests.exe -e
|
||||||
|
|
||||||
win_installer:
|
win_installer:
|
||||||
powershell 'start "C:\Program Files (x86)\solicus\InstallForge\bin\ifbuilderenvx86.exe" win_installer.ifp'
|
powershell 'start "C:\Program Files (x86)\solicus\InstallForge\bin\ifbuilderenvx86.exe" win_installer.ifp'
|
||||||
|
|
1
ai.cpp
1
ai.cpp
|
@ -18,6 +18,7 @@ namespace ai {
|
||||||
Action config_action(AIProfile& profile, nlohmann::json& config) {
|
Action config_action(AIProfile& profile, nlohmann::json& config) {
|
||||||
check(config.contains("name"), "config_action: action config missing name");
|
check(config.contains("name"), "config_action: action config missing name");
|
||||||
check(config.contains("cost"), "config_action: action config missing cost");
|
check(config.contains("cost"), "config_action: action config missing cost");
|
||||||
|
check(config["cost"] < STATE_MAX, "config_action: action cost is greater than STATE_MAX");
|
||||||
|
|
||||||
Action result(config["name"], config["cost"]);
|
Action result(config["name"], config["cost"]);
|
||||||
|
|
||||||
|
|
|
@ -7,13 +7,15 @@
|
||||||
"no_more_enemies": 4,
|
"no_more_enemies": 4,
|
||||||
"in_combat": 5,
|
"in_combat": 5,
|
||||||
"have_item": 6,
|
"have_item": 6,
|
||||||
"have_healing": 7
|
"have_healing": 7,
|
||||||
|
"detect_enemy": 8
|
||||||
},
|
},
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"name": "find_enemy",
|
"name": "find_enemy",
|
||||||
"cost": 5,
|
"cost": 5,
|
||||||
"needs": {
|
"needs": {
|
||||||
|
"detect_enemy": true,
|
||||||
"in_combat": false,
|
"in_combat": false,
|
||||||
"no_more_enemies": false,
|
"no_more_enemies": false,
|
||||||
"enemy_found": false
|
"enemy_found": false
|
||||||
|
@ -68,7 +70,8 @@
|
||||||
"no_more_enemies": false,
|
"no_more_enemies": false,
|
||||||
"in_combat": false,
|
"in_combat": false,
|
||||||
"have_item": false,
|
"have_item": false,
|
||||||
"have_healing": false
|
"have_healing": false,
|
||||||
|
"detect_enemy": true
|
||||||
},
|
},
|
||||||
"Walker::final_state": {
|
"Walker::final_state": {
|
||||||
"enemy_found": true,
|
"enemy_found": true,
|
||||||
|
@ -79,12 +82,14 @@
|
||||||
"no_more_enemies": true
|
"no_more_enemies": true
|
||||||
},
|
},
|
||||||
"Enemy::initial_state": {
|
"Enemy::initial_state": {
|
||||||
|
"detect_enemy": false,
|
||||||
"enemy_found": false,
|
"enemy_found": false,
|
||||||
"enemy_dead": false,
|
"enemy_dead": false,
|
||||||
"health_good": true,
|
"health_good": true,
|
||||||
"in_combat": false
|
"in_combat": false
|
||||||
},
|
},
|
||||||
"Enemy::final_state": {
|
"Enemy::final_state": {
|
||||||
|
"detect_enemy": true,
|
||||||
"enemy_found": true,
|
"enemy_found": true,
|
||||||
"enemy_dead": true,
|
"enemy_dead": true,
|
||||||
"health_good": true
|
"health_good": true
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
},
|
},
|
||||||
{"_type": "Combat", "hp": 20, "max_hp": 20, "damage": 1, "dead": false},
|
{"_type": "Combat", "hp": 20, "max_hp": 20, "damage": 1, "dead": false},
|
||||||
{"_type": "Motion", "dx": 0, "dy": 0, "random": false},
|
{"_type": "Motion", "dx": 0, "dy": 0, "random": false},
|
||||||
{"_type": "EnemyConfig", "hearing_distance": 5},
|
{"_type": "EnemyConfig", "hearing_distance": 5, "ai_script": "Enemy::actions", "ai_start_name": "Enemy::initial_state", "ai_goal_name": "Enemy::final_state"},
|
||||||
{"_type": "Animation", "easing": 1, "ease_rate": 0.2, "scale": 0.1, "simple": true, "frames": 10, "speed": 0.3, "stationary": false},
|
{"_type": "Animation", "easing": 1, "ease_rate": 0.2, "scale": 0.1, "simple": true, "frames": 10, "speed": 0.3, "stationary": false},
|
||||||
{"_type": "Sprite", "name": "armored_knight", "width": 256, "height": 256, "width": 256, "height": 256, "scale": 1.0},
|
{"_type": "Sprite", "name": "armored_knight", "width": 256, "height": 256, "width": 256, "height": 256, "scale": 1.0},
|
||||||
{"_type": "Sound", "attack": "Sword_Hit_2", "death": "Humanoid_Death_1"}
|
{"_type": "Sound", "attack": "Sword_Hit_2", "death": "Humanoid_Death_1"}
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
},
|
},
|
||||||
{"_type": "Combat", "hp": 40, "max_hp": 40, "damage": 10, "dead": false},
|
{"_type": "Combat", "hp": 40, "max_hp": 40, "damage": 10, "dead": false},
|
||||||
{"_type": "Motion", "dx": 0, "dy": 0, "random": true},
|
{"_type": "Motion", "dx": 0, "dy": 0, "random": true},
|
||||||
{"_type": "EnemyConfig", "hearing_distance": 5},
|
{"_type": "EnemyConfig", "hearing_distance": 5, "ai_script": "Enemy::actions", "ai_start_name": "Enemy::initial_state", "ai_goal_name": "Enemy::final_state"},
|
||||||
{"_type": "Sprite", "name": "axe_ranger", "width": 256, "height": 256, "scale": 1.0},
|
{"_type": "Sprite", "name": "axe_ranger", "width": 256, "height": 256, "scale": 1.0},
|
||||||
{"_type": "Animation", "easing": 3, "ease_rate": 0.5, "scale": 0.1, "simple": false, "frames": 2, "speed": 0.6, "stationary": false},
|
{"_type": "Animation", "easing": 3, "ease_rate": 0.5, "scale": 0.1, "simple": false, "frames": 2, "speed": 0.6, "stationary": false},
|
||||||
{"_type": "Sound", "attack": "Sword_Hit_2", "death": "Ranger_1"}
|
{"_type": "Sound", "attack": "Sword_Hit_2", "death": "Ranger_1"}
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
},
|
},
|
||||||
{"_type": "Combat", "hp": 20, "max_hp": 20, "damage": 20, "dead": false},
|
{"_type": "Combat", "hp": 20, "max_hp": 20, "damage": 20, "dead": false},
|
||||||
{"_type": "Motion", "dx": 0, "dy": 0, "random": false},
|
{"_type": "Motion", "dx": 0, "dy": 0, "random": false},
|
||||||
{"_type": "EnemyConfig", "hearing_distance": 10},
|
{"_type": "EnemyConfig", "hearing_distance": 10, "ai_script": "Enemy::actions", "ai_start_name": "Enemy::initial_state", "ai_goal_name": "Enemy::final_state"},
|
||||||
{"_type": "Animation", "easing": 3, "ease_rate": 0.5, "scale": 0.1, "simple": true, "frames": 10, "speed": 1.0, "stationary": false},
|
{"_type": "Animation", "easing": 3, "ease_rate": 0.5, "scale": 0.1, "simple": true, "frames": 10, "speed": 1.0, "stationary": false},
|
||||||
{"_type": "Sprite", "name": "rat_with_sword", "width": 256, "height": 256, "scale": 1.0},
|
{"_type": "Sprite", "name": "rat_with_sword", "width": 256, "height": 256, "scale": 1.0},
|
||||||
{"_type": "Sound", "attack": "Small_Rat", "death": "Creature_Death_1"}
|
{"_type": "Sound", "attack": "Small_Rat", "death": "Creature_Death_1"}
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
},
|
},
|
||||||
{"_type": "Combat", "hp": 20, "max_hp": 20, "damage": 20, "dead": false},
|
{"_type": "Combat", "hp": 20, "max_hp": 20, "damage": 20, "dead": false},
|
||||||
{"_type": "Motion", "dx": 0, "dy": 0, "random": false},
|
{"_type": "Motion", "dx": 0, "dy": 0, "random": false},
|
||||||
{"_type": "EnemyConfig", "hearing_distance": 10},
|
{"_type": "EnemyConfig", "hearing_distance": 10, "ai_script": "Enemy::actions", "ai_start_name": "Enemy::initial_state", "ai_goal_name": "Enemy::final_state"},
|
||||||
{"_type": "Animation", "easing": 2, "ease_rate": 0.5, "scale": 0.1, "simple": true, "frames": 10, "speed": 1.0, "stationary": false},
|
{"_type": "Animation", "easing": 2, "ease_rate": 0.5, "scale": 0.1, "simple": true, "frames": 10, "speed": 1.0, "stationary": false},
|
||||||
{"_type": "Sprite", "name": "hairy_spider", "width": 256, "height": 256, "scale": 1.0},
|
{"_type": "Sprite", "name": "hairy_spider", "width": 256, "height": 256, "scale": 1.0},
|
||||||
{"_type": "Sound", "attack": "Spider_1", "death": "Spider_2"}
|
{"_type": "Sound", "attack": "Spider_1", "death": "Spider_2"}
|
||||||
|
|
|
@ -222,7 +222,7 @@ void Autowalker::handle_boss_fight() {
|
||||||
void Autowalker::handle_player_walk(ai::State& start, ai::State& goal) {
|
void Autowalker::handle_player_walk(ai::State& start, ai::State& goal) {
|
||||||
start = update_state(start);
|
start = update_state(start);
|
||||||
auto a_plan = ai::plan("Walker::actions", start, goal);
|
auto a_plan = ai::plan("Walker::actions", start, goal);
|
||||||
dump_script("\n\n\n-----WALKER SCRIPT", start, a_plan.script);
|
ai::dump_script("\n\n\n-----WALKER SCRIPT", start, a_plan.script);
|
||||||
auto action = a_plan.script.front();
|
auto action = a_plan.script.front();
|
||||||
|
|
||||||
if(action.name == "find_enemy") {
|
if(action.name == "find_enemy") {
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include "easings.hpp"
|
#include "easings.hpp"
|
||||||
#include "json_mods.hpp"
|
#include "json_mods.hpp"
|
||||||
|
#include "goap.hpp"
|
||||||
|
|
||||||
|
|
||||||
namespace components {
|
namespace components {
|
||||||
|
@ -45,6 +46,11 @@ namespace components {
|
||||||
|
|
||||||
struct EnemyConfig {
|
struct EnemyConfig {
|
||||||
int hearing_distance = 10;
|
int hearing_distance = 10;
|
||||||
|
std::string ai_script;
|
||||||
|
std::string ai_start_name;
|
||||||
|
std::string ai_goal_name;
|
||||||
|
ai::State ai_start;
|
||||||
|
ai::State ai_goal;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Debug {
|
struct Debug {
|
||||||
|
@ -138,7 +144,8 @@ namespace components {
|
||||||
ENROLL_COMPONENT(Weapon, damage);
|
ENROLL_COMPONENT(Weapon, damage);
|
||||||
ENROLL_COMPONENT(Loot, amount);
|
ENROLL_COMPONENT(Loot, amount);
|
||||||
ENROLL_COMPONENT(Position, location.x, location.y);
|
ENROLL_COMPONENT(Position, location.x, location.y);
|
||||||
ENROLL_COMPONENT(EnemyConfig, hearing_distance);
|
ENROLL_COMPONENT(EnemyConfig, hearing_distance,
|
||||||
|
ai_script, ai_start_name, ai_goal_name);
|
||||||
ENROLL_COMPONENT(Motion, dx, dy, random);
|
ENROLL_COMPONENT(Motion, dx, dy, random);
|
||||||
ENROLL_COMPONENT(Combat, hp, max_hp, damage, dead);
|
ENROLL_COMPONENT(Combat, hp, max_hp, damage, dead);
|
||||||
ENROLL_COMPONENT(Device, config, events);
|
ENROLL_COMPONENT(Device, config, events);
|
||||||
|
|
|
@ -198,6 +198,7 @@ namespace DinkyECS
|
||||||
return !queue.empty();
|
return !queue.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* std::optional can't do references. Don't try it! */
|
||||||
template <typename Comp>
|
template <typename Comp>
|
||||||
std::optional<Comp> get_if(DinkyECS::Entity entity) {
|
std::optional<Comp> get_if(DinkyECS::Entity entity) {
|
||||||
if(has<Comp>(entity)) {
|
if(has<Comp>(entity)) {
|
||||||
|
|
9
goap.cpp
9
goap.cpp
|
@ -46,9 +46,9 @@ namespace ai {
|
||||||
return (state | $positive_effects) & ~$negative_effects;
|
return (state | $positive_effects) & ~$negative_effects;
|
||||||
}
|
}
|
||||||
|
|
||||||
int distance_to_goal(State from, State to, Action& action) {
|
int distance_to_goal(State from, State to) {
|
||||||
auto result = from ^ to;
|
auto result = from ^ to;
|
||||||
return result.count() + action.cost;
|
return result.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
Script reconstruct_path(std::unordered_map<Action, Action>& came_from, Action& current) {
|
Script reconstruct_path(std::unordered_map<Action, Action>& came_from, Action& current) {
|
||||||
|
@ -65,11 +65,12 @@ namespace ai {
|
||||||
}
|
}
|
||||||
|
|
||||||
inline int h(State start, State goal, Action& action) {
|
inline int h(State start, State goal, Action& action) {
|
||||||
return distance_to_goal(start, goal, action);
|
(void)action; // not sure if cost goes here or on d()
|
||||||
|
return distance_to_goal(start, goal);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline int d(State start, State goal, Action& action) {
|
inline int d(State start, State goal, Action& action) {
|
||||||
return distance_to_goal(start, goal, action);
|
return distance_to_goal(start, goal) + action.cost;
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionState find_lowest(std::unordered_map<ActionState, int>& open_set) {
|
ActionState find_lowest(std::unordered_map<ActionState, int>& open_set) {
|
||||||
|
|
4
goap.hpp
4
goap.hpp
|
@ -30,6 +30,8 @@ namespace ai {
|
||||||
State $positive_effects;
|
State $positive_effects;
|
||||||
State $negative_effects;
|
State $negative_effects;
|
||||||
|
|
||||||
|
Action() {}
|
||||||
|
|
||||||
Action(std::string name, int cost) :
|
Action(std::string name, int cost) :
|
||||||
name(name), cost(cost) { }
|
name(name), cost(cost) { }
|
||||||
|
|
||||||
|
@ -68,7 +70,7 @@ namespace ai {
|
||||||
|
|
||||||
bool is_subset(State& source, State& target);
|
bool is_subset(State& source, State& target);
|
||||||
|
|
||||||
int distance_to_goal(State from, State to, Action& action);
|
int distance_to_goal(State from, State to);
|
||||||
|
|
||||||
ActionPlan plan_actions(std::vector<Action>& actions, State start, State goal);
|
ActionPlan plan_actions(std::vector<Action>& actions, State start, State goal);
|
||||||
}
|
}
|
||||||
|
|
|
@ -340,6 +340,7 @@ namespace gui {
|
||||||
}
|
}
|
||||||
|
|
||||||
void FSM::run_systems() {
|
void FSM::run_systems() {
|
||||||
|
System::enemy_ai($level);
|
||||||
System::enemy_pathing($level);
|
System::enemy_pathing($level);
|
||||||
System::collision($level);
|
System::collision($level);
|
||||||
System::motion($level);
|
System::motion($level);
|
||||||
|
|
38
systems.cpp
38
systems.cpp
|
@ -9,6 +9,8 @@
|
||||||
#include "inventory.hpp"
|
#include "inventory.hpp"
|
||||||
#include "events.hpp"
|
#include "events.hpp"
|
||||||
#include "sound.hpp"
|
#include "sound.hpp"
|
||||||
|
#include "ai.hpp"
|
||||||
|
#include "ai_debug.hpp"
|
||||||
|
|
||||||
using std::string;
|
using std::string;
|
||||||
using namespace fmt;
|
using namespace fmt;
|
||||||
|
@ -35,10 +37,26 @@ void System::lighting(GameLevel &level) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void System::enemy_ai(GameLevel &level) {
|
void System::enemy_ai(GameLevel &level) {
|
||||||
(void)level;
|
auto &world = *level.world;
|
||||||
// AI: look up Enemy::actions in ai.json
|
auto &map = *level.map;
|
||||||
// AI: setup the state
|
auto player = world.get_the<Player>();
|
||||||
// AI: process it and keep the next action in the world
|
const auto &player_position = world.get<Position>(player.entity);
|
||||||
|
map.set_target(player_position.location);
|
||||||
|
map.make_paths();
|
||||||
|
|
||||||
|
world.query<Position, EnemyConfig>([&](const auto ent, auto& pos, auto& config) {
|
||||||
|
config.ai_start = ai::load_state(config.ai_start_name);
|
||||||
|
config.ai_goal = ai::load_state(config.ai_goal_name);
|
||||||
|
|
||||||
|
ai::set(config.ai_start, "detect_enemy",
|
||||||
|
map.distance(pos.location) < config.hearing_distance);
|
||||||
|
|
||||||
|
auto a_plan = ai::plan(config.ai_script, config.ai_start, config.ai_goal);
|
||||||
|
|
||||||
|
ai::dump_script("\n\n\n-----ENEMY SCRIPT", config.ai_start, a_plan.script);
|
||||||
|
auto action = a_plan.script.front();
|
||||||
|
world.set<ai::Action>(ent, action);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void System::enemy_pathing(GameLevel &level) {
|
void System::enemy_pathing(GameLevel &level) {
|
||||||
|
@ -52,15 +70,9 @@ void System::enemy_pathing(GameLevel &level) {
|
||||||
|
|
||||||
world.query<Position, Motion>([&](auto ent, auto &position, auto &motion) {
|
world.query<Position, Motion>([&](auto ent, auto &position, auto &motion) {
|
||||||
if(ent != player.entity) {
|
if(ent != player.entity) {
|
||||||
// AI: EnemyConfig can be replaced with an AI thing
|
auto action = world.get_if<ai::Action>(ent);
|
||||||
// AI: after the enemy_ai systems are run we can then look at what
|
if(action && (*action).name == "find_enemy") {
|
||||||
// AI: their next actions is, and if it's pathing do that
|
Point out = position.location; // copy
|
||||||
|
|
||||||
dbc::check(world.has<EnemyConfig>(ent), "enemy is missing config");
|
|
||||||
const auto &config = world.get<EnemyConfig>(ent);
|
|
||||||
|
|
||||||
Point out = position.location; // copy
|
|
||||||
if(map.distance(out) < config.hearing_distance) {
|
|
||||||
map.neighbors(out, motion.random);
|
map.neighbors(out, motion.random);
|
||||||
motion = { int(out.x - position.location.x), int(out.y - position.location.y)};
|
motion = { int(out.x - position.location.x), int(out.y - position.location.y)};
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,5 +19,6 @@ namespace System {
|
||||||
void plan_motion(DinkyECS::World& world, Point move_to);
|
void plan_motion(DinkyECS::World& world, Point move_to);
|
||||||
void draw_entities(DinkyECS::World &world, Map &map, const Matrix &lights, ftxui::Canvas &canvas, const Point &cam_orig, size_t view_x, size_t view_y);
|
void draw_entities(DinkyECS::World &world, Map &map, const Matrix &lights, ftxui::Canvas &canvas, const Point &cam_orig, size_t view_x, size_t view_y);
|
||||||
|
|
||||||
|
void enemy_ai(GameLevel &level);
|
||||||
void combat(GameLevel &level);
|
void combat(GameLevel &level);
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ TEST_CASE("state and actions work", "[ai]") {
|
||||||
// start is clean but after move is dirty
|
// start is clean but after move is dirty
|
||||||
REQUIRE(move_closer.can_effect(start));
|
REQUIRE(move_closer.can_effect(start));
|
||||||
REQUIRE(!move_closer.can_effect(after_move_state));
|
REQUIRE(!move_closer.can_effect(after_move_state));
|
||||||
REQUIRE(ai::distance_to_goal(start, after_move_state, move_closer) == 11);
|
REQUIRE(ai::distance_to_goal(start, after_move_state) == 1);
|
||||||
|
|
||||||
ai::Action kill_it("kill_it", 10);
|
ai::Action kill_it("kill_it", 10);
|
||||||
kill_it.needs(ENEMY_IN_RANGE, true);
|
kill_it.needs(ENEMY_IN_RANGE, true);
|
||||||
|
@ -48,7 +48,7 @@ TEST_CASE("state and actions work", "[ai]") {
|
||||||
|
|
||||||
auto after_kill_state = kill_it.apply_effect(after_move_state);
|
auto after_kill_state = kill_it.apply_effect(after_move_state);
|
||||||
REQUIRE(!kill_it.can_effect(after_kill_state));
|
REQUIRE(!kill_it.can_effect(after_kill_state));
|
||||||
REQUIRE(ai::distance_to_goal(after_move_state, after_kill_state, kill_it) == 11);
|
REQUIRE(ai::distance_to_goal(after_move_state, after_kill_state) == 1);
|
||||||
|
|
||||||
kill_it.ignore(ENEMY_IN_RANGE);
|
kill_it.ignore(ENEMY_IN_RANGE);
|
||||||
REQUIRE(kill_it.can_effect(after_move_state));
|
REQUIRE(kill_it.can_effect(after_move_state));
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue