#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(const ArbUt::BorrowedPtr& queue, const ArbUt::BorrowedPtr& battle) { for (const auto& choice : queue->GetInnerQueue()) { HOOK(OnBeforeTurn, choice, choice.get()); } while (queue->HasNext() && !battle->HasEnded()) { 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"); } if (!battle->HasEnded()) { for (const auto& side : battle->GetSides()) { for (const auto& creature : side->GetCreatures()) { if (!creature.HasValue()) { continue; } HOOK(OnEndTurn, creature.GetValue(), creature.GetValue()); } } } queue->HasCompletedQueue = true; } void TurnHandler::ExecuteChoice(const 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 not usable, we don't want to execute its choice. if (!user->IsUsable()) { 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; } } #define FAIL_HANDLING(source, user, target) \ battle.GetValue()->TriggerEventListener(user); \ HOOK(OnFail, source, user) \ HOOK(OnOpponentFail, target, user) #define FAIL_HANDLING_NO_TARGET(source, user) \ battle.GetValue()->TriggerEventListener(user); \ HOOK(OnFail, source, user) void TurnHandler::ExecuteAttackChoice(const ArbUt::BorrowedPtr& choice) { auto battle = choice->GetUser()->GetBattle(); auto attackData = choice->GetAttack()->GetAttack(); auto attackName = attackData->GetName(); HOOK(ChangeAttack, choice, choice.GetRaw(), &attackName); if (attackName != choice->GetAttack()->GetAttack()->GetName()) { attackData = battle.GetValue()->GetLibrary()->GetAttackLibrary()->Get(attackName); choice->ChangeAttackScript(attackData); } auto targetType = attackData->GetTarget(); ArbUt::List> targets; Ensure(battle.HasValue()); try_creature(targets = TargetResolver::ResolveTargets(choice->GetTarget(), targetType, battle.GetValue()); , "Exception during target determination"); // HOOK: override targets u8 numberHits = 1; HOOK(ModifyNumberOfHits, choice, choice, &numberHits); if (numberHits == 0) { return; } auto attackScoped = ArbUt::ScopedPtr(new ExecutingAttack( targets, numberHits, choice->GetUser(), choice->GetAttack(), attackData, choice->GetAttackScript())); bool prevented = false; HOOK(PreventAttack, attackScoped, attackScoped, &prevented); if (prevented) { return; } if (!choice->GetAttack()->TryUse(1)) { return; } auto* attack = attackScoped.TakeOwnership(); battle.GetValue()->TriggerEventListener(attack); battle.GetValue()->RegisterHistoryElement(attack); bool fail = false; HOOK(FailAttack, attack, attack, &fail); if (fail) { FAIL_HANDLING_NO_TARGET(attack, choice->GetUser()); 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) const auto& user = attack->GetUser(); const auto& battle = user->GetBattle(); Ensure(battle.HasValue()) if (battle.GetValue()->HasEnded()) { return; } bool fail = false; HOOK(FailIncomingAttack, target, attack, target.GetRaw(), &fail); if (fail) { FAIL_HANDLING(attack, user, target); 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; } const auto& attackData = attack->GetUseAttack(); const auto& library = battle.GetValue()->GetLibrary(); const auto& dmgLibrary = library->GetDamageLibrary(); const auto& typeLibrary = library->GetTypeLibrary(); const 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 { const auto& effect = attackData->GetSecondaryEffect(); bool hasSecondaryEffect = effect->GetChance() == -1 || battle.GetValue()->GetRandom()->EffectChance(effect->GetChance(), attack, target.GetRaw()); if (hasSecondaryEffect) { HOOK(OnSecondaryEffect, attack, attack, target.GetRaw(), hitIndex); if (hit.HasFailed()) { FAIL_HANDLING(attack, user, target); } } } 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 { const auto& effect = attackData->GetSecondaryEffect(); bool hasSecondaryEffect = effect->GetChance() == -1 || battle.GetValue()->GetRandom()->EffectChance( effect->GetChance(), attack, target.GetRaw()); if (hasSecondaryEffect) { HOOK(OnSecondaryEffect, attack, attack, target.GetRaw(), hitIndex); if (hit.HasFailed()) { battle.GetValue()->TriggerEventListener(user); FAIL_HANDLING(attack, user, target); } } } 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(const ArbUt::BorrowedPtr& choice) { auto user = choice->GetUser(); auto battle = user->GetBattle(); if (!battle.HasValue()) { return; } bool preventSwitch = false; HOOK(PreventSelfSwitch, choice, choice.GetRaw(), &preventSwitch); if (preventSwitch) { return; } // HOOK: PreventOpponentSwitch for each opponent. for (auto* side : battle.GetValue()->GetSides()) { if (side == user->GetBattleSide()) { continue; } for (const auto& creature : side->GetCreatures()) { if (!creature.HasValue()) { continue; } HOOK(PreventOpponentSwitch, creature.GetValue(), choice, &preventSwitch); if (preventSwitch) { return; } } } user->ClearVolatileScripts(); auto userSide = user->GetBattleSide(); if (userSide.HasValue()) { auto userIndex = userSide.GetValue()->GetCreatureIndex(user); userSide.GetValue()->SetCreature(choice->GetNewCreature(), userIndex); } } void TurnHandler::ExecuteFleeChoice(const ArbUt::BorrowedPtr& choice) { auto user = choice->GetUser(); auto battle = user->GetBattle(); if (!battle.HasValue() || !battle.GetValue()->CanFlee()) { return; } bool preventRun = false; HOOK(PreventRunAway, choice, choice, &preventRun); if (preventRun) { return; } for (auto* side : battle.GetValue()->GetSides()) { if (side == user->GetBattleSide()) { continue; } for (const auto& creature : side->GetCreatures()) { if (!creature.HasValue()) { continue; } HOOK(PreventOpponentRunAway, creature.GetValue(), choice, &preventRun); if (preventRun) { return; } } } if (battle.GetValue()->GetLibrary()->GetMiscLibrary()->CanFlee(choice.GetRaw())) { if (user->GetBattleSide().HasValue()) { user->GetBattleSide().GetValue()->MarkAsFled(); } battle.GetValue()->ValidateBattleState(); } }