From 986b2612d4b3ba68eda59751e8c18c60f09c68bd Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Thu, 11 Dec 2025 13:49:53 -0500 Subject: [PATCH] [BREAKING] Battle system now runs the turn based combat better, and lots of interesting things like if you don't choose an action the host AI rebels and does it for you. --- Makefile | 2 +- ai.cpp | 2 +- battle.cpp | 50 ++++++++++++++++++++++++++++++++++++------------ battle.hpp | 9 ++++++--- boss/fight.cpp | 37 ++++++++++++++++++++++++++++------- boss/fight.hpp | 5 +++-- boss/system.cpp | 10 +++++++--- systems.cpp | 1 + tests/battle.cpp | 16 +++++++++++++--- 9 files changed, 100 insertions(+), 32 deletions(-) diff --git a/Makefile b/Makefile index 43de32a..47b9365 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ tracy_build: meson compile -j 10 -C builddir test: build - ./builddir/runtests -d yes "[combat-battle]" + ./builddir/runtests -d yes run: build test ifeq '$(OS)' 'Windows_NT' diff --git a/ai.cpp b/ai.cpp index fa72b81..b3f01e3 100644 --- a/ai.cpp +++ b/ai.cpp @@ -126,7 +126,7 @@ namespace ai { Action load_action(std::string action_name) { check(initialized, "you forgot to initialize the AI first."); - check(AIMGR.states.contains(action_name), fmt::format( + check(AIMGR.actions.contains(action_name), fmt::format( "ai::load_action({}): action does not exist in config", action_name)); return AIMGR.actions.at(action_name); diff --git a/battle.cpp b/battle.cpp index fb25d85..91ca347 100644 --- a/battle.cpp +++ b/battle.cpp @@ -4,27 +4,30 @@ namespace combat { void BattleEngine::add_enemy(Combatant enemy) { $combatants.try_emplace(enemy.entity, enemy); + + if(enemy.is_host) { + dbc::check($host_combat == nullptr, "added the host twice!"); + $host_combat = enemy.combat; + } } - void BattleEngine::player_request(const std::string& request) { - $player_requests.emplace(request); + bool BattleEngine::player_request(const std::string& request) { + auto action = ai::load_action(request); + bool can_go = player_pending_ap() >= action.cost; + + if(can_go) { + $player_requests.try_emplace(request, action); + } + + return can_go; } void BattleEngine::clear_requests() { $player_requests.clear(); } - bool BattleEngine::plan() { - dbc::check($player_requests.size() > 0, "Calling plan without any player reqeusts queued."); - using enum BattleHostState; - - int active = 0; - bool had_host = false; - + void BattleEngine::ap_refresh() { for(auto& [entity, enemy] : $combatants) { - //NOTE: this is just for asserting I'm using things right - if(enemy.is_host) had_host = true; - if(enemy.combat->ap < enemy.combat->max_ap) { int new_ap = std::min(enemy.combat->max_ap, enemy.combat->ap_delta + enemy.combat->ap); @@ -34,6 +37,29 @@ namespace combat { enemy.combat->ap = new_ap; } + } + } + + int BattleEngine::player_pending_ap() { + dbc::check($host_combat != nullptr, "didn't set host before checking AP"); + int pending_ap = $host_combat->ap; + + for(auto& [name, action] : $player_requests) { + pending_ap -= action.cost; + } + + return pending_ap; + } + + bool BattleEngine::plan() { + using enum BattleHostState; + + int active = 0; + bool had_host = false; + + for(auto& [entity, enemy] : $combatants) { + //NOTE: this is just for asserting I'm using things right + if(enemy.is_host) had_host = true; enemy.ai->update(); active += enemy.ai->active(); diff --git a/battle.hpp b/battle.hpp index 376c09c..90b4417 100644 --- a/battle.hpp +++ b/battle.hpp @@ -4,7 +4,7 @@ #include "dinkyecs.hpp" #include #include "components.hpp" -#include +#include namespace combat { @@ -32,7 +32,8 @@ namespace combat { struct BattleEngine { std::unordered_map $combatants; std::vector $pending_actions; - std::set $player_requests; + std::unordered_map $player_requests; + components::Combat* $host_combat = nullptr; void add_enemy(Combatant ba); Combatant& get_enemy(DinkyECS::Entity entity); @@ -41,7 +42,9 @@ 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); + bool player_request(const std::string& request); + int player_pending_ap(); void clear_requests(); + void ap_refresh(); }; } diff --git a/boss/fight.cpp b/boss/fight.cpp index 26f56ce..9f10cfc 100644 --- a/boss/fight.cpp +++ b/boss/fight.cpp @@ -1,4 +1,3 @@ -#define FSM_DEBUG 1 #include "boss/fight.hpp" #include "boss/system.hpp" #include "animation.hpp" @@ -9,7 +8,8 @@ namespace boss { $boss_id(boss_id), $battle(System::create_battle($world, $boss_id)), $ui(world, boss_id, player_id), - $player_id(player_id) + $host(player_id), + $host_combat($world->get(player_id)) { $ui.init(); } @@ -31,8 +31,7 @@ namespace boss { return in_state(State::END); } - void Fight::START(gui::Event ev, std::any data) { - (void)data; + void Fight::START(gui::Event ev, std::any) { using enum gui::Event; switch(ev) { @@ -42,6 +41,7 @@ namespace boss { break; case TICK: $ui.status(L"PLAYER REQUESTS"); + $battle.ap_refresh(); state(State::PLAYER_REQUESTS); break; default: @@ -58,8 +58,18 @@ namespace boss { state(State::END); break; case START_COMBAT: + System::plan_battle($battle, $world, $boss_id); $ui.status(L"PLANNING BATTLE"); state(State::PLAN_BATTLE); + break; + case ATTACK: + if($battle.player_request("kill_enemy")) { + fmt::println("player requests kill_enemy {} vs. {}", + $host_combat.ap, $battle.player_pending_ap()); + } else { + fmt::println("NO MORE ACTION!"); + } + break; case TICK: break; // ignore tick default: @@ -77,6 +87,10 @@ namespace boss { break; case START_COMBAT: $ui.status(L"EXEC PLAN"); + while(auto action = $battle.next()) { + System::combat(*action, $world, $boss_id, 0); + run_systems(); + } state(State::EXEC_PLAN); break; case TICK: @@ -100,6 +114,8 @@ namespace boss { state(State::END); } else { $ui.status(L"PLAYER REQUESTS"); + $battle.ap_refresh(); + $battle.clear_requests(); state(State::PLAYER_REQUESTS); } break; // ignore tick @@ -113,7 +129,10 @@ namespace boss { } void Fight::run_systems() { - run++; + $ui.update_stats(); + $battle.set($host, "tough_personality", false); + $battle.set($host, "have_healing", false); + $battle.set($host, "health_good", $host_combat.hp > 20); } void Fight::render(sf::RenderWindow& window) { @@ -150,7 +169,11 @@ namespace boss { } bool Fight::player_dead() { - auto& combat = $world->get($player_id); - return combat.hp <= 0; + return $host_combat.hp <= 0; + } + + void Fight::init_fight() { + System::initialize_actor_ai(*$world, $boss_id); + run_systems(); } } diff --git a/boss/fight.hpp b/boss/fight.hpp index 4064c96..b9867a0 100644 --- a/boss/fight.hpp +++ b/boss/fight.hpp @@ -27,8 +27,8 @@ namespace boss { combat::BattleEngine $battle; boss::UI $ui; sf::Vector2f mouse_pos{0,0}; - Entity $player_id = NONE; - int run = 0; + Entity $host = NONE; + components::Combat& $host_combat; Fight(shared_ptr world, Entity boss_id, Entity player_id); @@ -44,5 +44,6 @@ namespace boss { void run_systems(); bool player_dead(); + void init_fight(); }; } diff --git a/boss/system.cpp b/boss/system.cpp index 0b1da2b..650d5bb 100644 --- a/boss/system.cpp +++ b/boss/system.cpp @@ -20,8 +20,10 @@ namespace boss { auto ai_goal = ai::load_state(config.ai_goal_name); ai::EntityAI boss_ai(config.ai_script, ai_start, ai_goal); + boss_ai.set_state("enemy_found", true); + boss_ai.set_state("in_combat", true); boss_ai.set_state("tough_personality", true); - boss_ai.set_state("detect_enemy", true); + boss_ai.set_state("health_good", true); world.set(entity_id, boss_ai); } @@ -97,15 +99,17 @@ namespace boss { switch(host_state) { case BattleHostState::agree: - dbc::log("host_agrees"); // BUG: this is hard coding only one boss, how to select targets? if(wants_to == "kill_enemy") { result.player_did = player_combat.attack(boss_combat); } break; case BattleHostState::disagree: - dbc::log("host_DISagrees"); fmt::println("HOST DISAGREES! {}", wants_to); + + if(wants_to == "kill_enemy") { + result.player_did = player_combat.attack(boss_combat); + } break; case BattleHostState::not_host: dbc::log("kill_enemy"); diff --git a/systems.cpp b/systems.cpp index 7b7ff34..6c45aee 100644 --- a/systems.cpp +++ b/systems.cpp @@ -253,6 +253,7 @@ void System::combat(int attack_id) { battle.set_all("enemy_found", true); battle.set_all("in_combat", true); battle.player_request("kill_enemy"); + battle.ap_refresh(); battle.plan(); } diff --git a/tests/battle.cpp b/tests/battle.cpp index c2f4270..b6572b8 100644 --- a/tests/battle.cpp +++ b/tests/battle.cpp @@ -59,6 +59,7 @@ TEST_CASE("battle operations fantasy", "[combat-battle]") { battle.player_request("use_healing"); battle.player_request("kill_enemy"); + battle.ap_refresh(); battle.plan(); while(auto act = battle.next()) { @@ -112,11 +113,18 @@ TEST_CASE("boss/systems.cpp works", "[combat-battle]") { System::initialize_actor_ai(*fight->$world, fight->$boss_id); - battle.set(host, "tough_personality", false); - battle.set(host, "have_healing", false); - battle.set(host, "health_good", host_combat.hp > 20); + // NEED UPDATE STATE + 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.ap_refresh(); battle.player_request("kill_enemy"); + int pending_ap = battle.player_pending_ap(); + + REQUIRE(pending_ap < host_combat.ap); System::plan_battle(battle, fight->$world, fight->$boss_id); @@ -124,4 +132,6 @@ TEST_CASE("boss/systems.cpp works", "[combat-battle]") { dbc::log("ACTION!"); System::combat(*action, fight->$world, fight->$boss_id, 0); } + + REQUIRE(host_combat.ap == pending_ap); }