Now have the basics of the turn based battle engine with AI rebellion working.
This commit is contained in:
parent
f3b20f30c5
commit
c78b2ae75e
8 changed files with 114 additions and 73 deletions
47
battle.cpp
47
battle.cpp
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
23
battle.hpp
23
battle.hpp
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
10
boss/ui.cpp
10
boss/ui.cpp
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue