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 1 week 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
test: build
./builddir/runtests
./builddir/runtests "[combat-battle]"
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 "[animation-fail]"
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'

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

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

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

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

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

@ -4,15 +4,24 @@
#include "config.hpp"
#include <functional>
#include "dinkyecs.hpp"
#include <optional>
#include "components.hpp"
namespace combat {
struct BattleAction {
DinkyECS::Entity entity;
ai::EntityAI &ai;
components::Combat &combat;
};
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();
void fight(std::function<void(DinkyECS::Entity, ai::EntityAI &)> cb);
std::optional<BattleAction> next();
void dump();
};

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

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

Loading…
Cancel
Save