Initial battle engine is now integrated in the systems so now I can finally get the turn based combat to work the way I envision.

master
Zed A. Shaw 2 weeks ago
parent e18aeaf05c
commit 1f90367f51
  1. 4
      Makefile
  2. 4
      ai.cpp
  3. 1
      ai.hpp
  4. 2
      assets/config.json
  5. 1
      guecs.cpp
  6. 39
      rituals.cpp
  7. 15
      rituals.hpp
  8. 29
      systems.cpp
  9. 2
      tests/animation.cpp
  10. 56
      tests/combat.cpp

@ -22,7 +22,7 @@ tracy_build:
meson compile -j 10 -C builddir meson compile -j 10 -C builddir
test: build test: build
./builddir/runtests ./builddir/runtests "[combat-battle]"
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 "[animation-fail]" 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'

@ -174,6 +174,10 @@ namespace ai {
} }
} }
std::string& EntityAI::wants_to() {
return plan.script[0].name;
}
bool EntityAI::wants_to(std::string name) { bool EntityAI::wants_to(std::string name) {
ai::check_valid_action(name, "EntityAI::wants_to"); ai::check_valid_action(name, "EntityAI::wants_to");
return plan.script.size() > 0 && plan.script[0].name == name; return plan.script.size() > 0 && plan.script[0].name == name;

@ -23,6 +23,7 @@ namespace ai {
EntityAI() {}; EntityAI() {};
bool wants_to(std::string name); bool wants_to(std::string name);
std::string& wants_to();
void fit_sort(); void fit_sort();
bool active(); bool active();

@ -302,6 +302,6 @@
"device_probability": 10 "device_probability": 10
}, },
"graphics": { "graphics": {
"smooth_textures": true "smooth_textures": false
} }
} }

