1 /*
2 * This file is part of EasyRPG Player.
3 *
4 * EasyRPG Player is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * EasyRPG Player is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with EasyRPG Player. If not, see <http://www.gnu.org/licenses/>.
16 */
17 #include "autobattle.h"
18 #include "game_actor.h"
19 #include "game_enemy.h"
20 #include "game_enemyparty.h"
21 #include "game_party.h"
22 #include "game_battlealgorithm.h"
23 #include "game_battle.h"
24 #include "algo.h"
25 #include "player.h"
26 #include "output.h"
27 #include "rand.h"
28 #include <lcf/reader_util.h>
29 #include <lcf/data.h>
30
31 namespace AutoBattle {
32
33 #ifdef EP_DEBUG_AUTOBATTLE
34 template <typename... Args>
DebugLog(const char * fmt,Args &&...args)35 static void DebugLog(const char* fmt, Args&&... args) {
36 Output::Debug(fmt, std::forward<Args>(args)...);
37 }
38 #else
39 template <typename... Args>
40 static void DebugLog(const char*, Args&&...) {}
41 #endif
42
43 constexpr decltype(RpgRtCompat::name) RpgRtCompat::name;
44 constexpr decltype(AttackOnly::name) AttackOnly::name;
45 constexpr decltype(RpgRtImproved::name) RpgRtImproved::name;
46
CreateAlgorithm(StringView name)47 std::unique_ptr<AlgorithmBase> CreateAlgorithm(StringView name) {
48 if (Utils::StrICmp(name, RpgRtImproved::name) == 0) {
49 return std::make_unique<RpgRtImproved>();
50 }
51 if (Utils::StrICmp(name, AttackOnly::name) == 0) {
52 return std::make_unique<AttackOnly>();
53 }
54 if (Utils::StrICmp(name, RpgRtCompat::name) != 0) {
55 static bool warned = false;
56 if (!warned) {
57 Output::Debug("Invalid AutoBattle algo name `{}' falling back to {} ...", name, RpgRtCompat::name);
58 warned = true;
59 }
60 }
61 return std::make_unique<RpgRtCompat>();
62 }
63
SetAutoBattleAction(Game_Actor & source)64 void AlgorithmBase::SetAutoBattleAction(Game_Actor& source) {
65 vSetAutoBattleAction(source);
66 if (source.GetBattleAlgorithm() == nullptr) {
67 source.SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::None>(&source));
68 }
69 }
70
vSetAutoBattleAction(Game_Actor & source)71 void RpgRtCompat::vSetAutoBattleAction(Game_Actor& source) {
72 SelectAutoBattleActionRpgRtCompat(source, Game_Battle::GetBattleCondition());
73 }
74
vSetAutoBattleAction(Game_Actor & source)75 void AttackOnly::vSetAutoBattleAction(Game_Actor& source) {
76 SelectAutoBattleAction(source, Game_Battler::WeaponAll, Game_Battle::GetBattleCondition(), false, false, false, false);
77 }
78
vSetAutoBattleAction(Game_Actor & source)79 void RpgRtImproved::vSetAutoBattleAction(Game_Actor& source) {
80 SelectAutoBattleAction(source, Game_Battler::WeaponAll, Game_Battle::GetBattleCondition(), true, false, false, false);
81 }
82
CalcSkillCostAutoBattle(const Game_Actor & source,const lcf::rpg::Skill & skill,bool emulate_bugs)83 static int CalcSkillCostAutoBattle(const Game_Actor& source, const lcf::rpg::Skill& skill, bool emulate_bugs) {
84 // RPG_RT autobattle ignores half sp cost modifier
85 return emulate_bugs
86 ? Algo::CalcSkillCost(skill, source.GetMaxSp(), false)
87 : source.CalculateSkillCost(skill.ID);
88 }
89
CalcSkillHealAutoBattleTargetRank(const Game_Actor & source,const Game_Battler & target,const lcf::rpg::Skill & skill,bool apply_variance,bool emulate_bugs)90 double CalcSkillHealAutoBattleTargetRank(const Game_Actor& source, const Game_Battler& target, const lcf::rpg::Skill& skill, bool apply_variance, bool emulate_bugs) {
91 assert(Algo::IsNormalOrSubskill(skill));
92 assert(Algo::SkillTargetsAllies(skill));
93
94 const double src_max_sp = source.GetMaxSp();
95 const double tgt_max_hp = target.GetMaxHp();
96 const double tgt_hp = target.GetHp();
97
98 if (target.GetHp() > 0) {
99 // Can the skill heal the target?
100 if (!skill.affect_hp) {
101 return 0.0;
102 }
103
104 const double base_effect = Algo::CalcSkillEffect(source, target, skill, apply_variance);
105 const double max_effect = std::min(base_effect, tgt_max_hp - tgt_hp);
106
107 auto rank = static_cast<double>(max_effect) / static_cast<double>(tgt_max_hp);
108 if (src_max_sp > 0) {
109 const double cost = CalcSkillCostAutoBattle(source, skill, emulate_bugs);
110 rank -= cost / src_max_sp / 8.0;
111 rank = std::max(rank, 0.0);
112 }
113 return rank;
114 }
115
116 // Can the skill revive the target?
117 if (skill.state_effects.size() > 1 && skill.state_effects[0]) {
118 // BUG: RPG_RT does not check the reverse_state_effect flag to skip skills which would kill party members
119 if (emulate_bugs || !skill.reverse_state_effect) {
120 return static_cast<double>(skill.power) / 1000.0 + 1.0;
121 }
122 }
123 return 0.0;
124 }
125
CalcSkillDmgAutoBattleTargetRank(const Game_Actor & source,const Game_Battler & target,const lcf::rpg::Skill & skill,bool apply_variance,bool emulate_bugs)126 double CalcSkillDmgAutoBattleTargetRank(const Game_Actor& source, const Game_Battler& target, const lcf::rpg::Skill& skill, bool apply_variance, bool emulate_bugs) {
127 assert(Algo::IsNormalOrSubskill(skill));
128 assert(Algo::SkillTargetsEnemies(skill));
129 (void)emulate_bugs;
130
131 if (!(skill.affect_hp && target.Exists())) {
132 return 0.0;
133 }
134
135 double rank = 0.0;
136 const double src_max_sp = source.GetMaxSp();
137 const double tgt_hp = target.GetHp();
138
139 const double base_effect = Algo::CalcSkillEffect(source, target, skill, apply_variance);
140 rank = std::min(base_effect, tgt_hp) / tgt_hp;
141 if (rank == 1.0) {
142 rank = 1.5;
143 }
144 if (src_max_sp > 0) {
145 const double cost = CalcSkillCostAutoBattle(source, skill, emulate_bugs);
146 rank -= cost / src_max_sp / 4.0;
147 rank = std::max(rank, 0.0);
148 }
149
150 // Bonus if the target is the first existing enemy?
151 for (auto* enemy: Main_Data::game_enemyparty->GetEnemies()) {
152 if (enemy->Exists()) {
153 if (enemy == &target) {
154 rank = rank * 1.5 + 0.5;
155 }
156 break;
157 }
158 }
159
160 return rank;
161 }
162
CalcSkillAutoBattleRank(const Game_Actor & source,const lcf::rpg::Skill & skill,bool apply_variance,bool emulate_bugs)163 double CalcSkillAutoBattleRank(const Game_Actor& source, const lcf::rpg::Skill& skill, bool apply_variance, bool emulate_bugs) {
164 if (!source.IsSkillUsable(skill.ID)) {
165 return 0.0;
166 }
167 if (!Algo::IsNormalOrSubskill(skill)) {
168 return 0.0;
169 }
170
171 double rank = 0.0;
172 switch (skill.scope) {
173 case lcf::rpg::Skill::Scope_ally:
174 for (auto* target: Main_Data::game_party->GetActors()) {
175 auto target_rank = CalcSkillHealAutoBattleTargetRank(source, *target, skill, apply_variance, emulate_bugs);
176 rank = std::max(rank, target_rank);
177 DebugLog("AUTOBATTLE: Actor {} Check Skill Single Ally {} Rank : {}({}): {} -> {}", source.GetName(), target->GetName(), skill.name, skill.ID, rank, target_rank);
178 }
179 break;
180 case lcf::rpg::Skill::Scope_party:
181 for (auto* target: Main_Data::game_party->GetActors()) {
182 auto target_rank = CalcSkillHealAutoBattleTargetRank(source, *target, skill, apply_variance, emulate_bugs);
183 rank += target_rank;
184 DebugLog("AUTOBATTLE: Actor {} Check Skill Party Ally {} Rank : {}({}): {} -> {}", source.GetName(), target->GetName(), skill.name, skill.ID, rank, target_rank);
185 }
186 break;
187 case lcf::rpg::Skill::Scope_enemy:
188 for (auto* target: Main_Data::game_enemyparty->GetEnemies()) {
189 auto target_rank = CalcSkillDmgAutoBattleTargetRank(source, *target, skill, apply_variance, emulate_bugs);
190 rank = std::max(rank, target_rank);
191 DebugLog("AUTOBATTLE: Actor {} Check Skill Single Enemy {} Rank : {}({}): {} -> {}", source.GetName(), target->GetName(), skill.name, skill.ID, rank, target_rank);
192 }
193 break;
194 case lcf::rpg::Skill::Scope_enemies:
195 for (auto* target: Main_Data::game_enemyparty->GetEnemies()) {
196 auto target_rank = CalcSkillDmgAutoBattleTargetRank(source, *target, skill, apply_variance, emulate_bugs);
197 rank += target_rank;
198 DebugLog("AUTOBATTLE: Actor {} Check Skill Party Enemy {} Rank : {}({}): {} -> {}", source.GetName(), target->GetName(), skill.name, skill.ID, rank, target_rank);
199 }
200 break;
201 case lcf::rpg::Skill::Scope_self:
202 rank = CalcSkillHealAutoBattleTargetRank(source, source, skill, apply_variance, emulate_bugs);
203 DebugLog("AUTOBATTLE: Actor {} Check Skill Self Rank : {}({}): {}", source.GetName(), skill.name, skill.ID, rank);
204 break;
205 }
206 if (rank > 0.0) {
207 rank += Rand::GetRandomNumber(0, 99) / 100.0;
208 }
209 return rank;
210 }
211
CalcNormalAttackAutoBattleTargetRank(const Game_Actor & source,const Game_Battler & target,Game_Battler::Weapon weapon,lcf::rpg::System::BattleCondition cond,bool apply_variance,bool emulate_bugs)212 double CalcNormalAttackAutoBattleTargetRank(const Game_Actor& source,
213 const Game_Battler& target,
214 Game_Battler::Weapon weapon,
215 lcf::rpg::System::BattleCondition cond,
216 bool apply_variance,
217 bool emulate_bugs)
218 {
219 if (!target.Exists()) {
220 return 0.0;
221 }
222 const bool is_critical_hit = false;
223 const bool is_charged = false;
224
225 // RPG_RT BUG: Normal damage variance is not used
226 // Note: RPG_RT does not do the "2k3_enemy_row_bug" when computing autobattle ranks.
227 double base_effect = Algo::CalcNormalAttackEffect(source, target, weapon, is_critical_hit, is_charged, apply_variance, cond, false);
228 // RPG_RT BUG: Dual Attack is ignored
229 if (!emulate_bugs) {
230 base_effect *= source.GetNumberOfAttacks(weapon);
231 }
232 const double tgt_hp = target.GetHp();
233
234 auto rank = std::min(base_effect, tgt_hp) / tgt_hp;
235 if (rank == 1.0) {
236 rank = 1.5;
237 }
238 if (!emulate_bugs) {
239 // EasyRPG customization - include sp cost of weapon attack using same logic as skill attack
240 const auto cost = std::min(source.CalculateWeaponSpCost(weapon), source.GetSp());
241 if (cost > 0) {
242 const double src_max_sp = source.GetMaxSp();
243 rank -= static_cast<double>(cost) / src_max_sp / 4.0;
244 rank = std::max(rank, 0.0);
245 }
246 }
247
248 // Bonus if the target is the first existing enemy?
249 for (auto* enemy: Main_Data::game_enemyparty->GetEnemies()) {
250 if (enemy->Exists()) {
251 if (enemy == &target) {
252 rank = rank * 1.5 + 0.5;
253 }
254 break;
255 }
256 }
257 if (rank > 0.0) {
258 rank = Rand::GetRandomNumber(0, 99) / 100.0 + rank * 1.5;
259 }
260 return rank;
261 }
262
CalcNormalAttackAutoBattleRank(const Game_Actor & source,Game_Battler::Weapon weapon,const lcf::rpg::System::BattleCondition cond,bool apply_variance,bool emulate_bugs)263 double CalcNormalAttackAutoBattleRank(const Game_Actor& source, Game_Battler::Weapon weapon, const lcf::rpg::System::BattleCondition cond, bool apply_variance, bool emulate_bugs) {
264 double rank = 0.0;
265 std::vector<Game_Battler*> targets;
266 Main_Data::game_enemyparty->GetBattlers(targets);
267
268 if (!emulate_bugs && source.HasAttackAll(weapon)) {
269 for (auto* target: targets) {
270 auto target_rank = CalcNormalAttackAutoBattleTargetRank(source, *target, weapon, cond, apply_variance, emulate_bugs);
271 rank += target_rank;
272 DebugLog("AUTOBATTLE: Actor {} Check Attack Party Enemy {} Rank : {} -> {}", source.GetName(), target->GetName(), rank, target_rank);
273 }
274 } else {
275 for (auto* target: targets) {
276 auto target_rank = CalcNormalAttackAutoBattleTargetRank(source, *target, weapon, cond, apply_variance, emulate_bugs);
277 rank = std::max(rank, target_rank);
278 DebugLog("AUTOBATTLE: Actor {} Check Attack Single Enemy {} Rank : {} -> {}", source.GetName(), target->GetName(), rank, target_rank);
279 }
280 }
281 return rank;
282 }
283
SelectAutoBattleAction(Game_Actor & source,Game_Battler::Weapon weapon,lcf::rpg::System::BattleCondition cond,bool do_skills,bool attack_variance,bool skill_variance,bool emulate_bugs)284 void SelectAutoBattleAction(Game_Actor& source,
285 Game_Battler::Weapon weapon,
286 lcf::rpg::System::BattleCondition cond,
287 bool do_skills,
288 bool attack_variance,
289 bool skill_variance,
290 bool emulate_bugs)
291 {
292 double skill_rank = 0.0;
293 lcf::rpg::Skill* skill = nullptr;
294
295 // Find the highest ranking skill
296 if (do_skills) {
297 for (auto& skill_id: source.GetSkills()) {
298 auto* candidate_skill = lcf::ReaderUtil::GetElement(lcf::Data::skills, skill_id);
299 if (candidate_skill) {
300 const auto rank = CalcSkillAutoBattleRank(source, *candidate_skill, skill_variance, emulate_bugs);
301 DebugLog("AUTOBATTLE: Actor {} Check Skill Rank : {}({}): {}", source.GetName(), candidate_skill->name, candidate_skill->ID, rank);
302 if (rank > skill_rank) {
303 skill_rank = rank;
304 skill = candidate_skill;
305 }
306 }
307 }
308 DebugLog("AUTOBATTLE: Actor {} Best Skill Rank : {}({}): {}", source.GetName(), skill->name, skill->ID, skill_rank);
309 }
310
311 double normal_attack_rank = CalcNormalAttackAutoBattleRank(source, weapon, cond, attack_variance, emulate_bugs);
312 DebugLog("AUTOBATTLE: Actor {} Normal Attack Rank : {}", source.GetName(), normal_attack_rank);
313
314 auto best_target_rank = 0.0;
315 Game_Battler* best_target = nullptr;
316 std::vector<Game_Battler*> targets;
317
318 if (skill != nullptr && normal_attack_rank < skill_rank) {
319 // Choose Skill Target
320 switch (skill->scope) {
321 case lcf::rpg::Skill::Scope_enemies:
322 DebugLog("AUTOBATTLE: Actor {} Select Skill Target : ALL ENEMIES", source.GetName());
323 source.SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Skill>(&source, Main_Data::game_enemyparty.get(), *skill));
324 return;
325 case lcf::rpg::Skill::Scope_party:
326 DebugLog("AUTOBATTLE: Actor {} Select Skill Target : ALL ALLIES", source.GetName());
327 source.SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Skill>(&source, Main_Data::game_party.get(), *skill));
328 return;
329 case lcf::rpg::Skill::Scope_enemy:
330 for (auto* target: Main_Data::game_enemyparty->GetEnemies()) {
331 const auto target_rank = CalcSkillDmgAutoBattleTargetRank(source, *target, *skill, skill_variance, emulate_bugs);
332 if (target_rank > best_target_rank) {
333 best_target_rank = target_rank;
334 best_target = target;
335 }
336 }
337 break;
338 case lcf::rpg::Skill::Scope_ally:
339 for (auto* target: Main_Data::game_party->GetActors()) {
340 const auto target_rank = CalcSkillHealAutoBattleTargetRank(source, *target, *skill, skill_variance, emulate_bugs);
341 if (target_rank > best_target_rank) {
342 best_target_rank = target_rank;
343 best_target = target;
344 }
345 }
346 break;
347 case lcf::rpg::Skill::Scope_self:
348 best_target = &source;
349 break;
350 }
351 if (best_target) {
352 DebugLog("AUTOBATTLE: Actor {} Select Skill Target : {}", source.GetName(), best_target->GetName());
353 source.SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Skill>(&source, best_target, *skill));
354 }
355 return;
356 }
357 // Choose normal attack
358 if (source.HasAttackAll(weapon)) {
359 DebugLog("AUTOBATTLE: Actor {} Select Attack Target : ALL ENEMIES", source.GetName());
360 source.SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Normal>(&source, Main_Data::game_enemyparty.get()));
361 return;
362 }
363
364 for (auto* target: Main_Data::game_enemyparty->GetEnemies()) {
365 const auto target_rank = CalcNormalAttackAutoBattleTargetRank(source, *target, weapon, cond, attack_variance, emulate_bugs);
366 // On case of ==, prefer the first enemy
367 if (target_rank > best_target_rank) {
368 best_target_rank = target_rank;
369 best_target = target;
370 }
371 }
372
373 if (best_target != nullptr) {
374 DebugLog("AUTOBATTLE: Actor {} Select Attack Target : {}", source.GetName(), best_target->GetName());
375 source.SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Normal>(&source, best_target));
376 return;
377 }
378 }
379
380 } // namespace AutoBattle
381