Now have the basics of the turn based battle engine with AI rebellion working.

This commit is contained in:
Zed A. Shaw 2025-12-01 00:14:08 -05:00
parent f3b20f30c5
commit c78b2ae75e
8 changed files with 114 additions and 73 deletions

View file

@ -3,34 +3,41 @@
namespace combat {
void BattleEngine::add_enemy(Combatant enemy) {
combatants.try_emplace(enemy.entity, enemy);
$combatants.try_emplace(enemy.entity, enemy);
}
void BattleEngine::player_request(const std::string& request) {
$player_requests.emplace(request);
}
bool BattleEngine::plan() {
dbc::check($player_requests.size() > 0, "Calling plan without any player reqeusts queued.");
using enum BattleHostState;
int active = 0;
for(auto& [entity, enemy] : combatants) {
for(auto& [entity, enemy] : $combatants) {
enemy.ai->update();
active += enemy.ai->active();
if(enemy.ai->active()) {
for(auto& action : enemy.ai->plan.script) {
if(enemy.ai->wants_to("kill_enemy")) {
pending_actions.emplace_back(enemy, action, BattleAction::ATTACK);
} else if(enemy.ai->wants_to("run_away")) {
pending_actions.emplace_back(enemy, action, BattleAction::ESCAPE);
} else {
pending_actions.emplace_back(enemy, action, BattleAction::OTHER);
BattleHostState host_state = not_host;
if(enemy.is_host) {
host_state = $player_requests.contains(action.name) ? agree : disagree;
}
$pending_actions.emplace_back(enemy, action.name, action.cost, host_state);
}
}
}
if(pending_actions.size() > 0) {
std::sort(pending_actions.begin(), pending_actions.end(),
if($pending_actions.size() > 0) {
std::sort($pending_actions.begin(), $pending_actions.end(),
[](const auto& a, const auto& b) -> bool
{
return a.wants_to.cost > b.wants_to.cost;
return a.cost > b.cost;
});
}
@ -38,35 +45,35 @@ namespace combat {
}
std::optional<BattleResult> BattleEngine::next() {
if(pending_actions.size() == 0) return std::nullopt;
if($pending_actions.size() == 0) return std::nullopt;
auto ba = pending_actions.back();
pending_actions.pop_back();
auto ba = $pending_actions.back();
$pending_actions.pop_back();
return std::make_optional(ba);
}
void BattleEngine::dump() {
for(auto& [entity, enemy] : combatants) {
for(auto& [entity, enemy] : $combatants) {
fmt::println("\n\n###### ENTITY #{}", entity);
enemy.ai->dump();
}
}
void BattleEngine::set(DinkyECS::Entity entity, const std::string& state, bool setting) {
dbc::check(combatants.contains(entity), "invalid combatant given to BattleEngine");
auto& action = combatants.at(entity);
dbc::check($combatants.contains(entity), "invalid combatant given to BattleEngine");
auto& action = $combatants.at(entity);
action.ai->set_state(state, setting);
}
void BattleEngine::set_all(const std::string& state, bool setting) {
for(auto& [ent, action] : combatants) {
for(auto& [ent, action] : $combatants) {
action.ai->set_state(state, setting);
}
}
Combatant& BattleEngine::get_enemy(DinkyECS::Entity entity) {
dbc::check(combatants.contains(entity), "invalid combatant given to BattleEngine");
dbc::check($combatants.contains(entity), "invalid combatant given to BattleEngine");
return combatants.at(entity);
return $combatants.at(entity);
}
}

View file

@ -4,28 +4,34 @@
#include "dinkyecs.hpp"
#include <optional>
#include "components.hpp"
#include <set>
namespace combat {
enum class BattleHostState {
not_host = 0,
agree = 1,
disagree = 2
};
struct Combatant {
DinkyECS::Entity entity = DinkyECS::NONE;
ai::EntityAI* ai = nullptr;
components::Combat* combat = nullptr;
};
enum class BattleAction {
ATTACK, BLOCK, ESCAPE, OTHER
bool is_host=false;
};
struct BattleResult {
Combatant state;
ai::Action wants_to;
BattleAction action;
std::string wants_to;
int cost;
BattleHostState host_state;
};
struct BattleEngine {
std::unordered_map<DinkyECS::Entity, Combatant> combatants;
std::vector<BattleResult> pending_actions;
std::unordered_map<DinkyECS::Entity, Combatant> $combatants;
std::vector<BattleResult> $pending_actions;
std::set<std::string> $player_requests;
void add_enemy(Combatant ba);
Combatant& get_enemy(DinkyECS::Entity entity);
@ -34,5 +40,6 @@ namespace combat {
void dump();
void set(DinkyECS::Entity entity, const std::string& state, bool setting);
void set_all(const std::string& state, bool setting);
void player_request(const std::string& request);
};
}

View file

@ -70,6 +70,8 @@ namespace boss {
$ui.status(L"PLAYER TURN");
state(State::PLAYER_TURN);
break;
case TICK:
break; // ignore tick
default:
fmt::println("BOSS_FIGHT:START unknown event {}", (int)ev);
break;
@ -98,9 +100,11 @@ namespace boss {
$ui.update_stats();
state(State::PLAYER_TURN);
} break;
case TICK:
break; // ignore tick
default:
fmt::println("BOSS_FIGHT:BOSS_TURN unknown event {}", (int)ev);
break;
// skip it
}
}
@ -124,8 +128,10 @@ namespace boss {
boss::System::combat($world, $boss_id, attack_id);
state(State::BOSS_TURN);
} break;
case TICK:
break; // ignore tick
default:
// skip it
fmt::println("BOSS_FIGHT:PLAYER_TURN unknown event {}", (int)ev);
break;
}
}

View file

@ -51,34 +51,51 @@ namespace boss {
auto& level = GameDB::current_level();
dbc::check(world->has<ai::EntityAI>(boss_id), "boss doesn't have an AI");
auto host_start = ai::load_state("Host::initial_state");
auto host_goal = ai::load_state("Host::final_state");
ai::EntityAI host_ai("Host::actions", host_start, host_goal);
auto& player_combat = world->get<Combat>(level.player);
auto& boss_combat = world->get<Combat>(boss_id);
auto& boss_ai = world->get<ai::EntityAI>(boss_id);
combat::BattleEngine battle;
battle.add_enemy({boss_id, &boss_ai, &boss_combat});
battle.add_enemy({level.player, &host_ai, &player_combat});
battle.set_all("enemy_found", true);
battle.set_all("in_combat", true);
battle.set(boss_id, "tough_personality", true);
battle.set(level.player, "tough_personality", false);
battle.set(level.player, "have_healing", false);
battle.set(level.player, "health_good", player_combat.hp > 20);
battle.player_request("kill_enemy");
battle.plan();
while(auto act = battle.next()) {
auto [enemy, ai_action, enemy_action] = *act;
auto [enemy, wants_to, cost, host_state] = *act;
Events::Combat result{};
Events::Combat result {
player_combat.attack(*enemy.combat), 0
};
switch(host_state) {
case combat::BattleHostState::agree:
result.player_did = player_combat.attack(*enemy.combat);
break;
case combat::BattleHostState::disagree:
fmt::println("HOST DISAGREES! {}", wants_to);
break;
case combat::BattleHostState::not_host:
if(wants_to == "kill_enemy") {
result.enemy_did = enemy.combat->attack(player_combat);
}
}
if(result.player_did > 0) {
auto& the_belt = world->get_the<ritual::Belt>();
dbc::check(the_belt.has(attack_id), "STOP passing invalid attack IDs to the system.");
}
if(enemy_action == combat::BattleAction::ATTACK) {
result.enemy_did = enemy.combat->attack(player_combat);
}
// need to replicate this in the boss UI
world->send<Events::GUI>(Events::GUI::COMBAT, enemy.entity, result);
}

View file

@ -72,11 +72,11 @@ namespace boss {
}
if(result.player_did > 0) {
zoom(boss_is.cell);
zoom("boss14", 1.8);
} else if(result.enemy_did > 0) {
zoom(player_is.cell);
zoom(player_is.cell, 2.0);
} else {
zoom("");
zoom("", 0.0);
}
}
@ -115,14 +115,14 @@ namespace boss {
$arena.play_animations();
}
void UI::zoom(const std::string &cell_name) {
void UI::zoom(const std::string &cell_name, double ratio) {
if(cell_name == "") {
dbc::log("!!!!!!!!! you should add this to guecs");
$camera.reset($view_texture, BOSS_VIEW_WIDTH, BOSS_VIEW_HEIGHT);
} else {
auto& cell = $arena.$ui.cell_for(cell_name);
$camera.resize(BOSS_VIEW_WIDTH/2, BOSS_VIEW_HEIGHT/2);
$camera.resize(double(BOSS_VIEW_WIDTH)/ratio, double(BOSS_VIEW_HEIGHT)/ratio);
$camera.move(float(cell.mid_x), float(cell.mid_y));
$camera.play();
}

View file

@ -38,6 +38,6 @@ namespace boss {
void animate_actor(const std::string& actor);
void update_stats();
void play_animations();
void zoom(const std::string &cell);
void zoom(const std::string &cell, double ratio);
};
}

View file

@ -256,7 +256,7 @@ void System::combat(int attack_id) {
battle.dump();
while(auto act = battle.next()) {
auto [enemy, ai_action, enemy_action] = *act;
auto [enemy, enemy_action, cost, host_state] = *act;
Events::Combat result {
player_combat.attack(*enemy.combat), 0
@ -266,7 +266,7 @@ void System::combat(int attack_id) {
spawn_attack(world, attack_id, enemy.entity);
}
if(enemy_action == combat::BattleAction::ATTACK) {
if(enemy_action == "kill_enemy") {
result.enemy_did = enemy.combat->attack(player_combat);
animation::animate_entity(world, enemy.entity);
}

View file

@ -18,11 +18,10 @@ TEST_CASE("battle operations fantasy", "[combat-battle]") {
auto host_goal = ai::load_state("Host::final_state");
BattleEngine battle;
DinkyECS::Entity host = 0;
ai::EntityAI host_ai("Host::actions", host_start, host_goal);
components::Combat host_combat{100, 100, 20};
battle.add_enemy({host, &host_ai, &host_combat});
battle.add_enemy({host, &host_ai, &host_combat, true});
DinkyECS::Entity axe_ranger = 1;
ai::EntityAI axe_ai("Enemy::actions", ai_start, ai_goal);
@ -40,39 +39,44 @@ TEST_CASE("battle operations fantasy", "[combat-battle]") {
battle.set_all("health_good", true);
battle.set(rat, "tough_personality", false);
battle.set(host, "have_healing", false);
battle.set(host, "health_good", false);
battle.set(host, "tough_personality", false);
battle.plan();
std::set<std::string> requests{
"use_healing",
"kill_enemy"
};
while(host_combat.hp > 0) {
battle.set(host, "health_good", host_combat.hp > 20);
while(auto act = battle.next()) {
auto& [enemy, wants_to, action] = *act;
battle.player_request("use_healing");
battle.player_request("kill_enemy");
fmt::println(">>>>> entity: {} wants to {} cost={} and has {} HP and {} damage",
enemy.entity, wants_to.name,
wants_to.cost, enemy.combat->hp,
enemy.combat->damage);
battle.plan();
if(enemy.entity == host) {
// negotiate between the player requested actions and the AI action
if(requests.contains(wants_to.name)) {
fmt::println("HOST and PLAYER requests match {}, doing it.",
wants_to.name);
requests.erase(wants_to.name);
} else {
fmt::println("REBELIOUS ACT: {}", wants_to.name);
while(auto act = battle.next()) {
auto& [enemy, wants_to, cost, host_behavior] = *act;
fmt::println(">>>>> entity: {} wants to {} cost={} and has {} HP and {} damage",
enemy.entity, wants_to,
cost, enemy.combat->hp,
enemy.combat->damage);
switch(host_behavior) {
case BattleHostState::agree:
fmt::println("HOST and PLAYER requests match {}, doing it.",
wants_to);
break;
case BattleHostState::disagree:
fmt::println("REBELIOUS ACT: {}", wants_to);
battle.$player_requests.clear();
break;
case BattleHostState::not_host:
if(wants_to == "kill_enemy") {
enemy.combat->attack(host_combat);
}
}
fmt::println("<<<<<<<<<<<<<<<<");
}
fmt::println("<<<<<<<<<<<<<<<<");
REQUIRE(!battle.next());
}
REQUIRE(!battle.next());
}