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
|
||||
|
||||
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'
|
||||
|
|
1
ai.cpp
1
ai.cpp
|
@ -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)) {
|
||||
|
|
9
goap.cpp
9
goap.cpp
|
@ -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) {
|
||||
|
|
4
goap.hpp
4
goap.hpp
|
@ -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);
|
||||
|
|
38
systems.cpp
38
systems.cpp
|
@ -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…
Add table
Add a link
Reference in a new issue