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
18 // Headers
19 #include <algorithm>
20 #include <sstream>
21
22 #include "bitmap.h"
23 #include "input.h"
24 #include "output.h"
25 #include "player.h"
26 #include "transition.h"
27 #include "game_battlealgorithm.h"
28 #include "game_interpreter_battle.h"
29 #include "game_message.h"
30 #include "game_system.h"
31 #include "game_party.h"
32 #include "game_enemy.h"
33 #include "game_enemyparty.h"
34 #include "game_battle.h"
35 #include "game_screen.h"
36 #include "game_pictures.h"
37 #include "battle_animation.h"
38 #include <lcf/reader_util.h>
39 #include "scene_battle.h"
40 #include "scene_battle_rpg2k.h"
41 #include "scene_battle_rpg2k3.h"
42 #include "scene_gameover.h"
43 #include "scene_debug.h"
44 #include "game_interpreter.h"
45 #include "rand.h"
46 #include "autobattle.h"
47 #include "enemyai.h"
48
Scene_Battle(const BattleArgs & args)49 Scene_Battle::Scene_Battle(const BattleArgs& args)
50 : troop_id(args.troop_id),
51 allow_escape(args.allow_escape),
52 first_strike(args.first_strike),
53 on_battle_end(args.on_battle_end)
54 {
55 SetUseSharedDrawables(true);
56
57 Scene::type = Scene::Battle;
58
59 // Face graphic is cleared when battle scene is created.
60 // Even if the battle gets interrupted by another scene and never starts.
61 Main_Data::game_system->ClearMessageFace();
62 Main_Data::game_system->SetBeforeBattleMusic(Main_Data::game_system->GetCurrentBGM());
63 Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_BeginBattle));
64 Main_Data::game_system->BgmPlay(Main_Data::game_system->GetSystemBGM(Main_Data::game_system->BGM_Battle));
65
66 Game_Battle::SetTerrainId(args.terrain_id);
67 Game_Battle::ChangeBackground(args.background);
68 Game_Battle::SetBattleCondition(args.condition);
69 Game_Battle::SetBattleFormation(args.formation);
70 }
71
~Scene_Battle()72 Scene_Battle::~Scene_Battle() {
73 Game_Battle::Quit();
74 }
75
Start()76 void Scene_Battle::Start() {
77 if (Scene::Find(Scene::Map) == nullptr) {
78 // Battletest mode - need to initialize screen
79 Main_Data::game_screen->InitGraphics();
80 Main_Data::game_pictures->InitGraphics();
81 }
82
83 // RPG_RT will cancel any active screen flash from the map, including
84 // wiping out all flash LSD chunks.
85 Main_Data::game_screen->FlashOnce(0, 0, 0, 0, 0);
86
87 const lcf::rpg::Troop* troop = lcf::ReaderUtil::GetElement(lcf::Data::troops, troop_id);
88
89 if (!troop) {
90 Output::Warning("Invalid Monster Party ID {}", troop_id);
91 EndBattle(BattleResult::Victory);
92 return;
93 }
94
95 autobattle_algo = AutoBattle::CreateAlgorithm(Player::player_config.autobattle_algo.Get());
96 enemyai_algo = EnemyAi::CreateAlgorithm(Player::player_config.enemyai_algo.Get());
97
98 Output::Debug("Starting battle {} ({}): algos=({}/{})", troop_id, troop->name, autobattle_algo->GetName(), enemyai_algo->GetName());
99
100 Game_Battle::Init(troop_id);
101
102 CreateUi();
103
104 InitEscapeChance();
105
106 SetState(State_Start);
107 }
108
InitEscapeChance()109 void Scene_Battle::InitEscapeChance() {
110 int avg_enemy_agi = Main_Data::game_enemyparty->GetAverageAgility();
111 int avg_actor_agi = Main_Data::game_party->GetAverageAgility();
112
113 int base_chance = Utils::RoundTo<int>(100.0 * static_cast<double>(avg_enemy_agi) / static_cast<double>(avg_actor_agi));
114 this->escape_chance = Utils::Clamp(150 - base_chance, 64, 100);
115 }
116
TryEscape()117 bool Scene_Battle::TryEscape() {
118 if (first_strike || Game_Battle::GetInterpreterBattle().IsForceFleeEnabled() || Rand::PercentChance(escape_chance)) {
119 return true;
120 }
121 escape_chance += 10;
122 return false;
123 }
124
Continue(SceneType)125 void Scene_Battle::Continue(SceneType /* prev_scene */) {
126 Game_Message::SetWindow(message_window.get());
127
128 // Debug scene / other scene could have changed party status.
129 status_window->Refresh();
130 }
131
TransitionIn(SceneType prev_scene)132 void Scene_Battle::TransitionIn(SceneType prev_scene) {
133 if (prev_scene == Scene::Debug) {
134 Scene::TransitionIn(prev_scene);
135 return;
136 }
137 Transition::instance().InitShow(Main_Data::game_system->GetTransition(Main_Data::game_system->Transition_BeginBattleShow), this);
138 }
139
TransitionOut(SceneType next_scene)140 void Scene_Battle::TransitionOut(SceneType next_scene) {
141 auto& transition = Transition::instance();
142
143 if (next_scene == Scene::Debug) {
144 transition.InitErase(Transition::TransitionCutOut, this);
145 return;
146 }
147
148 if (next_scene == Scene::Null || next_scene == Scene::Title) {
149 Scene::TransitionOut(next_scene);
150 return;
151 }
152
153 transition.InitErase(Main_Data::game_system->GetTransition(Main_Data::game_system->Transition_EndBattleErase), this);
154 }
155
DrawBackground(Bitmap & dst)156 void Scene_Battle::DrawBackground(Bitmap& dst) {
157 dst.Clear();
158 }
159
CreateUi()160 void Scene_Battle::CreateUi() {
161 std::vector<std::string> commands;
162
163 for (auto option: lcf::Data::system.easyrpg_battle_options) {
164 battle_options.push_back((BattleOptionType)option);
165 }
166
167 // Add all menu items
168 for (auto option: battle_options) {
169 switch(option) {
170 case Battle:
171 commands.push_back(ToString(lcf::Data::terms.battle_fight));
172 break;
173 case AutoBattle:
174 commands.push_back(ToString(lcf::Data::terms.battle_auto));
175 break;
176 case Escape:
177 commands.push_back(ToString(lcf::Data::terms.battle_escape));
178 break;
179 }
180 }
181
182 options_window.reset(new Window_Command(commands, option_command_mov));
183 options_window->SetHeight(80);
184 options_window->SetY(SCREEN_TARGET_HEIGHT - 80);
185
186 help_window.reset(new Window_Help(0, 0, SCREEN_TARGET_WIDTH, 32));
187 help_window->SetVisible(false);
188
189 item_window.reset(new Window_Item(0, (SCREEN_TARGET_HEIGHT-80), SCREEN_TARGET_WIDTH, 80));
190 item_window->SetHelpWindow(help_window.get());
191 item_window->Refresh();
192 item_window->SetIndex(0);
193
194 skill_window.reset(new Window_BattleSkill(0, (SCREEN_TARGET_HEIGHT-80), SCREEN_TARGET_WIDTH, 80));
195 skill_window->SetHelpWindow(help_window.get());
196
197 message_window.reset(new Window_Message(0, (SCREEN_TARGET_HEIGHT - 80), SCREEN_TARGET_WIDTH, 80));
198 Game_Message::SetWindow(message_window.get());
199 }
200
UpdateScreen()201 void Scene_Battle::UpdateScreen() {
202 Main_Data::game_screen->Update();
203 Main_Data::game_pictures->Update(true);
204 }
205
UpdateBattlers()206 void Scene_Battle::UpdateBattlers() {
207 std::vector<Game_Battler*> battlers;
208 Main_Data::game_enemyparty->GetBattlers(battlers);
209 Main_Data::game_party->GetBattlers(battlers);
210 for (auto* b : battlers) {
211 b->UpdateBattle();
212 }
213 Game_Battle::UpdateAnimation();
214 }
215
UpdateUi()216 void Scene_Battle::UpdateUi() {
217 options_window->Update();
218 status_window->Update();
219 command_window->Update();
220 help_window->Update();
221 item_window->Update();
222 skill_window->Update();
223 target_window->Update();
224
225 Game_Message::Update();
226 }
227
UpdateEvents()228 bool Scene_Battle::UpdateEvents() {
229 auto& interp = Game_Battle::GetInterpreterBattle();
230 interp.Update();
231 status_window->Refresh();
232
233 if (interp.IsForceFleeEnabled()) {
234 if (state != State_Escape) {
235 SetState(State_Escape);
236 }
237 }
238
239 auto call = TakeRequestedScene();
240 if (call && call->type == Scene::Gameover) {
241 Scene::Push(std::move(call));
242 }
243
244 if (interp.IsAsyncPending()) {
245 auto aop = interp.GetAsyncOp();
246
247 if (aop.GetType() == AsyncOp::eTerminateBattle) {
248 EndBattle(static_cast<BattleResult>(aop.GetBattleResult()));
249 return false;
250 }
251
252 if (CheckSceneExit(aop)) {
253 return false;
254 }
255 }
256
257 return true;
258 }
259
UpdateTimers()260 bool Scene_Battle::UpdateTimers() {
261 const int timer1 = Main_Data::game_party->GetTimerSeconds(Game_Party::Timer1);
262 const int timer2 = Main_Data::game_party->GetTimerSeconds(Game_Party::Timer2);
263
264 // Screen Effects
265 Main_Data::game_party->UpdateTimers();
266
267 // Query Timer before and after update.
268 // If it reached zero during update was a running battle timer.
269 if ((Main_Data::game_party->GetTimerSeconds(Game_Party::Timer1) == 0 && timer1 > 0) ||
270 (Main_Data::game_party->GetTimerSeconds(Game_Party::Timer2) == 0 && timer2 > 0)) {
271 EndBattle(BattleResult::Abort);
272 return false;
273 }
274 return true;
275 }
276
UpdateGraphics()277 void Scene_Battle::UpdateGraphics() {
278 Game_Battle::UpdateGraphics();
279 }
280
IsWindowMoving()281 bool Scene_Battle::IsWindowMoving() {
282 return options_window->IsMovementActive() || status_window->IsMovementActive() || command_window->IsMovementActive();
283 }
284
EnemySelected()285 Game_Enemy* Scene_Battle::EnemySelected() {
286 std::vector<Game_Battler*> enemies;
287 Main_Data::game_enemyparty->GetActiveBattlers(enemies);
288
289 Game_Enemy* target = static_cast<Game_Enemy*>(enemies[target_window->GetIndex()]);
290
291 if (previous_state == State_SelectCommand) {
292 active_actor->SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Normal>(active_actor, target));
293 } else if (previous_state == State_SelectSkill) {
294 active_actor->SetBattleAlgorithm(
295 std::make_shared<Game_BattleAlgorithm::Skill>(active_actor, target, *skill_window->GetSkill()));
296 } else if (previous_state == State_SelectItem) {
297 auto* item = item_window->GetItem();
298 assert(item);
299 if (item->type == lcf::rpg::Item::Type_special
300 || (item->use_skill && (item->type == lcf::rpg::Item::Type_weapon
301 || item->type == lcf::rpg::Item::Type_shield
302 || item->type == lcf::rpg::Item::Type_armor
303 || item->type == lcf::rpg::Item::Type_helmet
304 || item->type == lcf::rpg::Item::Type_accessory)))
305 {
306 const lcf::rpg::Skill* skill = lcf::ReaderUtil::GetElement(lcf::Data::skills, item->skill_id);
307 if (!skill) {
308 Output::Warning("EnemySelected: Item {} references invalid skill {}", item->ID, item->skill_id);
309 return nullptr;
310 }
311 active_actor->SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Skill>(active_actor, target, *skill, item));
312 } else {
313 active_actor->SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Item>(active_actor, target, *item));
314 }
315 } else {
316 assert("Invalid previous state for enemy selection" && false);
317 }
318
319 Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision));
320 ActionSelectedCallback(active_actor);
321 return target;
322 }
323
AllySelected()324 Game_Actor* Scene_Battle::AllySelected() {
325 Game_Actor& target = (*Main_Data::game_party)[status_window->GetIndex()];
326
327 if (previous_state == State_SelectSkill) {
328 active_actor->SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Skill>(active_actor, &target, *skill_window->GetSkill()));
329 } else if (previous_state == State_SelectItem) {
330 auto* item = item_window->GetItem();
331 assert(item);
332 if (item->type == lcf::rpg::Item::Type_special
333 || (item->use_skill && (item->type == lcf::rpg::Item::Type_weapon
334 || item->type == lcf::rpg::Item::Type_shield
335 || item->type == lcf::rpg::Item::Type_armor
336 || item->type == lcf::rpg::Item::Type_helmet
337 || item->type == lcf::rpg::Item::Type_accessory)))
338 {
339 const lcf::rpg::Skill* skill = lcf::ReaderUtil::GetElement(lcf::Data::skills, item->skill_id);
340 if (!skill) {
341 Output::Warning("AllySelected: Item {} references invalid skill {}", item->ID, item->skill_id);
342 return nullptr;
343 }
344 active_actor->SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Skill>(active_actor, &target, *skill, item));
345 } else {
346 active_actor->SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Item>(active_actor, &target, *item));
347 }
348 } else {
349 assert("Invalid previous state for ally selection" && false);
350 }
351
352 Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision));
353 ActionSelectedCallback(active_actor);
354 return ⌖
355 }
356
AttackSelected()357 void Scene_Battle::AttackSelected() {
358 Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision));
359
360 if (active_actor->HasAttackAll()) {
361 active_actor->SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Normal>(active_actor, Main_Data::game_enemyparty.get()));
362 ActionSelectedCallback(active_actor);
363 } else {
364 SetState(State_SelectEnemyTarget);
365 }
366 }
367
DefendSelected()368 void Scene_Battle::DefendSelected() {
369 Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision));
370
371 active_actor->SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Defend>(active_actor));
372
373 ActionSelectedCallback(active_actor);
374 }
375
ItemSelected()376 void Scene_Battle::ItemSelected() {
377 const lcf::rpg::Item* item = item_window->GetItem();
378
379 if (!item || !item_window->CheckEnable(item->ID)) {
380 Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Buzzer));
381 return;
382 }
383
384 Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision));
385
386 switch (item->type) {
387 case lcf::rpg::Item::Type_normal:
388 case lcf::rpg::Item::Type_book:
389 case lcf::rpg::Item::Type_material:
390 assert(false);
391 return;
392 case lcf::rpg::Item::Type_weapon:
393 case lcf::rpg::Item::Type_shield:
394 case lcf::rpg::Item::Type_armor:
395 case lcf::rpg::Item::Type_helmet:
396 case lcf::rpg::Item::Type_accessory:
397 case lcf::rpg::Item::Type_special: {
398 const lcf::rpg::Skill* skill = lcf::ReaderUtil::GetElement(lcf::Data::skills, item->skill_id);
399 if (!skill) {
400 Output::Warning("ItemSelected: Item {} references invalid skill {}", item->ID, item->skill_id);
401 return;
402 }
403 AssignSkill(skill, item);
404 break;
405 }
406 case lcf::rpg::Item::Type_medicine:
407 if (item->entire_party) {
408 active_actor->SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Item>(active_actor, Main_Data::game_party.get(), *item_window->GetItem()));
409 ActionSelectedCallback(active_actor);
410 } else {
411 SetState(State_SelectAllyTarget);
412 status_window->SetChoiceMode(Window_BattleStatus::ChoiceMode_All);
413 }
414 break;
415 case lcf::rpg::Item::Type_switch:
416 active_actor->SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Item>(active_actor, *item_window->GetItem()));
417 ActionSelectedCallback(active_actor);
418 break;
419 }
420 }
421
SkillSelected()422 void Scene_Battle::SkillSelected() {
423 const lcf::rpg::Skill* skill = skill_window->GetSkill();
424
425 if (!skill || !skill_window->CheckEnable(skill->ID)) {
426 Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Buzzer));
427 return;
428 }
429
430 Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision));
431
432 AssignSkill(skill, nullptr);
433 }
434
AssignSkill(const lcf::rpg::Skill * skill,const lcf::rpg::Item * item)435 void Scene_Battle::AssignSkill(const lcf::rpg::Skill* skill, const lcf::rpg::Item* item) {
436 switch (skill->type) {
437 case lcf::rpg::Skill::Type_teleport:
438 case lcf::rpg::Skill::Type_escape:
439 case lcf::rpg::Skill::Type_switch: {
440 active_actor->SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Skill>(active_actor, *skill, item));
441 ActionSelectedCallback(active_actor);
442 return;
443 }
444 default:
445 break;
446 }
447
448 switch (skill->scope) {
449 case lcf::rpg::Skill::Scope_enemy:
450 SetState(State_SelectEnemyTarget);
451 break;
452 case lcf::rpg::Skill::Scope_ally:
453 SetState(State_SelectAllyTarget);
454 status_window->SetChoiceMode(Window_BattleStatus::ChoiceMode_All);
455 break;
456 case lcf::rpg::Skill::Scope_enemies:
457 active_actor->SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Skill>(
458 active_actor, Main_Data::game_enemyparty.get(), *skill, item));
459 ActionSelectedCallback(active_actor);
460 break;
461 case lcf::rpg::Skill::Scope_self:
462 active_actor->SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Skill>(
463 active_actor, active_actor, *skill, item));
464 ActionSelectedCallback(active_actor);
465 break;
466 case lcf::rpg::Skill::Scope_party:
467 active_actor->SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Skill>(
468 active_actor, Main_Data::game_party.get(), *skill, item));
469 ActionSelectedCallback(active_actor);
470 break;
471 }
472 }
473
Create(const BattleArgs & args)474 std::shared_ptr<Scene_Battle> Scene_Battle::Create(const BattleArgs& args)
475 {
476 if (Player::IsRPG2k()) {
477 return std::make_shared<Scene_Battle_Rpg2k>(args);
478 }
479 else {
480 return std::make_shared<Scene_Battle_Rpg2k3>(args);
481 }
482 }
483
PrepareBattleAction(Game_Battler * battler)484 void Scene_Battle::PrepareBattleAction(Game_Battler* battler) {
485 if (battler->GetBattleAlgorithm() == nullptr) {
486 return;
487 }
488
489 if (!battler->CanAct()) {
490 if (battler->GetBattleAlgorithm()->GetType() != Game_BattleAlgorithm::Type::None) {
491 battler->SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::None>(battler));
492 }
493 return;
494 }
495
496 if (battler->GetSignificantRestriction() == lcf::rpg::State::Restriction_attack_ally) {
497 Game_Battler *target = battler->GetType() == Game_Battler::Type_Enemy ?
498 Main_Data::game_enemyparty->GetRandomActiveBattler() :
499 Main_Data::game_party->GetRandomActiveBattler();
500
501 battler->SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Normal>(battler, target));
502 return;
503 }
504
505 if (battler->GetSignificantRestriction() == lcf::rpg::State::Restriction_attack_enemy) {
506 Game_Battler *target = battler->GetType() == Game_Battler::Type_Ally ?
507 Main_Data::game_enemyparty->GetRandomActiveBattler() :
508 Main_Data::game_party->GetRandomActiveBattler();
509
510 battler->SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::Normal>(battler, target));
511 return;
512 }
513
514 // If we can no longer perform the action (no more items, ran out of SP, etc..)
515 if (!battler->GetBattleAlgorithm()->ActionIsPossible()) {
516 battler->SetBattleAlgorithm(std::make_shared<Game_BattleAlgorithm::None>(battler));
517 }
518 }
519
RemoveCurrentAction()520 void Scene_Battle::RemoveCurrentAction() {
521 battle_actions.front()->SetBattleAlgorithm(nullptr);
522 battle_actions.pop_front();
523 }
524
ActionSelectedCallback(Game_Battler * for_battler)525 void Scene_Battle::ActionSelectedCallback(Game_Battler* for_battler) {
526 assert(for_battler->GetBattleAlgorithm() != nullptr);
527
528 if (for_battler->GetBattleAlgorithm() == nullptr) {
529 Output::Warning("ActionSelectedCallback: Invalid action for battler {} ({})",
530 for_battler->GetId(), for_battler->GetName());
531 Output::Warning("Please report a bug!");
532 }
533
534 battle_actions.push_back(for_battler);
535 }
536
CallDebug()537 bool Scene_Battle::CallDebug() {
538 if (Player::debug_flag) {
539 Scene::Push(std::make_shared<Scene_Debug>());
540 return true;
541 }
542 return false;
543 }
544
SelectionFlash(Game_Battler * battler)545 void Scene_Battle::SelectionFlash(Game_Battler* battler) {
546 if (battler) {
547 battler->Flash(31, 31, 31, 24, 16);
548 }
549 }
550
EndBattle(BattleResult result)551 void Scene_Battle::EndBattle(BattleResult result) {
552 assert(Scene::instance.get() == this && "EndBattle called multiple times!");
553
554 Main_Data::game_party->IncBattleCount();
555 switch (result) {
556 case BattleResult::Victory: Main_Data::game_party->IncWinCount(); break;
557 case BattleResult::Escape: Main_Data::game_party->IncRunCount(); break;
558 case BattleResult::Defeat: Main_Data::game_party->IncDefeatCount(); break;
559 case BattleResult::Abort: break;
560 }
561
562 Scene::Pop();
563
564 // For RPG_RT compatibility, wait 30 frames if a battle test ends
565 if (Game_Battle::battle_test.enabled) {
566 Scene::instance->SetDelayFrames(30);
567 }
568
569 if (on_battle_end) {
570 on_battle_end(result);
571 on_battle_end = {};
572 }
573 }
574
575