#include "TurnHandler.hpp" #include "../EventHooks/EventDataClasses.hpp" #include "../History/HistoryElements/AttackUseHistory.hpp" #include "../ScriptHandling/ScriptMacros.hpp" #include "ResolveTarget.hpp" using namespace CreatureLib::Battling; void TurnHandler::RunTurn(ArbUt::BorrowedPtr queue) { for (auto choice : queue->GetInnerQueue()) { HOOK(OnBeforeTurn, choice, choice.get()); } while (queue->HasNext()) { auto item = queue->Dequeue(); EnsureNotNull(item) Ensure(item->GetUser()->GetBattle().HasValue()) Ensure(item->GetUser()->GetBattleSide().HasValue()) auto index = (uint32_t)item->GetUser()->GetBattleSide().GetValue()->GetCreatureIndex(item->GetUser()); try_creature(ExecuteChoice(item.get()), "Executing choice failed for choice by mon on side " << ((uint32_t)item->GetUser()->GetBattleSide().GetValue()->GetSideIndex()) << " and index " << index << ". Choice had choice kind " << CreatureLib::Battling::TurnChoiceKindHelper::ToString(item->GetKind()) << " " << " with message"); } queue->HasCompletedQueue = true; } void TurnHandler::ExecuteChoice(ArbUt::BorrowedPtr choice) { auto choiceKind = choice->GetKind(); if (choiceKind == TurnChoiceKind::Pass) { return; } auto user = choice->GetUser(); if (!user->GetBattle().HasValue()) return; auto battle = user->GetBattle().GetValue(); if (battle->HasEnded()) return; // If the user is fainted, we don't want to execute its choice. if (user->IsFainted()) { return; } // If the user is not in the field, we don't want to execute its choice. if (!user->IsOnBattleField()) { return; } // If the choice is not valid, we don't want to execute it. if (!battle->CanUse(choice)) { return; } switch (choiceKind) { case TurnChoiceKind::Pass: return; case TurnChoiceKind::Attack: try_creature(return ExecuteAttackChoice(choice.ForceAs()), "Encountered exception during attack choice execution"); case TurnChoiceKind::Switch: return ExecuteSwitchChoice(choice.ForceAs()); case TurnChoiceKind::Flee: return ExecuteFleeChoice(choice.ForceAs()); case TurnChoiceKind::Item: NOT_IMPLEMENTED; } } void TurnHandler::ExecuteAttackChoice(const ArbUt::BorrowedPtr& choice) { auto battle = choice->GetUser()->GetBattle(); auto attackName = choice->GetAttack()->GetAttack()->GetName(); HOOK(ChangeAttack, choice, choice.GetRaw(), &attackName); if (attackName != choice->GetAttack()->GetAttack()->GetName()) { // TODO: Change attack } auto targetType = choice->GetAttack()->GetAttack()->GetTarget(); ArbUt::List> targets; Ensure(battle.HasValue()); try_creature(targets = TargetResolver::ResolveTargets(choice->GetTarget(), targetType, battle.GetValue()); , "Exception during target determination"); auto attack = new ExecutingAttack(targets, 1, choice->GetUser(), choice->GetAttack(), choice->GetAttackScript()); bool prevented = false; HOOK(PreventAttack, attack, attack, &prevented); if (prevented) { delete attack; return; } // HOOK: override targets if (!choice->GetAttack()->TryUse(1)) { delete attack; return; } battle.GetValue()->TriggerEventListener(attack); battle.GetValue()->RegisterHistoryElement(attack); // HOOK: check if attack fails bool fail = false; HOOK(FailAttack, attack, attack, &fail); if (fail) { // TODO: Fail handling. return; } bool stopBeforeAttack = false; HOOK(StopBeforeAttack, attack, attack, &stopBeforeAttack); if (stopBeforeAttack) { return; } HOOK(OnBeforeAttack, attack, attack); for (uint8_t i = 0; i < attack->GetTargetCount(); i++) { auto target = attack->GetTargets()[i]; if (target.HasValue()) { try_creature(HandleAttackForTarget(attack, target.GetValue()), "Exception occurred during handling attack " "for target"); } } } void TurnHandler::HandleAttackForTarget(ExecutingAttack* attack, const ArbUt::BorrowedPtr& target) { EnsureNotNull(attack) auto& user = attack->GetUser(); auto& battle = user->GetBattle(); Ensure(battle.HasValue()) if (battle.GetValue()->HasEnded()) return; bool fail = false; HOOK(FailIncomingAttack, target, attack, target.GetRaw(), &fail); if (fail) { // TODO: Fail handling. return; } bool invulnerable = false; HOOK(IsInvulnerable, target, attack, target.GetRaw(), &invulnerable); if (invulnerable) { // TODO: We should probably do something when a target is invulnerable. return; } auto numberOfHits = attack->GetNumberOfHits(); if (numberOfHits == 0) { HOOK(OnAttackMiss, target, attack, target.GetRaw()); battle.GetValue()->TriggerEventListener(user); return; } auto& learnedAttack = attack->GetAttack(); auto& attackData = learnedAttack->GetAttack(); auto& library = battle.GetValue()->GetLibrary(); auto& dmgLibrary = library->GetDamageLibrary(); auto& typeLibrary = library->GetTypeLibrary(); auto& miscLibrary = library->GetMiscLibrary(); EnsureNotNull(dmgLibrary) EnsureNotNull(typeLibrary) EnsureNotNull(miscLibrary) auto hitIterator = attack->GetTargetIteratorBegin(target); for (uint8_t hitIndex = 0; hitIndex < numberOfHits; hitIndex++) { if (battle.GetValue()->HasEnded()) return; if (user->IsFainted()) { break; } if (target->IsFainted()) { break; } auto& hit = hitIterator[hitIndex]; uint8_t hitType = attackData->GetType(); HOOK(ChangeAttackType, target, attack, target.GetRaw(), hitIndex, &hitType); hit.SetType(hitType); auto effectiveness = typeLibrary->GetEffectiveness(hitType, target->GetTypes()); HOOK(ChangeEffectiveness, attack, attack, target.GetRaw(), hitIndex, &effectiveness) hit.SetEffectiveness(effectiveness); hit.SetCritical(miscLibrary->IsCritical(attack, target.GetRaw(), hitIndex)); hit.SetBasePower(dmgLibrary->GetBasePower(attack, target.GetRaw(), hitIndex, hit)); hit.SetDamage(dmgLibrary->GetDamage(attack, target.GetRaw(), hitIndex, hit)); if (attackData->GetCategory() == Library::AttackCategory::Status) { if (attackData->HasSecondaryEffect()) { try { auto& effect = attackData->GetSecondaryEffect(); bool hasSecondaryEffect; if (effect->GetChance() == -1) { hasSecondaryEffect = true; } else { hasSecondaryEffect = battle.GetValue()->GetRandom()->EffectChance(effect->GetChance(), attack, target.GetRaw()); } if (hasSecondaryEffect) { HOOK(OnSecondaryEffect, user, attack, target.GetRaw(), hitIndex); } } catch (const ArbUt::Exception& e) { throw e; } catch (const std::exception& e) { THROW("Exception during status attack effect handling: " << e.what() << "."); } } } else { auto damage = hit.GetDamage(); if (damage > target->GetCurrentHealth()) { damage = target->GetCurrentHealth(); hit.SetDamage(damage); } if (damage > 0) { target->Damage(damage, DamageSource::AttackDamage); if (attackData->HasSecondaryEffect() && !user->IsFainted()) { bool preventSecondary = false; HOOK(PreventSecondaryEffects, target, attack, target.GetRaw(), hitIndex, &preventSecondary); if (!preventSecondary) { try { auto& effect = attackData->GetSecondaryEffect(); bool hasSecondaryEffect; if (effect->GetChance() == -1) { hasSecondaryEffect = true; } else { auto random = battle.GetValue()->GetRandom(); EnsureNotNull(random); hasSecondaryEffect = random->EffectChance(effect->GetChance(), attack, target.GetRaw()); } if (hasSecondaryEffect) { HOOK(OnSecondaryEffect, user, attack, target.GetRaw(), hitIndex); } } catch (const ArbUt::Exception& e) { throw e; } catch (const std::exception& e) { THROW("Exception during offensive attack secondary effect handling: " << e.what() << "."); } } } } } } if (!user->IsFainted()) { HOOK(OnAfterHits, user, attack, target.GetRaw()); } } void TurnHandler::ExecuteSwitchChoice(ArbUt::BorrowedPtr choice) { bool preventSwitch = false; HOOK(PreventSelfSwitch, choice, choice.GetRaw(), &preventSwitch); if (preventSwitch) { return; } // HOOK: PreventOpponentSwitch for each opponent. auto user = choice->GetUser(); user->ClearVolatileScripts(); auto userSide = user->GetBattleSide(); if (userSide.HasValue()) { auto userIndex = userSide.GetValue()->GetCreatureIndex(user); userSide.GetValue()->SetCreature(choice->GetNewCreature(), userIndex); } } void TurnHandler::ExecuteFleeChoice(ArbUt::BorrowedPtr choice) { auto user = choice->GetUser(); auto battle = user->GetBattle(); if (!battle.HasValue() || !battle.GetValue()->CanFlee()) { return; } // TODO: If any of the creatures on the users side has a script that prevents it from running, block. // TODO: If any of the creatures on any other side has a script that prevents this side from running, block. if (battle.GetValue()->GetLibrary()->GetMiscLibrary()->CanFlee(choice.GetRaw())) { if (user->GetBattleSide().HasValue()) { user->GetBattleSide().GetValue()->MarkAsFled(); } battle.GetValue()->ValidateBattleState(); } }