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.

master
Zed A. Shaw 2 weeks ago
parent 77f2e94515
commit ad71631809
  1. 4
      Makefile
  2. 1
      ai.cpp
  3. 9
      assets/ai.json
  4. 8
      assets/enemies.json
  5. 2
      autowalker.cpp
  6. 9
      components.hpp
  7. 1
      dinkyecs.hpp
  8. 9
      goap.cpp
  9. 4
      goap.hpp
  10. 1
      gui_fsm.cpp
  11. 38
      systems.cpp
  12. 1
      systems.hpp
  13. 4
      tests/ai.cpp

@ -22,7 +22,7 @@ tracy_build:
meson compile -j 10 -C builddir
test: build
./builddir/runtests "[ai]"
./builddir/runtests
run: build test
powershell "cp ./builddir/zedcaster.exe ."
@ -41,7 +41,7 @@ clean:
meson compile --clean -C builddir
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:
powershell 'start "C:\Program Files (x86)\solicus\InstallForge\bin\ifbuilderenvx86.exe" win_installer.ifp'

@ -18,6 +18,7 @@ namespace ai {
Action config_action(AIProfile& profile, nlohmann::json& config) {
check(config.contains("name"), "config_action: action config missing name");
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"]);

@ -7,13 +7,15 @@
"no_more_enemies": 4,
"in_combat": 5,
"have_item": 6,
"have_healing": 7
"have_healing": 7,
"detect_enemy": 8
},
"actions": [
{
"name": "find_enemy",
"cost": 5,
"needs": {
"detect_enemy": true,
"in_combat": false,
"no_more_enemies": false,
"enemy_found": false
@ -68,7 +70,8 @@
"no_more_enemies": false,
"in_combat": false,
"have_item": false,
"have_healing": false
"have_healing": false,
"detect_enemy": true
},
"Walker::final_state": {
"enemy_found": true,
@ -79,12 +82,14 @@
"no_more_enemies": true
},
"Enemy::initial_state": {
"detect_enemy": false,
"enemy_found": false,
"enemy_dead": false,
"health_good": true,
"in_combat": false
},
"Enemy::final_state": {
"detect_enemy": true,
"enemy_found": true,
"enemy_dead": true,
"health_good": true

@ -19,7 +19,7 @@
},
{"_type": "Combat", "hp": 20, "max_hp": 20, "damage": 1, "dead": 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": "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"}
@ -33,7 +33,7 @@
},
{"_type": "Combat", "hp": 40, "max_hp": 40, "damage": 10, "dead": false},
{"_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": "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"}
@ -47,7 +47,7 @@
},
{"_type": "Combat", "hp": 20, "max_hp": 20, "damage": 20, "dead": 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": "Sprite", "name": "rat_with_sword", "width": 256, "height": 256, "scale": 1.0},
{"_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": "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": "Sprite", "name": "hairy_spider", "width": 256, "height": 256, "scale": 1.0},
{"_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) {
start = update_state(start);
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();
if(action.name == "find_enemy") {

@ -9,6 +9,7 @@
#include <optional>
#include "easings.hpp"
#include "json_mods.hpp"
#include "goap.hpp"
namespace components {
@ -45,6 +46,11 @@ namespace components {
struct EnemyConfig {
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 {
@ -138,7 +144,8 @@ namespace components {
ENROLL_COMPONENT(Weapon, damage);
ENROLL_COMPONENT(Loot, amount);
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(Combat, hp, max_hp, damage, dead);
ENROLL_COMPONENT(Device, config, events);

@ -198,6 +198,7 @@ namespace DinkyECS
return !queue.empty();
}
/* std::optional can't do references. Don't try it! */
template <typename Comp>
std::optional<Comp> get_if(DinkyECS::Entity entity) {
if(has<Comp>(entity)) {

@ -46,9 +46,9 @@ namespace ai {
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;
return result.count() + action.cost;
return result.count();
}
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) {
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) {
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) {

@ -30,6 +30,8 @@ namespace ai {
State $positive_effects;
State $negative_effects;
Action() {}
Action(std::string name, int cost) :
name(name), cost(cost) { }
@ -68,7 +70,7 @@ namespace ai {
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);
}

@ -340,6 +340,7 @@ namespace gui {
}
void FSM::run_systems() {
System::enemy_ai($level);
System::enemy_pathing($level);
System::collision($level);
System::motion($level);

@ -9,6 +9,8 @@
#include "inventory.hpp"
#include "events.hpp"
#include "sound.hpp"
#include "ai.hpp"
#include "ai_debug.hpp"
using std::string;
using namespace fmt;
@ -35,10 +37,26 @@ void System::lighting(GameLevel &level) {
}
void System::enemy_ai(GameLevel &level) {
(void)level;
// AI: look up Enemy::actions in ai.json
// AI: setup the state
// AI: process it and keep the next action in the world
auto &world = *level.world;
auto &map = *level.map;
auto player = world.get_the<Player>();
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) {
@ -52,15 +70,9 @@ void System::enemy_pathing(GameLevel &level) {
world.query<Position, Motion>([&](auto ent, auto &position, auto &motion) {
if(ent != player.entity) {
// AI: EnemyConfig can be replaced with an AI thing
// AI: after the enemy_ai systems are run we can then look at what
// AI: their next actions is, and if it's pathing do that
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) {
auto action = world.get_if<ai::Action>(ent);
if(action && (*action).name == "find_enemy") {
Point out = position.location; // copy
map.neighbors(out, motion.random);
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 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);
}

@ -36,7 +36,7 @@ TEST_CASE("state and actions work", "[ai]") {
// start is clean but after move is dirty
REQUIRE(move_closer.can_effect(start));
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);
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);
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);
REQUIRE(kill_it.can_effect(after_move_state));

Loading…
Cancel
Save