From d5502bd6fcc175ff2edddea15f3e6bfbd49b3cdb Mon Sep 17 00:00:00 2001 From: Matthew Fioravante Date: Mon, 5 Oct 2020 15:34:15 -0400 Subject: [PATCH] Refactor EnemyAi functions --- CMakeLists.txt | 2 + Makefile.am | 2 + src/algo.h | 21 +++ src/enemyai.cpp | 354 +++++++++++++++++++++++++++++++++++++++++++++++ src/enemyai.h | 109 +++++++++++++++ src/game_enemy.h | 7 + 6 files changed, 495 insertions(+) create mode 100644 src/enemyai.cpp create mode 100644 src/enemyai.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d6f4d6782a3..f025530fc69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,6 +97,8 @@ add_library(${PROJECT_NAME} STATIC src/dynrpg.h src/dynrpg_easyrpg.cpp src/dynrpg_easyrpg.h + src/enemyai.cpp + src/enemyai.h src/exe_reader.cpp src/exe_reader.h src/exfont.h diff --git a/Makefile.am b/Makefile.am index 74bd615db36..25ac4adf46b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -87,6 +87,8 @@ libeasyrpg_player_a_SOURCES = \ src/dynrpg.h \ src/dynrpg_easyrpg.cpp \ src/dynrpg_easyrpg.h \ + src/enemyai.cpp \ + src/enemyai.h \ src/exe_reader.cpp \ src/exe_reader.h \ src/exfont.h \ diff --git a/src/algo.h b/src/algo.h index e3884c67d18..bbf1358e69c 100644 --- a/src/algo.h +++ b/src/algo.h @@ -190,6 +190,27 @@ inline bool IsNormalOrSubskill(const lcf::rpg::Skill& skill) { || skill.type >= lcf::rpg::Skill::Type_subskill; } +/** + * Checks if the skill targets the opposing party. + * + * @param skill the skill to check + * @return true if targets opposing party + */ +inline bool SkillTargetsEnemies(const lcf::rpg::Skill& skill) { + return skill.scope == lcf::rpg::Skill::Scope_enemy + || skill.type == lcf::rpg::Skill::Scope_enemies; +} + +/** + * Checks if the skill targets the allied party. + * + * @param skill the skill to check + * @return true if targets allied party + */ +inline bool SkillTargetsAllies(const lcf::rpg::Skill& skill) { + return !SkillTargetsEnemies(skill); +} + } // namespace Algo diff --git a/src/enemyai.cpp b/src/enemyai.cpp new file mode 100644 index 00000000000..e309efaef2e --- /dev/null +++ b/src/enemyai.cpp @@ -0,0 +1,354 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ +#include "enemyai.h" +#include "game_actor.h" +#include "game_enemy.h" +#include "game_enemyparty.h" +#include "game_party.h" +#include "game_switches.h" +#include "game_battlealgorithm.h" +#include "game_battle.h" +#include "algo.h" +#include "player.h" +#include "output.h" +#include "rand.h" +#include +#include + +namespace EnemyAi { + +#ifdef EP_DEBUG_ENEMYAI +template +static void DebugLog(const char* fmt, Args&&... args) { + Output::Debug(fmt, std::forward(args)...); +} +#else +template +static void DebugLog(const char*, Args&&...) {} +#endif + +constexpr decltype(RpgRtCompat::name) RpgRtCompat::name; +constexpr decltype(RpgRtImproved::name) RpgRtImproved::name; + +std::unique_ptr CreateAlgorithm(StringView name) { + if (Utils::StrICmp(name, RpgRtImproved::name) == 0) { + return std::make_unique(); + } + if (Utils::StrICmp(name, RpgRtCompat::name) != 0) { + static bool warned = false; + if (!warned) { + Output::Debug("Invalid AutoBattle algo name `{}' falling back to {} ...", name, RpgRtCompat::name); + warned = true; + } + } + return std::make_unique(); +} + +void AlgorithmBase::SetEnemyAiAction(Game_Enemy& source) { + vSetEnemyAiAction(source); + if (source.GetBattleAlgorithm() == nullptr) { + source.SetBattleAlgorithm(std::make_shared(&source)); + } +} + +void RpgRtCompat::vSetEnemyAiAction(Game_Enemy& source) { + SelectEnemyAiActionRpgRtCompat(source, true); +} + +void RpgRtImproved::vSetEnemyAiAction(Game_Enemy& source) { + SelectEnemyAiActionRpgRtCompat(source, false); +} + +static std::shared_ptr MakeAttack(Game_Enemy& enemy) { + return std::make_shared(&enemy, Main_Data::game_party->GetRandomActiveBattler()); +} + +static std::shared_ptr MakeDoubleAttack(Game_Enemy& enemy) { + auto algo = std::make_shared(&enemy, Main_Data::game_party->GetRandomActiveBattler()); + algo->SetRepeat(2); + return algo; +} + +static std::shared_ptr MakeBasicAction(Game_Enemy& enemy, const lcf::rpg::EnemyAction& action) { + switch (action.basic) { + case lcf::rpg::EnemyAction::Basic_attack: + return MakeAttack(enemy); + case lcf::rpg::EnemyAction::Basic_dual_attack: + return MakeDoubleAttack(enemy); + case lcf::rpg::EnemyAction::Basic_defense: + return std::make_shared(&enemy); + case lcf::rpg::EnemyAction::Basic_observe: + return std::make_shared(&enemy); + case lcf::rpg::EnemyAction::Basic_charge: + return std::make_shared(&enemy); + case lcf::rpg::EnemyAction::Basic_autodestruction: + return std::make_shared(&enemy, Main_Data::game_party.get()); + case lcf::rpg::EnemyAction::Basic_escape: + return std::make_shared(&enemy); + case lcf::rpg::EnemyAction::Basic_nothing: + return std::make_shared(&enemy); + } + return nullptr; +} + +static Game_Battler* GetRandomSkillTarget(Game_Party_Base& party, const lcf::rpg::Skill& skill, bool emulate_bugs) { + std::vector battlers; + party.GetBattlers(battlers); + for (auto iter = battlers.begin(); iter != battlers.end();) { + if (IsSkillEffectiveOn(skill, **iter, emulate_bugs)) { + ++iter; + } else { + iter = battlers.erase(iter); + } + } + auto choice = Rand::GetRandomNumber(0, battlers.size() - 1); + return battlers[choice]; +} + +static std::shared_ptr MakeSkillAction(Game_Enemy& enemy, const lcf::rpg::EnemyAction& action, bool emulate_bugs) { + const auto* skill = lcf::ReaderUtil::GetElement(lcf::Data::skills, action.skill_id); + if (!skill) { + Output::Warning("CreateEnemyAction: Enemy can't use invalid skill {}", action.skill_id); + return nullptr; + } + + switch (skill->scope) { + case lcf::rpg::Skill::Scope_enemy: + return std::make_shared(&enemy, Main_Data::game_party->GetRandomActiveBattler(), *skill); + case lcf::rpg::Skill::Scope_ally: + return std::make_shared(&enemy, GetRandomSkillTarget(*Main_Data::game_enemyparty, *skill, emulate_bugs), *skill); + case lcf::rpg::Skill::Scope_enemies: + return std::make_shared(&enemy, Main_Data::game_party.get(), *skill); + case lcf::rpg::Skill::Scope_self: + return std::make_shared(&enemy, &enemy, *skill); + case lcf::rpg::Skill::Scope_party: + return std::make_shared(&enemy, Main_Data::game_enemyparty.get(), *skill); + } + return nullptr; +} + +static std::shared_ptr MakeAction(Game_Enemy& enemy, const lcf::rpg::EnemyAction& action, bool emulate_bugs) { + switch (action.kind) { + case lcf::rpg::EnemyAction::Kind_basic: + return MakeBasicAction(enemy, action); + case lcf::rpg::EnemyAction::Kind_skill: + return MakeSkillAction(enemy, action, emulate_bugs); + case lcf::rpg::EnemyAction::Kind_transformation: + return std::make_shared(&enemy, action.enemy_id); + } + return nullptr; +} + +void SetEnemyAction(Game_Enemy& enemy, const lcf::rpg::EnemyAction& action, bool emulate_bugs) { + auto algo = MakeAction(enemy, action, emulate_bugs); + + if (algo) { + if (action.switch_on) { + algo->SetSwitchEnable(action.switch_on_id); + } + if (action.switch_off) { + algo->SetSwitchEnable(action.switch_off_id); + } + } + + enemy.SetBattleAlgorithm(std::move(algo)); +} + + +bool IsSkillEffectiveOn(const lcf::rpg::Skill& skill, + const Game_Battler& target, + bool emulate_bugs) { + + if (skill.type == lcf::rpg::Skill::Type_switch) { + return true; + } + + if (!Algo::IsNormalOrSubskill(skill)) { + return false; + } + + if (Algo::SkillTargetsEnemies(skill)) { + return target.Exists(); + } + + // RPG_RT has a bug where if enemy is hidden and skill revivies, it is allowed. + if (!target.Exists()) { + // RPG_RT has a bug where 2k3 doesn't check the reverse_state_effects flag + return (skill.state_effects.size() > 0 && skill.state_effects[0]) + && (emulate_bugs || (!skill.reverse_state_effect && target.IsDead())); + } + + if (skill.affect_hp + || skill.affect_sp + || skill.affect_attack + || skill.affect_defense + || skill.affect_spirit + || skill.affect_agility) { + return true; + } + + for (int id = 1; id <= static_cast(skill.state_effects.size()); ++id) { + if (skill.state_effects[id - 1] && target.HasState(id)) { + return true; + } + } + + if (skill.affect_attr_defence) { + for (auto& attr: skill.attribute_effects) { + if (attr) { + return true; + } + } + } + + return false; +} + +bool IsActionValid(const Game_Enemy& source, const lcf::rpg::EnemyAction& action) { + if (action.kind == action.Kind_skill) { + if (!source.IsSkillUsable(action.skill_id)) { + return false; + } + } + + switch (action.condition_type) { + case lcf::rpg::EnemyAction::ConditionType_always: + return true; + case lcf::rpg::EnemyAction::ConditionType_switch: + return Main_Data::game_switches->Get(action.switch_id); + case lcf::rpg::EnemyAction::ConditionType_turn: + { + int turns = Game_Battle::GetTurn(); + return Game_Battle::CheckTurns(turns, action.condition_param2, action.condition_param1); + } + case lcf::rpg::EnemyAction::ConditionType_actors: + { + std::vector battlers; + Main_Data::game_enemyparty->GetActiveBattlers(battlers); + int count = (int)battlers.size(); + return count >= action.condition_param1 && count <= action.condition_param2; + } + case lcf::rpg::EnemyAction::ConditionType_hp: + { + int hp_percent = source.GetHp() * 100 / source.GetMaxHp(); + return hp_percent >= action.condition_param1 && hp_percent <= action.condition_param2; + } + case lcf::rpg::EnemyAction::ConditionType_sp: + { + int sp_percent = source.GetSp() * 100 / source.GetMaxSp(); + return sp_percent >= action.condition_param1 && sp_percent <= action.condition_param2; + } + case lcf::rpg::EnemyAction::ConditionType_party_lvl: + { + int party_lvl = Main_Data::game_party->GetAverageLevel(); + return party_lvl >= action.condition_param1 && party_lvl <= action.condition_param2; + } + case lcf::rpg::EnemyAction::ConditionType_party_fatigue: + { + int party_exh = Main_Data::game_party->GetFatigue(); + return party_exh >= action.condition_param1 && party_exh <= action.condition_param2; + } + default: + return true; + } +} + +static bool IsSkillEffectiveOnAnyTarget(Game_Enemy& source, int skill_id, bool emulate_bugs) { + const auto* skill = lcf::ReaderUtil::GetElement(lcf::Data::skills, skill_id); + assert(skill); + if (!Algo::IsNormalOrSubskill(*skill)) { + return true; + } + + switch (skill->scope) { + case lcf::rpg::Skill::Scope_enemy: + case lcf::rpg::Skill::Scope_enemies: + break; + case lcf::rpg::Skill::Scope_self: + return IsSkillEffectiveOn(*skill, source, emulate_bugs); + case lcf::rpg::Skill::Scope_ally: + case lcf::rpg::Skill::Scope_party: + for (auto* enemy: Main_Data::game_enemyparty->GetEnemies()) { + if (IsSkillEffectiveOn(*skill, *enemy, emulate_bugs)) { + return true; + } + } + return false; + } + + return true; +} + +void SelectEnemyAiActionRpgRtCompat(Game_Enemy& source, bool emulate_bugs) { + if (source.IsCharged()) { + source.SetBattleAlgorithm(MakeAttack(source)); + return; + } + + const auto& actions = source.GetDbEnemy().actions; + std::vector prios(actions.size(), 0); + int max_prio = 0; + for (int i = 0; i < static_cast(actions.size()); ++i) { + const auto& action = actions[i]; + if (IsActionValid(source, action)) { + prios[i] = action.rating; + max_prio = std::max(max_prio, action.rating); + } + } + + if (max_prio) { + for (auto& pr: prios) { + pr = std::max(0, pr - (max_prio - 10)); + } + } + + for (int i = 0; i < static_cast(actions.size()); ++i) { + const auto& action = actions[i]; + if (action.kind == lcf::rpg::EnemyAction::Kind_skill) { + if (!IsSkillEffectiveOnAnyTarget(source, action.skill_id, emulate_bugs)) { + prios[i] = 0; + } + } + } + + int sum_prios = 0; + for (auto& pr: prios) { + sum_prios += pr; + } + + if (sum_prios == 0) { + return; + } + + int which = Rand::GetRandomNumber(0, sum_prios - 1); + const lcf::rpg::EnemyAction* selected_action = nullptr; + for (int i = 0; i < static_cast(actions.size()); ++i) { + const auto& action = actions[i]; + if (which >= action.rating) { + which -= action.rating; + selected_action = &action; + continue; + } + break; + } + + if (selected_action) { + SetEnemyAction(source, *selected_action, emulate_bugs); + } +} + +} // namespace EnemyAi diff --git a/src/enemyai.h b/src/enemyai.h new file mode 100644 index 00000000000..cf3b02201ba --- /dev/null +++ b/src/enemyai.h @@ -0,0 +1,109 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ +#ifndef EP_ENENMYAI_H +#define EP_ENENMYAI_H + +#include +#include +#include +#include +#include + +class Game_Actor; +class Game_Enemy; + +namespace EnemyAi { +class AlgorithmBase; + +/** + * Enemy AI algorithm factory function which creates enemy ai algorithm from the given name + * + * @param name of the algo. + * @return An auto battle algorithm to be used in battles. + */ +std::unique_ptr CreateAlgorithm(StringView name); + +/** + * Base class for enemy ai algorithm implementations. + */ +class AlgorithmBase { +public: + virtual ~AlgorithmBase() {} + + /** @return the name of this algorithm */ + virtual StringView GetName() const = 0; + + /** + * Calculates the enemy ai algorithm and sets the algorithm on source. + * This computation ignores states on the actor, it is the resposibility of the caller + * to handle death, confuse, provoke, etc.. + * + * @param source The source actor to set the action for. + * @post source will have a BattleAlgorithm set. + */ + void SetEnemyAiAction(Game_Enemy& source); +private: + virtual void vSetEnemyAiAction(Game_Enemy& source) = 0; +}; + +/** + * The default autobattle algorithm which is strictly compatible with RPG_RT. + */ +class RpgRtCompat: public AlgorithmBase { +public: + static constexpr auto name = "RPG_RT"; + + StringView GetName() const override { return name; } +private: + void vSetEnemyAiAction(Game_Enemy& source) override; +}; + +/** + * The default autobattle algorithm which is strictly compatible with RPG_RT. + */ +class RpgRtImproved: public AlgorithmBase { +public: + static constexpr auto name = "RPG_RT+"; + + StringView GetName() const override { return name; } +private: + void vSetEnemyAiAction(Game_Enemy& source) override; +}; + +/** + * Runs the RPG_RT algorithm for selecting a battle action for an enemy. + * + * @param source the enemy who will take the action + * @param emulate_bugs if true, emulate RPG_RT bugs + * @post If an action was selected, the source will have a new battle algorithm attached. + */ +void SelectEnemyAiActionRpgRtCompat(Game_Enemy& source, bool emulate_bugs); + +/** + * Checks if the skill will be effective on a target. + * + * @param skill the skill to check + * @param emulate_bugs if true, emulate RPG_RT bugs + * @return true if a normal skill or a 2k3 subskill. + */ +bool IsSkillEffectiveOn(const lcf::rpg::Skill& skill, + const Game_Battler& target, + bool emulate_bugs); + +} // namespace AutoBattle + +#endif diff --git a/src/game_enemy.h b/src/game_enemy.h index b6f5df18439..f86b323fe2e 100644 --- a/src/game_enemy.h +++ b/src/game_enemy.h @@ -184,6 +184,9 @@ class Game_Enemy final : public Game_Battler const lcf::rpg::EnemyAction* ChooseRandomAction(); bool IsInParty() const override; + /** @return database enemy struct */ + const lcf::rpg::Enemy& GetDbEnemy() const; + protected: const lcf::rpg::Enemy* enemy = nullptr; const lcf::rpg::TroopMember* troop_member = nullptr; @@ -283,4 +286,8 @@ inline bool Game_Enemy::IsInParty() const { return true; } +inline const lcf::rpg::Enemy& Game_Enemy::GetDbEnemy() const { + return *enemy; +} + #endif