Can now run the full AI for all combatants and then sort by the action costs to make the action queue.

This commit is contained in:
Zed A. Shaw 2025-11-27 12:46:14 -05:00
parent d244106981
commit b48df3f4db
10 changed files with 104 additions and 59 deletions

View file

@ -36,8 +36,8 @@ tracy_build:
meson setup --wipe builddir --buildtype debugoptimized -Dtracy_enable=true -Dtracy:on_demand=true
meson compile -j 10 -C builddir
test:
./builddir/runtests -d yes
test: build
./builddir/runtests -d yes "[combat-battle]"
run: build test
ifeq '$(OS)' 'Windows_NT'
@ -60,7 +60,7 @@ clean:
meson compile --clean -C builddir
debug_test: build
gdb --nx -x .gdbinit --ex run --ex bt --ex q --args builddir/runtests -e
gdb --nx -x .gdbinit --ex run --ex bt --ex q --args builddir/runtests -e "[combat-battle]"
win_installer:
powershell 'start "C:\Program Files (x86)\solicus\InstallForge\bin\ifbuilderenvx86.exe" scripts\win_installer.ifp'

View file

@ -82,7 +82,6 @@
"name": "use_healing",
"cost": 1,
"needs": {
"have_item": true,
"have_healing": true,
"health_good": false
},
@ -133,6 +132,7 @@
"kill_enemy",
"collect_items",
"find_healing",
"run_away",
"use_healing"],
"Enemy::actions":
["find_enemy", "run_away", "kill_enemy", "use_healing"]

View file

