diff --git a/assets/enemies.json b/assets/enemies.json index f09909d..9efbb78 100644 --- a/assets/enemies.json +++ b/assets/enemies.json @@ -9,6 +9,7 @@ {"_type": "Combat", "hp": 200, "max_hp": 200, "ap": 0, "max_ap": 12, "ap_delta": 6, "damage": 10, "dead": false}, {"_type": "Motion", "dx": 0, "dy": 0, "random": false}, {"_type": "Collision", "has": true}, + {"_type": "EnemyConfig", "ai_script": "Host::actions", "ai_start_name": "Host::initial_state", "ai_goal_name": "Host::final_state"}, {"_type": "LightSource", "strength": 35, "radius": 2.0} ] }, diff --git a/battle.cpp b/battle.cpp index b851b8b..5dcd599 100644 --- a/battle.cpp +++ b/battle.cpp @@ -15,12 +15,18 @@ namespace combat { using enum BattleHostState; int active = 0; + bool had_host = false; fmt::println("---------- start combatants"); for(auto& [entity, enemy] : $combatants) { if(enemy.combat->ap < enemy.combat->max_ap) { + int new_ap = std::min(enemy.combat->max_ap, enemy.combat->ap_delta + enemy.combat->ap); + // only add up to the max - enemy.combat->ap = std::min(enemy.combat->max_ap, enemy.combat->ap_delta + enemy.combat->ap); + fmt::println("enemy {} get more ap {}->{}", + entity, enemy.combat->ap, new_ap); + + enemy.combat->ap = new_ap; } fmt::println("--- enemy {} has {} ap", entity, enemy.combat->ap); @@ -32,6 +38,8 @@ namespace combat { if(enemy.ai->active()) { for(auto& action : enemy.ai->plan.script) { BattleHostState host_state = not_host; + //NOTE: this is just for asserting I'm using things right + if(enemy.is_host) had_host = true; if(action.cost > enemy.combat->ap) { host_state = out_of_ap; @@ -58,12 +66,13 @@ namespace combat { $pending_actions.emplace_back(enemy, action.name, action.cost, host_state); } + dbc::check(had_host, "FAIL, you forgot to set enemy.is_host=true for one entity"); dbc::check(enemy.combat->ap >= 0, "enemy's AP went below 0"); dbc::check(enemy.combat->ap <= enemy.combat->max_ap, "enemy's AP went above max"); } } - fmt::print("<---- end of enemy setup, sorting"); + fmt::println("<---- end of enemy setup, sorting"); if($pending_actions.size() > 0) { std::sort($pending_actions.begin(), $pending_actions.end(), diff --git a/battle.hpp b/battle.hpp index 779aebc..3062376 100644 --- a/battle.hpp +++ b/battle.hpp @@ -23,7 +23,7 @@ namespace combat { }; struct BattleResult { - Combatant state; + Combatant enemy; std::string wants_to; int cost; BattleHostState host_state; diff --git a/boss/fight.cpp b/boss/fight.cpp index 7779a2a..c068469 100644 --- a/boss/fight.cpp +++ b/boss/fight.cpp @@ -6,6 +6,7 @@ namespace boss { Fight::Fight(shared_ptr world, Entity boss_id, Entity player_id) : $world(world), $boss_id(boss_id), + $battle(System::create_battle($world, $boss_id)), $ui(world, boss_id, player_id) { $ui.init(); @@ -96,7 +97,14 @@ namespace boss { $ui.status(L"PLAYER TURN"); const std::string& player_pos = run % 10 < 5 ? "player1" : "player2"; $ui.move_actor("player", player_pos); - boss::System::combat($world, $boss_id, attack_id); + boss::System::plan_battle($battle, $world, $boss_id); + + while(auto action = $battle.next()) { + fmt::println("*** combat turn run: eid={}", + action->enemy.entity); + boss::System::combat(*action, $world, $boss_id, attack_id); + } + $ui.update_stats(); state(State::PLAYER_TURN); } break; @@ -125,7 +133,6 @@ namespace boss { $ui.move_actor("boss", boss_at); $ui.animate_actor("boss"); int attack_id = std::any_cast(data); - boss::System::combat($world, $boss_id, attack_id); state(State::BOSS_TURN); } break; case TICK: diff --git a/boss/fight.hpp b/boss/fight.hpp index 4efb0c4..5931906 100644 --- a/boss/fight.hpp +++ b/boss/fight.hpp @@ -4,6 +4,7 @@ #include "dinkyecs.hpp" #include "boss/ui.hpp" #include "gui/fsm_events.hpp" +#include "battle.hpp" #include #include @@ -22,6 +23,7 @@ namespace boss { public: shared_ptr $world = nullptr; DinkyECS::Entity $boss_id = NONE; + combat::BattleEngine $battle; boss::UI $ui; sf::Vector2f mouse_pos{0,0}; int run = 0; diff --git a/boss/system.cpp b/boss/system.cpp index 6e4be5d..a898a9c 100644 --- a/boss/system.cpp +++ b/boss/system.cpp @@ -7,14 +7,15 @@ namespace boss { using namespace components; + using namespace combat; void System::load_config() { fmt::println("load it"); } - void System::initialize_boss_ai(DinkyECS::World& world, DinkyECS::Entity boss_id) { - dbc::check(world.has(boss_id), "boss doesn't have an AI EnemyConfig"); - auto& config = world.get(boss_id); + void System::initialize_actor_ai(DinkyECS::World& world, DinkyECS::Entity entity_id) { + dbc::check(world.has(entity_id), "boss doesn't have an AI EnemyConfig"); + auto& config = world.get(entity_id); auto ai_start = ai::load_state(config.ai_start_name); auto ai_goal = ai::load_state(config.ai_goal_name); @@ -22,7 +23,7 @@ namespace boss { boss_ai.set_state("tough_personality", true); boss_ai.set_state("detect_enemy", true); - world.set(boss_id, boss_ai); + world.set(entity_id, boss_ai); } shared_ptr System::create_bossfight() { @@ -39,69 +40,89 @@ namespace boss { auto boss_id = world->entity(); components::configure_entity(*world, boss_id, boss_data["components"]); - initialize_boss_ai(*world, boss_id); - + initialize_actor_ai(*world, boss_id); dbc::check(world->has(boss_id), "boss doesn't have an AI"); + initialize_actor_ai(*world, level.player); + dbc::check(world->has(level.player), "player/host doesn't have an AI"); + return make_shared(world, boss_id, level.player); } - void System::combat(std::shared_ptr world, DinkyECS::Entity boss_id, int attack_id) { - // get the player from the previous level, but should I just make the boss fights a level? + BattleEngine System::create_battle(std::shared_ptr world, DinkyECS::Entity boss_id) { auto& level = GameDB::current_level(); - dbc::check(world->has(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_if(level.player); + dbc::check(player_combat != nullptr, "No Combat for player."); - auto& player_combat = world->get(level.player); - auto& boss_combat = world->get(boss_id); - auto& boss_ai = world->get(boss_id); + auto boss_combat = world->get_if(boss_id); + dbc::check(boss_combat != nullptr, "No Combat for Boss."); - combat::BattleEngine battle; - battle.add_enemy({boss_id, &boss_ai, &boss_combat}); - battle.add_enemy({level.player, &host_ai, &player_combat}); + // BUG: should I reset AP here? + player_combat->ap = player_combat->max_ap; + boss_combat->ap = boss_combat->max_ap; + + auto boss_ai = world->get_if(boss_id); + dbc::check(boss_ai != nullptr, "boss doesn't have an AI"); + + auto host_ai = world->get_if(boss_id); + dbc::check(host_ai != nullptr, "host doesn't have an AI"); + + BattleEngine battle; + battle.add_enemy({boss_id, boss_ai, boss_combat, false}); + battle.add_enemy({level.player, host_ai, player_combat, true}); battle.set_all("enemy_found", true); battle.set_all("in_combat", true); battle.set(boss_id, "tough_personality", true); + + return battle; + } + + void System::plan_battle(BattleEngine& battle, std::shared_ptr world, DinkyECS::Entity boss_id) { + // REFACTOR: make this loop the list of entities in the battle then + // use their world state to configure the plan + auto& level = GameDB::current_level(); + auto& player_combat = world->get(level.player); + 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, wants_to, cost, host_state] = *act; - Events::Combat result{}; + void System::combat(BattleResult& action, std::shared_ptr world, DinkyECS::Entity boss_id, int attack_id) { - 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); - } - break; - case combat::BattleHostState::out_of_ap: - fmt::println("OUT OF AP {}", wants_to); - break; - } + auto& level = GameDB::current_level(); + auto& player_combat = world->get(level.player); + auto& boss_combat = world->get(boss_id); - if(result.player_did > 0) { - auto& the_belt = world->get_the(); - dbc::check(the_belt.has(attack_id), "STOP passing invalid attack IDs to the system."); - } + auto& [enemy, wants_to, cost, host_state] = action; - // need to replicate this in the boss UI - world->send(Events::GUI::COMBAT, enemy.entity, result); + Events::Combat result{}; + + switch(host_state) { + case BattleHostState::agree: + // 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: + fmt::println("HOST DISAGREES! {}", wants_to); + break; + case BattleHostState::not_host: + if(wants_to == "kill_enemy") { + result.enemy_did = enemy.combat->attack(player_combat); + } + break; + case BattleHostState::out_of_ap: + fmt::println("OUT OF AP {}", wants_to); + break; } + + world->send(Events::GUI::COMBAT, enemy.entity, result); } } diff --git a/boss/system.hpp b/boss/system.hpp index cea496f..fad3ae5 100644 --- a/boss/system.hpp +++ b/boss/system.hpp @@ -8,6 +8,13 @@ namespace boss { void load_config(); std::shared_ptr create_bossfight(); void combat(std::shared_ptr world, DinkyECS::Entity boss_id, int attack_id); - void initialize_boss_ai(DinkyECS::World& world, DinkyECS::Entity boss_id); + + void initialize_actor_ai(DinkyECS::World& world, DinkyECS::Entity boss_id); + combat::BattleEngine create_battle(std::shared_ptr world, DinkyECS::Entity boss_id); + + void plan_battle(combat::BattleEngine& battle, std::shared_ptr world, DinkyECS::Entity boss_id); + + void combat(combat::BattleResult& action, std::shared_ptr world, DinkyECS::Entity boss_id, int attack_id); + } } diff --git a/boss/ui.cpp b/boss/ui.cpp index 1bed3bb..f96272f 100644 --- a/boss/ui.cpp +++ b/boss/ui.cpp @@ -51,7 +51,11 @@ namespace boss { auto& boss_combat = $world->get($boss_id); std::wstring status = fmt::format( - L"PLAYER: {}\nBOSS: {}", player_combat.hp, boss_combat.hp); + L"--PLAYER--\nHP:{}/{}\nAP:{}/{}\n\n--BOSS--\nHP:{}/{}\nAP:{}/{}\n----\n", + player_combat.hp, player_combat.max_hp, + player_combat.ap, player_combat.max_ap, + boss_combat.hp, boss_combat.max_hp, + boss_combat.ap, boss_combat.max_ap); if($world->has_event()) { auto [evt, entity, data] = $world->recv(); @@ -71,6 +75,7 @@ namespace boss { status += L"\nBOSS MISSED!"; } + /* if(result.player_did > 0) { zoom("boss14", 1.8); } else if(result.enemy_did > 0) { @@ -78,6 +83,7 @@ namespace boss { } else { zoom("", 0.0); } + */ } $actions.show_text("stats", status); diff --git a/components.hpp b/components.hpp index 039ecac..9716007 100644 --- a/components.hpp +++ b/components.hpp @@ -101,9 +101,9 @@ namespace components { struct Combat { int hp; int max_hp; - int damage; int ap_delta; int max_ap; + int damage; // everyone starts at 0 but ap_delta is added each round int ap = 0; @@ -181,7 +181,7 @@ namespace components { ENROLL_COMPONENT(EnemyConfig, ai_script, ai_start_name, ai_goal_name); ENROLL_COMPONENT(Personality, hearing_distance, tough); ENROLL_COMPONENT(Motion, dx, dy, random); - ENROLL_COMPONENT(Combat, hp, max_hp, damage, ap_delta, max_ap, dead); + ENROLL_COMPONENT(Combat, hp, max_hp, ap_delta, max_ap, damage, dead); ENROLL_COMPONENT(Device, config, events); ENROLL_COMPONENT(Storyboard, image, audio, layout, beats); ENROLL_COMPONENT(Animation, min_x, min_y, diff --git a/tests/battle.cpp b/tests/battle.cpp index 899c95b..e979e10 100644 --- a/tests/battle.cpp +++ b/tests/battle.cpp @@ -20,17 +20,20 @@ TEST_CASE("battle operations fantasy", "[combat-battle]") { DinkyECS::Entity host = 0; ai::EntityAI host_ai("Host::actions", host_start, host_goal); - components::Combat host_combat{100, 100, 20, 6, 12}; + components::Combat host_combat{ + .hp=100, .max_hp=100, .ap_delta=6, .max_ap=12, .damage=20}; battle.add_enemy({host, &host_ai, &host_combat, true}); DinkyECS::Entity axe_ranger = 1; ai::EntityAI axe_ai("Enemy::actions", ai_start, ai_goal); - components::Combat axe_combat{100, 100, 20, 8, 12}; + components::Combat axe_combat{ + .hp=20, .max_hp=20, .ap_delta=8, .max_ap=12, .damage=20}; 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, 12, 18}; + components::Combat rat_combat{ + .hp=10, .max_hp=10, .ap_delta=2, .max_ap=10, .damage=10}; battle.add_enemy({rat, &rat_ai, &rat_combat}); battle.set_all("enemy_found", true);