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