@ -10,18 +10,30 @@ namespace combat {
int active = 0;
for(auto& [entity, enemy] : combatants) {
enemy.ai.update();
active += enemy.ai.active();
enemy.ai->update();
active += enemy.ai->active();
if(enemy.ai.active()) {
if(enemy.ai.wants_to("kill_enemy")) {
pending_actions.emplace_back(enemy, BattleAction::ATTACK);
} else if(enemy.ai.wants_to("run_away")) {
pending_actions.emplace_back(enemy, BattleAction::ESCAPE);
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);
}
}
}
}
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 active > 0;
}
@ -36,25 +48,25 @@ namespace combat {
void BattleEngine::dump() {
for(auto& [entity, enemy] : combatants) {
fmt::println("\n\n###### ENTITY #{}", entity);
enemy.ai.dump();
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);
action.ai.set_state(state, setting);
action.ai->set_state(state, setting);
}
void BattleEngine::set_all(const std::string& state, bool setting) {
for(auto& [ent, action] : combatants) {
action.ai.set_state(state, setting);
action.ai->set_state(state, setting);
}
}
void BattleEngine::queue(DinkyECS::Entity entity, BattleAction action) {
Combatant& BattleEngine::get_enemy(DinkyECS::Entity entity) {
dbc::check(combatants.contains(entity), "invalid combatant given to BattleEngine");
auto& enemy = combatants.at(entity);
pending_actions.emplace_back(enemy, action);
return combatants.at(entity);
}
}

View file

@ -8,17 +8,18 @@
namespace combat {
struct Combatant {
DinkyECS::Entity entity;
ai::EntityAI &ai;
components::Combat &combat;
DinkyECS::Entity entity = DinkyECS::NONE;
ai::EntityAI* ai = nullptr;
components::Combat* combat = nullptr;
};
enum class BattleAction {
ATTACK, BLOCK, ESCAPE
ATTACK, BLOCK, ESCAPE, OTHER
};
struct BattleResult {
Combatant &state;
Combatant state;
ai::Action wants_to;
BattleAction action;
};
@ -27,11 +28,11 @@ namespace combat {
std::vector<BattleResult> pending_actions;
void add_enemy(Combatant ba);
Combatant& get_enemy(DinkyECS::Entity entity);
bool plan();
std::optional<BattleResult> next();
void dump();
void set(DinkyECS::Entity entity, const std::string& state, bool setting);
void set_all(const std::string& state, bool setting);
void queue(DinkyECS::Entity entity, BattleAction action);
};
}

View file

@ -33,11 +33,10 @@ namespace boss {
switch(ev) {
case MOUSE_CLICK: {
$ui.mouse(mouse_pos.x, mouse_pos.y, guecs::NO_MODS);
} break;
} break;
case MOUSE_MOVE: {
$ui.mouse(mouse_pos.x, mouse_pos.y, {1 << guecs::ModBit::hover});
}
break;
$ui.mouse(mouse_pos.x, mouse_pos.y, {1 << guecs::ModBit::hover});
} break;
case MOUSE_DRAG:
dbc::log("mouse drag");
break;
@ -64,12 +63,15 @@ namespace boss {
case BOSS_START:
state(State::END);
break;
case KEY_PRESS:
fmt::println("KEY_PRESS");
break;
case ATTACK:
$ui.status(L"PLAYER TURN");
state(State::PLAYER_TURN);
break;
default:
// fmt::println("BOSS_FIGHT unknown event {}", (int)ev);
fmt::println("BOSS_FIGHT:START unknown event {}", (int)ev);
break;
}
}
@ -82,11 +84,16 @@ namespace boss {
case BOSS_START:
state(State::END);
break;
case KEY_PRESS:
fmt::println("KEY_PRESS");
break;
case ATTACK: {
int attack_id = std::any_cast<int>(data);
fmt::println("Player attack: {}", attack_id);
$ui.status(L"PLAYER TURN");
const std::string& player_pos = run % 10 < 5 ? "player1" : "player2";
$ui.move_actor("player", player_pos);
int attack_id = std::any_cast<int>(data);
boss::System::combat($world, $boss_id, attack_id);
$ui.update_stats();
state(State::PLAYER_TURN);
@ -105,6 +112,9 @@ namespace boss {
case BOSS_START:
state(State::END);
break;
case KEY_PRESS:
fmt::println("KEY_PRESS");
break;
case ATTACK: {
$ui.status(L"BOSS TURN");
const std::string &boss_at = run % 10 < 5 ? "boss5" : "boss6";
@ -120,10 +130,8 @@ namespace boss {
}
}
void Fight::END(gui::Event ev, std::any data) {
// We need to clean up that world I think, but not sure how
(void)ev;
(void)data;
void Fight::END(gui::Event ev, std::any) {
fmt::println("BOSS_FIGHT:END event {}", (int)ev);
}
void Fight::run_systems() {

View file

@ -56,17 +56,17 @@ namespace boss {
auto& boss_ai = world->get<ai::EntityAI>(boss_id);
combat::BattleEngine battle;
battle.add_enemy({boss_id, boss_ai, boss_combat});
battle.add_enemy({boss_id, &boss_ai, &boss_combat});
battle.set_all("enemy_found", true);
battle.set_all("in_combat", true);
battle.plan();
while(auto act = battle.next()) {
auto [enemy, enemy_action] = *act;
auto [enemy, ai_action, enemy_action] = *act;
Events::Combat result {
player_combat.attack(enemy.combat), 0
player_combat.attack(*enemy.combat), 0
};
if(result.player_did > 0) {
@ -76,7 +76,7 @@ namespace boss {
}
if(enemy_action == combat::BattleAction::ATTACK) {
result.enemy_did = enemy.combat.attack(player_combat);
result.enemy_did = enemy.combat->attack(player_combat);
}
// need to replicate this in the boss UI

View file

@ -8,7 +8,11 @@ project('raycaster', 'cpp',
])
# use this for common options only for our executables
cpp_args=[]
cpp_args=[
'-Wno-unused-parameter',
'-Wno-unused-function',
'-Wno-unused-variable',
]
link_args=[]
# these are passed as override_defaults
exe_defaults = [ 'warning_level=2' ]

View file

@ -244,7 +244,7 @@ void System::combat(int attack_id) {
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});
battle.add_enemy({entity, &enemy_ai, &enemy_combat});
}
}
@ -256,10 +256,10 @@ void System::combat(int attack_id) {
battle.dump();
while(auto act = battle.next()) {
auto [enemy, enemy_action] = *act;
auto [enemy, ai_action, enemy_action] = *act;
Events::Combat result {
player_combat.attack(enemy.combat), 0
player_combat.attack(*enemy.combat), 0
};
if(result.player_did > 0) {
@ -267,7 +267,7 @@ void System::combat(int attack_id) {
}
if(enemy_action == combat::BattleAction::ATTACK) {
result.enemy_did = enemy.combat.attack(player_combat);
result.enemy_did = enemy.combat->attack(player_combat);
animation::animate_entity(world, enemy.entity);
}

View file

@ -1,5 +1,6 @@
#include <catch2/catch_test_macros.hpp>
#include <iostream>
#include <set>
#include "rituals.hpp"
#include "battle.hpp"
#include "simplefsm.hpp"
@ -17,41 +18,60 @@ TEST_CASE("battle operations fantasy", "[combat-battle]") {
auto host_goal = ai::load_state("Host::final_state");
BattleEngine battle;
DinkyECS::Entity player = 0;
ai::EntityAI player_ai("Host::actions", host_start, host_goal);
components::Combat player_combat{100, 100, 20};
battle.add_enemy({player, player_ai, player_combat});
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});
DinkyECS::Entity axe_ranger = 1;
ai::EntityAI axe_ai("Enemy::actions", ai_start, ai_goal);
components::Combat axe_combat{100, 100, 20};
battle.add_enemy({axe_ranger, axe_ai, axe_combat});
battle.add_enemy({axe_ranger, &axe_ai, &axe_combat});
DinkyECS::Entity rat = 2;
ai::EntityAI rat_ai("Enemy::actions", ai_start, ai_goal);
components::Combat rat_combat{10, 10, 2};
battle.add_enemy({rat, rat_ai, rat_combat});
battle.add_enemy({rat, &rat_ai, &rat_combat});
battle.set_all("enemy_found", true);
battle.set_all("in_combat", true);
battle.set_all("tough_personality", true);
battle.set_all("health_good", true);
battle.set(rat, "tough_personality", false);
battle.queue(player, BattleAction::ATTACK);
battle.queue(player, BattleAction::BLOCK);
battle.queue(player, BattleAction::ESCAPE);
battle.set(host, "have_healing", false);
battle.set(host, "health_good", false);
battle.set(host, "tough_personality", false);
battle.plan();
while(auto act = battle.next()) {
auto& [enemy, action] = *act;
std::set<std::string> requests{
"use_healing",
"kill_enemy"
};
fmt::println("entity: {} wants to {} action={} and has {} HP and {} damage",
enemy.entity, enemy.ai.wants_to(),
int(action), enemy.combat.hp,
enemy.combat.damage);
while(auto act = battle.next()) {
auto& [enemy, wants_to, action] = *act;
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);
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);
}
}
fmt::println("<<<<<<<<<<<<<<<<");
}
REQUIRE(!battle.next());

View file

@ -33,6 +33,7 @@ int main(int, char*[]) {
sf::RenderWindow window(sf::VideoMode({SCREEN_WIDTH, SCREEN_HEIGHT}), "Bossfight Testing Arena");
window.setVerticalSyncEnabled(VSYNC);
if(FRAME_LIMIT) window.setFramerateLimit(FRAME_LIMIT);
window.setPosition({0,0});
@ -63,7 +64,6 @@ int main(int, char*[]) {
while(world->has_event<Events::GUI>()) {
auto [evt, entity, data] = world->recv<Events::GUI>();
// FIX YOUR DAMN EVENTS
switch(evt) {
case Events::GUI::ATTACK:
main->event(gui::Event::ATTACK, data);