/* * 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 . */ // Headers #include #include #include "bitmap.h" #include "input.h" #include "output.h" #include "player.h" #include "transition.h" #include "game_battlealgorithm.h" #include "game_interpreter_battle.h" #include "game_message.h" #include "game_system.h" #include "game_party.h" #include "game_enemy.h" #include "game_enemyparty.h" #include "game_battle.h" #include "game_screen.h" #include "game_pictures.h" #include "battle_animation.h" #include #include "scene_battle.h" #include "scene_battle_rpg2k.h" #include "scene_battle_rpg2k3.h" #include "scene_gameover.h" #include "scene_debug.h" #include "game_interpreter.h" #include "rand.h" #include "autobattle.h" #include "enemyai.h" Scene_Battle::Scene_Battle(const BattleArgs& args) : troop_id(args.troop_id), allow_escape(args.allow_escape), first_strike(args.first_strike), on_battle_end(args.on_battle_end) { SetUseSharedDrawables(true); Scene::type = Scene::Battle; // Face graphic is cleared when battle scene is created. // Even if the battle gets interrupted by another scene and never starts. Main_Data::game_system->ClearMessageFace(); Main_Data::game_system->SetBeforeBattleMusic(Main_Data::game_system->GetCurrentBGM()); Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_BeginBattle)); Main_Data::game_system->BgmPlay(Main_Data::game_system->GetSystemBGM(Main_Data::game_system->BGM_Battle)); Game_Battle::SetTerrainId(args.terrain_id); Game_Battle::ChangeBackground(args.background); Game_Battle::SetBattleCondition(args.condition); Game_Battle::SetBattleFormation(args.formation); } Scene_Battle::~Scene_Battle() { Game_Battle::Quit(); } void Scene_Battle::Start() { if (Scene::Find(Scene::Map) == nullptr) { // Battletest mode - need to initialize screen Main_Data::game_screen->InitGraphics(); Main_Data::game_pictures->InitGraphics(); } // RPG_RT will cancel any active screen flash from the map, including // wiping out all flash LSD chunks. Main_Data::game_screen->FlashOnce(0, 0, 0, 0, 0); const lcf::rpg::Troop* troop = lcf::ReaderUtil::GetElement(lcf::Data::troops, troop_id); if (!troop) { Output::Warning("Invalid Monster Party ID {}", troop_id); EndBattle(BattleResult::Victory); return; } autobattle_algo = AutoBattle::CreateAlgorithm(Player::player_config.autobattle_algo.Get()); enemyai_algo = EnemyAi::CreateAlgorithm(Player::player_config.enemyai_algo.Get()); Output::Debug("Starting battle {} ({}): algos=({}/{})", troop_id, troop->name, autobattle_algo->GetName(), enemyai_algo->GetName()); Game_Battle::Init(troop_id); CreateUi(); InitEscapeChance(); SetState(State_Start); } void Scene_Battle::InitEscapeChance() { int avg_enemy_agi = Main_Data::game_enemyparty->GetAverageAgility(); int avg_actor_agi = Main_Data::game_party->GetAverageAgility(); int base_chance = Utils::RoundTo(100.0 * static_cast(avg_enemy_agi) / static_cast(avg_actor_agi)); this->escape_chance = Utils::Clamp(150 - base_chance, 64, 100); } bool Scene_Battle::TryEscape() { if (first_strike || Game_Battle::GetInterpreterBattle().IsForceFleeEnabled() || Rand::PercentChance(escape_chance)) { return true; } escape_chance += 10; return false; } void Scene_Battle::Continue(SceneType /* prev_scene */) { Game_Message::SetWindow(message_window.get()); // Debug scene / other scene could have changed party status. status_window->Refresh(); } void Scene_Battle::TransitionIn(SceneType prev_scene) { if (prev_scene == Scene::Debug) { Scene::TransitionIn(prev_scene); return; } Transition::instance().InitShow(Main_Data::game_system->GetTransition(Main_Data::game_system->Transition_BeginBattleShow), this); } void Scene_Battle::TransitionOut(SceneType next_scene) { auto& transition = Transition::instance(); if (next_scene == Scene::Debug) { transition.InitErase(Transition::TransitionCutOut, this); return; } if (next_scene == Scene::Null || next_scene == Scene::Title) { Scene::TransitionOut(next_scene); return; } transition.InitErase(Main_Data::game_system->GetTransition(Main_Data::game_system->Transition_EndBattleErase), this); } void Scene_Battle::DrawBackground(Bitmap& dst) { dst.Clear(); } void Scene_Battle::CreateUi() { std::vector commands; for (auto option: lcf::Data::system.easyrpg_battle_options) { battle_options.push_back((BattleOptionType)option); } // Add all menu items for (auto option: battle_options) { switch(option) { case Battle: commands.push_back(ToString(lcf::Data::terms.battle_fight)); break; case AutoBattle: commands.push_back(ToString(lcf::Data::terms.battle_auto)); break; case Escape: commands.push_back(ToString(lcf::Data::terms.battle_escape)); break; } } options_window.reset(new Window_Command(commands, option_command_mov)); options_window->SetHeight(80); options_window->SetY(SCREEN_TARGET_HEIGHT - 80); help_window.reset(new Window_Help(0, 0, SCREEN_TARGET_WIDTH, 32)); help_window->SetVisible(false); item_window.reset(new Window_Item(0, (SCREEN_TARGET_HEIGHT-80), SCREEN_TARGET_WIDTH, 80)); item_window->SetHelpWindow(help_window.get()); item_window->Refresh(); item_window->SetIndex(0); skill_window.reset(new Window_BattleSkill(0, (SCREEN_TARGET_HEIGHT-80), SCREEN_TARGET_WIDTH, 80)); skill_window->SetHelpWindow(help_window.get()); message_window.reset(new Window_Message(0, (SCREEN_TARGET_HEIGHT - 80), SCREEN_TARGET_WIDTH, 80)); Game_Message::SetWindow(message_window.get()); } void Scene_Battle::UpdateScreen() { Main_Data::game_screen->Update(); Main_Data::game_pictures->Update(true); } void Scene_Battle::UpdateBattlers() { std::vector battlers; Main_Data::game_enemyparty->GetBattlers(battlers); Main_Data::game_party->GetBattlers(battlers); for (auto* b : battlers) { b->UpdateBattle(); } Game_Battle::UpdateAnimation(); } void Scene_Battle::UpdateUi() { options_window->Update(); status_window->Update(); command_window->Update(); help_window->Update(); item_window->Update(); skill_window->Update(); target_window->Update(); Game_Message::Update(); } bool Scene_Battle::UpdateEvents() { auto& interp = Game_Battle::GetInterpreterBattle(); interp.Update(); status_window->Refresh(); if (interp.IsForceFleeEnabled()) { if (state != State_Escape) { SetState(State_Escape); } } auto call = TakeRequestedScene(); if (call && call->type == Scene::Gameover) { Scene::Push(std::move(call)); } if (interp.IsAsyncPending()) { auto aop = interp.GetAsyncOp(); if (aop.GetType() == AsyncOp::eTerminateBattle) { EndBattle(static_cast(aop.GetBattleResult())); return false; } if (CheckSceneExit(aop)) { return false; } } return true; } bool Scene_Battle::UpdateTimers() { const int timer1 = Main_Data::game_party->GetTimerSeconds(Game_Party::Timer1); const int timer2 = Main_Data::game_party->GetTimerSeconds(Game_Party::Timer2); // Screen Effects Main_Data::game_party->UpdateTimers(); // Query Timer before and after update. // If it reached zero during update was a running battle timer. if ((Main_Data::game_party->GetTimerSeconds(Game_Party::Timer1) == 0 && timer1 > 0) || (Main_Data::game_party->GetTimerSeconds(Game_Party::Timer2) == 0 && timer2 > 0)) { EndBattle(BattleResult::Abort); return false; } return true; } void Scene_Battle::UpdateGraphics() { Game_Battle::UpdateGraphics(); } bool Scene_Battle::IsWindowMoving() { return options_window->IsMovementActive() || status_window->IsMovementActive() || command_window->IsMovementActive(); } Game_Enemy* Scene_Battle::EnemySelected() { std::vector enemies; Main_Data::game_enemyparty->GetActiveBattlers(enemies); Game_Enemy* target = static_cast(enemies[target_window->GetIndex()]); if (previous_state == State_SelectCommand) { active_actor->SetBattleAlgorithm(std::make_shared(active_actor, target)); } else if (previous_state == State_SelectSkill) { active_actor->SetBattleAlgorithm( std::make_shared(active_actor, target, *skill_window->GetSkill())); } else if (previous_state == State_SelectItem) { auto* item = item_window->GetItem(); assert(item); if (item->type == lcf::rpg::Item::Type_special || (item->use_skill && (item->type == lcf::rpg::Item::Type_weapon || item->type == lcf::rpg::Item::Type_shield || item->type == lcf::rpg::Item::Type_armor || item->type == lcf::rpg::Item::Type_helmet || item->type == lcf::rpg::Item::Type_accessory))) { const lcf::rpg::Skill* skill = lcf::ReaderUtil::GetElement(lcf::Data::skills, item->skill_id); if (!skill) { Output::Warning("EnemySelected: Item {} references invalid skill {}", item->ID, item->skill_id); return nullptr; } active_actor->SetBattleAlgorithm(std::make_shared(active_actor, target, *skill, item)); } else { active_actor->SetBattleAlgorithm(std::make_shared(active_actor, target, *item)); } } else { assert("Invalid previous state for enemy selection" && false); } Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); ActionSelectedCallback(active_actor); return target; } Game_Actor* Scene_Battle::AllySelected() { Game_Actor& target = (*Main_Data::game_party)[status_window->GetIndex()]; if (previous_state == State_SelectSkill) { active_actor->SetBattleAlgorithm(std::make_shared(active_actor, &target, *skill_window->GetSkill())); } else if (previous_state == State_SelectItem) { auto* item = item_window->GetItem(); assert(item); if (item->type == lcf::rpg::Item::Type_special || (item->use_skill && (item->type == lcf::rpg::Item::Type_weapon || item->type == lcf::rpg::Item::Type_shield || item->type == lcf::rpg::Item::Type_armor || item->type == lcf::rpg::Item::Type_helmet || item->type == lcf::rpg::Item::Type_accessory))) { const lcf::rpg::Skill* skill = lcf::ReaderUtil::GetElement(lcf::Data::skills, item->skill_id); if (!skill) { Output::Warning("AllySelected: Item {} references invalid skill {}", item->ID, item->skill_id); return nullptr; } active_actor->SetBattleAlgorithm(std::make_shared(active_actor, &target, *skill, item)); } else { active_actor->SetBattleAlgorithm(std::make_shared(active_actor, &target, *item)); } } else { assert("Invalid previous state for ally selection" && false); } Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); ActionSelectedCallback(active_actor); return ⌖ } void Scene_Battle::AttackSelected() { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); if (active_actor->HasAttackAll()) { active_actor->SetBattleAlgorithm(std::make_shared(active_actor, Main_Data::game_enemyparty.get())); ActionSelectedCallback(active_actor); } else { SetState(State_SelectEnemyTarget); } } void Scene_Battle::DefendSelected() { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); active_actor->SetBattleAlgorithm(std::make_shared(active_actor)); ActionSelectedCallback(active_actor); } void Scene_Battle::ItemSelected() { const lcf::rpg::Item* item = item_window->GetItem(); if (!item || !item_window->CheckEnable(item->ID)) { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Buzzer)); return; } Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); switch (item->type) { case lcf::rpg::Item::Type_normal: case lcf::rpg::Item::Type_book: case lcf::rpg::Item::Type_material: assert(false); return; case lcf::rpg::Item::Type_weapon: case lcf::rpg::Item::Type_shield: case lcf::rpg::Item::Type_armor: case lcf::rpg::Item::Type_helmet: case lcf::rpg::Item::Type_accessory: case lcf::rpg::Item::Type_special: { const lcf::rpg::Skill* skill = lcf::ReaderUtil::GetElement(lcf::Data::skills, item->skill_id); if (!skill) { Output::Warning("ItemSelected: Item {} references invalid skill {}", item->ID, item->skill_id); return; } AssignSkill(skill, item); break; } case lcf::rpg::Item::Type_medicine: if (item->entire_party) { active_actor->SetBattleAlgorithm(std::make_shared(active_actor, Main_Data::game_party.get(), *item_window->GetItem())); ActionSelectedCallback(active_actor); } else { SetState(State_SelectAllyTarget); status_window->SetChoiceMode(Window_BattleStatus::ChoiceMode_All); } break; case lcf::rpg::Item::Type_switch: active_actor->SetBattleAlgorithm(std::make_shared(active_actor, *item_window->GetItem())); ActionSelectedCallback(active_actor); break; } } void Scene_Battle::SkillSelected() { const lcf::rpg::Skill* skill = skill_window->GetSkill(); if (!skill || !skill_window->CheckEnable(skill->ID)) { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Buzzer)); return; } Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); AssignSkill(skill, nullptr); } void Scene_Battle::AssignSkill(const lcf::rpg::Skill* skill, const lcf::rpg::Item* item) { switch (skill->type) { case lcf::rpg::Skill::Type_teleport: case lcf::rpg::Skill::Type_escape: case lcf::rpg::Skill::Type_switch: { active_actor->SetBattleAlgorithm(std::make_shared(active_actor, *skill, item)); ActionSelectedCallback(active_actor); return; } default: break; } switch (skill->scope) { case lcf::rpg::Skill::Scope_enemy: SetState(State_SelectEnemyTarget); break; case lcf::rpg::Skill::Scope_ally: SetState(State_SelectAllyTarget); status_window->SetChoiceMode(Window_BattleStatus::ChoiceMode_All); break; case lcf::rpg::Skill::Scope_enemies: active_actor->SetBattleAlgorithm(std::make_shared( active_actor, Main_Data::game_enemyparty.get(), *skill, item)); ActionSelectedCallback(active_actor); break; case lcf::rpg::Skill::Scope_self: active_actor->SetBattleAlgorithm(std::make_shared( active_actor, active_actor, *skill, item)); ActionSelectedCallback(active_actor); break; case lcf::rpg::Skill::Scope_party: active_actor->SetBattleAlgorithm(std::make_shared( active_actor, Main_Data::game_party.get(), *skill, item)); ActionSelectedCallback(active_actor); break; } } std::shared_ptr Scene_Battle::Create(const BattleArgs& args) { if (Player::IsRPG2k()) { return std::make_shared(args); } else { return std::make_shared(args); } } void Scene_Battle::PrepareBattleAction(Game_Battler* battler) { if (battler->GetBattleAlgorithm() == nullptr) { return; } if (!battler->CanAct()) { if (battler->GetBattleAlgorithm()->GetType() != Game_BattleAlgorithm::Type::None) { battler->SetBattleAlgorithm(std::make_shared(battler)); } return; } if (battler->GetSignificantRestriction() == lcf::rpg::State::Restriction_attack_ally) { Game_Battler *target = battler->GetType() == Game_Battler::Type_Enemy ? Main_Data::game_enemyparty->GetRandomActiveBattler() : Main_Data::game_party->GetRandomActiveBattler(); battler->SetBattleAlgorithm(std::make_shared(battler, target)); return; } if (battler->GetSignificantRestriction() == lcf::rpg::State::Restriction_attack_enemy) { Game_Battler *target = battler->GetType() == Game_Battler::Type_Ally ? Main_Data::game_enemyparty->GetRandomActiveBattler() : Main_Data::game_party->GetRandomActiveBattler(); battler->SetBattleAlgorithm(std::make_shared(battler, target)); return; } // If we can no longer perform the action (no more items, ran out of SP, etc..) if (!battler->GetBattleAlgorithm()->ActionIsPossible()) { battler->SetBattleAlgorithm(std::make_shared(battler)); } } void Scene_Battle::RemoveCurrentAction() { battle_actions.front()->SetBattleAlgorithm(nullptr); battle_actions.pop_front(); } void Scene_Battle::ActionSelectedCallback(Game_Battler* for_battler) { assert(for_battler->GetBattleAlgorithm() != nullptr); if (for_battler->GetBattleAlgorithm() == nullptr) { Output::Warning("ActionSelectedCallback: Invalid action for battler {} ({})", for_battler->GetId(), for_battler->GetName()); Output::Warning("Please report a bug!"); } battle_actions.push_back(for_battler); } bool Scene_Battle::CallDebug() { if (Player::debug_flag) { Scene::Push(std::make_shared()); return true; } return false; } void Scene_Battle::SelectionFlash(Game_Battler* battler) { if (battler) { battler->Flash(31, 31, 31, 24, 16); } } void Scene_Battle::EndBattle(BattleResult result) { assert(Scene::instance.get() == this && "EndBattle called multiple times!"); Main_Data::game_party->IncBattleCount(); switch (result) { case BattleResult::Victory: Main_Data::game_party->IncWinCount(); break; case BattleResult::Escape: Main_Data::game_party->IncRunCount(); break; case BattleResult::Defeat: Main_Data::game_party->IncDefeatCount(); break; case BattleResult::Abort: break; } Scene::Pop(); // For RPG_RT compatibility, wait 30 frames if a battle test ends if (Game_Battle::battle_test.enabled) { Scene::instance->SetDelayFrames(30); } if (on_battle_end) { on_battle_end(result); on_battle_end = {}; } }