@ -25,7 +25,6 @@ namespace guecs {
text->setString(content); text->setString(content);
} }
void Sprite::init(lel::Cell &cell) { void Sprite::init(lel::Cell &cell) {
auto sprite_texture = textures::get(name); auto sprite_texture = textures::get(name);

@ -4,46 +4,41 @@
namespace combat { namespace combat {
void BattleEngine::add_enemy(DinkyECS::Entity enemy_id, ai::EntityAI& enemy) { void BattleEngine::add_enemy(BattleAction enemy) {
combatants.insert_or_assign(enemy_id, enemy); combatants.try_emplace(enemy.entity, enemy);
} }
bool BattleEngine::plan() { bool BattleEngine::plan() {
int active = 0; int active = 0;
for(auto& [entity, enemy_ai] : combatants) { for(auto& [entity, enemy] : combatants) {
enemy_ai.set_state("enemy_found", true); enemy.ai.set_state("enemy_found", true);
enemy_ai.set_state("in_combat", true); enemy.ai.set_state("in_combat", true);
enemy_ai.update(); enemy.ai.update();
active += enemy_ai.active(); active += enemy.ai.active();
// yes, copy it out of the combatants list
pending_actions.push_back(enemy);
} }
return active > 0; return active > 0;
} }
void BattleEngine::fight(std::function<void(DinkyECS::Entity, ai::EntityAI &)> cb) { std::optional<BattleAction> BattleEngine::next() {
for(auto& [entity, enemy_ai] : combatants) { if(pending_actions.size() == 0) return std::nullopt;
if(enemy_ai.wants_to("kill_enemy")) {
cb(entity, enemy_ai); auto ba = pending_actions.back();
} else if(!enemy_ai.active()) { pending_actions.pop_back();
enemy_ai.dump(); return std::make_optional(ba);
dbc::sentinel("enemy AI ended early, fix your ai.json");
} else {
dbc::log("enemy doesn't want to fight");
enemy_ai.dump();
}
}
} }
void BattleEngine::dump() { void BattleEngine::dump() {
for(auto& [entity, enemy_ai] : combatants) { for(auto& [entity, enemy] : combatants) {
fmt::println("\n\n###### ENTITY #{}", entity); fmt::println("\n\n###### ENTITY #{}", entity);
enemy_ai.dump(); enemy.ai.dump();
} }
} }
RitualEngine::RitualEngine(std::string config_path) : RitualEngine::RitualEngine(std::string config_path) :
$config(config_path) $config(config_path)
{ {

@ -4,15 +4,24 @@
#include "config.hpp" #include "config.hpp"
#include <functional> #include <functional>
#include "dinkyecs.hpp" #include "dinkyecs.hpp"
#include <optional>
#include "components.hpp"
namespace combat { namespace combat {
struct BattleAction {
DinkyECS::Entity entity;
ai::EntityAI &ai;
components::Combat &combat;
};
struct BattleEngine { struct BattleEngine {
std::unordered_map<DinkyECS::Entity, ai::EntityAI&> combatants; std::unordered_map<DinkyECS::Entity, BattleAction> combatants;
std::vector<BattleAction> pending_actions;
void add_enemy(DinkyECS::Entity enemy_id, ai::EntityAI& enemy); void add_enemy(BattleAction ba);
bool plan(); bool plan();
void fight(std::function<void(DinkyECS::Entity, ai::EntityAI &)> cb); std::optional<BattleAction> next();
void dump(); void dump();
}; };

@ -12,6 +12,7 @@
#include "ai.hpp" #include "ai.hpp"
#include "ai_debug.hpp" #include "ai_debug.hpp"
#include "shiterator.hpp" #include "shiterator.hpp"
#include "rituals.hpp"
#include <iostream> #include <iostream>
using std::string; using std::string;
@ -210,29 +211,31 @@ void System::combat(GameLevel &level) {
// this is guaranteed to not return the given position // this is guaranteed to not return the given position
auto [found, nearby] = collider.neighbors(player_position.location); auto [found, nearby] = collider.neighbors(player_position.location);
combat::BattleEngine battle;
if(found) { if(found) {
for(auto entity : nearby) { for(auto entity : nearby) {
if(world.has<ai::EntityAI>(entity)) { if(world.has<ai::EntityAI>(entity)) {
auto& enemy_ai = world.get<ai::EntityAI>(entity); auto& enemy_ai = world.get<ai::EntityAI>(entity);
auto& enemy_combat = world.get<Combat>(entity); auto& enemy_combat = world.get<Combat>(entity);
battle.add_enemy({entity, enemy_ai, enemy_combat});
}
}
Events::Combat result { battle.plan();
player_combat.attack(enemy_combat), 0 }
};
enemy_ai.set_state("enemy_found", true);
enemy_ai.set_state("in_combat", true);
enemy_ai.update();
if(enemy_ai.wants_to("kill_enemy")) { while(auto enemy = battle.next()) {
result.enemy_did = enemy_combat.attack(player_combat); Events::Combat result {
animate_entity(world, entity); player_combat.attack(enemy->combat), 0
} };
world.send<Events::GUI>(Events::GUI::COMBAT, entity, result); if(enemy->ai.wants_to("kill_enemy")) {
} result.enemy_did = enemy->combat.attack(player_combat);
animate_entity(world, enemy->entity);
} }
world.send<Events::GUI>(Events::GUI::COMBAT, enemy->entity, result);
} }
} }

@ -38,7 +38,7 @@ TEST_CASE("animation easing tests", "[animation]") {
} }
TEST_CASE("animation utility API", "[animation-fail]") { TEST_CASE("animation utility API", "[animation]") {
textures::init(); textures::init();
animation::init(); animation::init();

@ -6,8 +6,7 @@
using namespace combat; using namespace combat;
TEST_CASE("battle operations fantasy", "[combat-battle]") {
TEST_CASE("cause scared rat won't run away bug", "[combat-fail]") {
ai::reset(); ai::reset();
ai::init("assets/ai.json"); ai::init("assets/ai.json");
@ -15,41 +14,30 @@ TEST_CASE("cause scared rat won't run away bug", "[combat-fail]") {
auto ai_goal = ai::load_state("Enemy::final_state"); auto ai_goal = ai::load_state("Enemy::final_state");
BattleEngine battle; BattleEngine battle;
DinkyECS::Entity rat_id = 1; DinkyECS::Entity axe_ranger = 0;
ai::EntityAI rat("Enemy::actions", ai_start, ai_goal); ai::EntityAI axe_ai("Enemy::actions", ai_start, ai_goal);
rat.set_state("tough_personality", false); axe_ai.set_state("tough_personality", true);
rat.set_state("health_good", false); axe_ai.set_state("health_good", true);
REQUIRE(!rat.active()); components::Combat axe_combat{100, 100, 20};
battle.add_enemy(rat_id, rat); battle.add_enemy({axe_ranger, axe_ai, axe_combat});
battle.plan();
REQUIRE(rat.active());
rat.dump();
REQUIRE(rat.wants_to("run_away"));
}
TEST_CASE("battle operations fantasy", "[combat]") { DinkyECS::Entity rat = 1;
ai::reset(); ai::EntityAI rat_ai("Enemy::actions", ai_start, ai_goal);
ai::init("assets/ai.json"); rat_ai.set_state("tough_personality", false);
rat_ai.set_state("health_good", true);
components::Combat rat_combat{10, 10, 2};
battle.add_enemy({rat, rat_ai, rat_combat});
auto ai_start = ai::load_state("Enemy::initial_state"); battle.plan();
auto ai_goal = ai::load_state("Enemy::final_state");
DinkyECS::Entity enemy_id = 0; while(auto act = battle.next()) {
ai::EntityAI enemy("Enemy::actions", ai_start, ai_goal); auto& [entity, enemy_ai, combat] = *act;
enemy.set_state("tough_personality", true);
enemy.set_state("health_good", true);
BattleEngine battle; fmt::println("entity: {} wants to {} and has {} HP and {} damage",
battle.add_enemy(enemy_id, enemy); entity,
enemy_ai.wants_to(),
// responsible for running the AI and determining: combat.hp, combat.damage);
// 1. Which enemy gets to go. }
// 2. What they want to do.
battle.plan();
// Then it will go through each in order and REQUIRE(!battle.next());
// have them fight, producing the results
battle.fight([&](auto, auto& entity_ai) {
entity_ai.dump();
});
} }

Loading…
Cancel
Save