1188 lines
49 KiB
C++
1188 lines
49 KiB
C++
/*
|
|
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
|
|
*
|
|
* This program 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 2 of the License, or (at your
|
|
* option) any later version.
|
|
*
|
|
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "WorldSession.h"
|
|
#include "BattlePetMgr.h"
|
|
#include "BattlePetPackets.h"
|
|
#include "Creature.h"
|
|
#include "DB2Stores.h"
|
|
#include "Log.h"
|
|
#include "MotionMaster.h"
|
|
#include "ObjectAccessor.h"
|
|
#include "PetBattleMgr.h"
|
|
#include "Player.h"
|
|
#include "TemporarySummon.h"
|
|
|
|
void WorldSession::HandleBattlePetRequestJournal(WorldPackets::BattlePet::BattlePetRequestJournal& /*battlePetRequestJournal*/)
|
|
{
|
|
GetBattlePetMgr()->SendJournal();
|
|
}
|
|
|
|
void WorldSession::HandleBattlePetRequestJournalLock(WorldPackets::BattlePet::BattlePetRequestJournalLock& /*battlePetRequestJournalLock*/)
|
|
{
|
|
GetBattlePetMgr()->SendJournalLockStatus();
|
|
|
|
if (GetBattlePetMgr()->HasJournalLock())
|
|
GetBattlePetMgr()->SendJournal();
|
|
}
|
|
|
|
void WorldSession::HandleBattlePetSetBattleSlot(WorldPackets::BattlePet::BattlePetSetBattleSlot& battlePetSetBattleSlot)
|
|
{
|
|
BattlePets::BattlePetMgr* petMgr = GetBattlePetMgr();
|
|
|
|
BattlePets::BattlePet* pet = petMgr->GetPet(battlePetSetBattleSlot.PetGuid);
|
|
if (!pet)
|
|
return;
|
|
|
|
WorldPackets::BattlePet::BattlePetSlot* targetSlot = petMgr->GetSlot(BattlePets::BattlePetSlot(battlePetSetBattleSlot.Slot));
|
|
if (!targetSlot || targetSlot->Locked)
|
|
return;
|
|
|
|
// Find which slot this pet currently occupies (if any)
|
|
WorldPackets::BattlePet::BattlePetSlot* sourceSlot = nullptr;
|
|
for (uint8 i = 0; i < uint8(BattlePets::BattlePetSlot::Count); ++i)
|
|
{
|
|
WorldPackets::BattlePet::BattlePetSlot* slot = petMgr->GetSlot(BattlePets::BattlePetSlot(i));
|
|
if (slot && slot->Pet.Guid == battlePetSetBattleSlot.PetGuid)
|
|
{
|
|
sourceSlot = slot;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (sourceSlot == targetSlot)
|
|
return; // Already in that slot
|
|
|
|
// Swap: move whatever is in the target slot to the source slot
|
|
if (sourceSlot)
|
|
{
|
|
sourceSlot->Pet = targetSlot->Pet;
|
|
}
|
|
|
|
// Place the pet in the target slot
|
|
targetSlot->Pet = pet->PacketInfo;
|
|
}
|
|
|
|
void WorldSession::HandleBattlePetModifyName(WorldPackets::BattlePet::BattlePetModifyName& battlePetModifyName)
|
|
{
|
|
GetBattlePetMgr()->ModifyName(battlePetModifyName.PetGuid, battlePetModifyName.Name, std::move(battlePetModifyName.DeclinedNames));
|
|
}
|
|
|
|
void WorldSession::HandleQueryBattlePetName(WorldPackets::BattlePet::QueryBattlePetName& queryBattlePetName)
|
|
{
|
|
WorldPackets::BattlePet::QueryBattlePetNameResponse response;
|
|
response.BattlePetID = queryBattlePetName.BattlePetID;
|
|
|
|
Creature* summonedBattlePet = ObjectAccessor::GetCreatureOrPetOrVehicle(*_player, queryBattlePetName.UnitGUID);
|
|
if (!summonedBattlePet || !summonedBattlePet->IsSummon())
|
|
{
|
|
SendPacket(response.Write());
|
|
return;
|
|
}
|
|
|
|
response.CreatureID = summonedBattlePet->GetEntry();
|
|
response.Timestamp = summonedBattlePet->GetBattlePetCompanionNameTimestamp();
|
|
|
|
Unit* petOwner = summonedBattlePet->ToTempSummon()->GetSummonerUnit();
|
|
if (!petOwner->IsPlayer())
|
|
{
|
|
SendPacket(response.Write());
|
|
return;
|
|
}
|
|
|
|
BattlePets::BattlePet const* battlePet = petOwner->ToPlayer()->GetSession()->GetBattlePetMgr()->GetPet(queryBattlePetName.BattlePetID);
|
|
if (!battlePet)
|
|
{
|
|
SendPacket(response.Write());
|
|
return;
|
|
}
|
|
|
|
response.Name = battlePet->PacketInfo.Name;
|
|
if (battlePet->DeclinedName)
|
|
{
|
|
response.HasDeclined = true;
|
|
response.DeclinedNames = *battlePet->DeclinedName;
|
|
}
|
|
|
|
response.Allow = !response.Name.empty();
|
|
|
|
SendPacket(response.Write());
|
|
}
|
|
|
|
void WorldSession::HandleBattlePetDeletePet(WorldPackets::BattlePet::BattlePetDeletePet& battlePetDeletePet)
|
|
{
|
|
GetBattlePetMgr()->RemovePet(battlePetDeletePet.PetGuid);
|
|
}
|
|
|
|
void WorldSession::HandleBattlePetSetFlags(WorldPackets::BattlePet::BattlePetSetFlags& battlePetSetFlags)
|
|
{
|
|
if (!GetBattlePetMgr()->HasJournalLock())
|
|
return;
|
|
|
|
if (BattlePets::BattlePet* pet = GetBattlePetMgr()->GetPet(battlePetSetFlags.PetGuid))
|
|
{
|
|
if (battlePetSetFlags.ControlType == BattlePets::FLAGS_CONTROL_TYPE_APPLY)
|
|
pet->PacketInfo.Flags |= battlePetSetFlags.Flags;
|
|
else // FLAGS_CONTROL_TYPE_REMOVE
|
|
pet->PacketInfo.Flags &= ~battlePetSetFlags.Flags;
|
|
|
|
if (pet->SaveInfo != BattlePets::BATTLE_PET_NEW)
|
|
pet->SaveInfo = BattlePets::BATTLE_PET_CHANGED;
|
|
}
|
|
}
|
|
|
|
void WorldSession::HandleBattlePetClearFanfare(WorldPackets::BattlePet::BattlePetClearFanfare& battlePetClearFanfare)
|
|
{
|
|
GetBattlePetMgr()->ClearFanfare(battlePetClearFanfare.PetGuid);
|
|
}
|
|
|
|
void WorldSession::HandleCageBattlePet(WorldPackets::BattlePet::CageBattlePet& cageBattlePet)
|
|
{
|
|
GetBattlePetMgr()->CageBattlePet(cageBattlePet.PetGuid);
|
|
}
|
|
|
|
void WorldSession::HandleBattlePetSummon(WorldPackets::BattlePet::BattlePetSummon& battlePetSummon)
|
|
{
|
|
if (*_player->m_activePlayerData->SummonedBattlePetGUID != battlePetSummon.PetGuid)
|
|
GetBattlePetMgr()->SummonPet(battlePetSummon.PetGuid);
|
|
else
|
|
GetBattlePetMgr()->DismissPet();
|
|
}
|
|
|
|
void WorldSession::HandleBattlePetUpdateNotify(WorldPackets::BattlePet::BattlePetUpdateNotify& battlePetUpdateNotify)
|
|
{
|
|
GetBattlePetMgr()->UpdateBattlePetData(battlePetUpdateNotify.PetGuid);
|
|
}
|
|
|
|
void WorldSession::HandleBattlePetUpdateDisplayNotify(WorldPackets::BattlePet::BattlePetUpdateDisplayNotify& battlePetUpdateDisplayNotify)
|
|
{
|
|
GetBattlePetMgr()->UpdateBattlePetData(battlePetUpdateDisplayNotify.PetGuid);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Pet Battle Combat Handlers
|
|
// ============================================================================
|
|
|
|
static void BuildPetBattlePlayerUpdate(WorldPackets::BattlePet::PetBattlePlayerUpdateInfo& update,
|
|
PetBattles::PetBattleTeamData const& team, bool isWildTeam, PetBattles::PetBattle const* battle, uint8 teamIdx)
|
|
{
|
|
update.CharacterID = team.PlayerGUID;
|
|
update.TrapAbilityID = team.TrapAbilityID;
|
|
update.TrapStatus = battle ? battle->GetTrapStatus(teamIdx) : team.TrapStatus;
|
|
update.RoundTimeSecs = 0; // Initial update uses 0; round-level timer is in PvpMaxRoundTime
|
|
update.FrontPet = team.FrontPetIndex;
|
|
update.InputFlags = team.InputFlags;
|
|
|
|
for (uint8 i = 0; i < team.PetCount; ++i)
|
|
{
|
|
PetBattles::PetBattlePetData const& petData = team.Pets[i];
|
|
|
|
WorldPackets::BattlePet::PetBattlePetUpdateInfo petInfo;
|
|
petInfo.BattlePetGUID = petData.BattlePetGUID;
|
|
petInfo.SpeciesID = petData.Species;
|
|
petInfo.DisplayID = isWildTeam ? 0 : petData.DisplayID;
|
|
petInfo.Level = petData.Level;
|
|
petInfo.Xp = petData.Xp;
|
|
petInfo.CurHealth = petData.Health;
|
|
petInfo.MaxHealth = petData.MaxHealth;
|
|
petInfo.Power = petData.Power;
|
|
petInfo.Speed = petData.Speed;
|
|
petInfo.NpcTeamMemberID = petData.NpcTeamMemberID;
|
|
petInfo.BreedQuality = petData.Quality;
|
|
petInfo.Slot = static_cast<int8>(i);
|
|
petInfo.CustomName = petData.CustomName;
|
|
|
|
// Compute StatusFlags from pet state
|
|
uint16 statusFlags = 0;
|
|
if (petData.IsCaptured)
|
|
statusFlags |= PetBattles::PET_BATTLE_PET_STATUS_TRAPPED;
|
|
if (petData.IsStunned)
|
|
statusFlags |= PetBattles::PET_BATTLE_PET_STATUS_STUNNED;
|
|
if (petData.IsLockedByMultiTurn)
|
|
statusFlags |= PetBattles::PET_BATTLE_PET_STATUS_SWAP_OUT_LOCKED;
|
|
petInfo.StatusFlags = statusFlags;
|
|
|
|
// Fill ability info
|
|
for (uint8 j = 0; j < PetBattles::MAX_PET_BATTLE_ABILITIES; ++j)
|
|
{
|
|
if (petData.AbilityIDs[j] == 0)
|
|
continue;
|
|
|
|
WorldPackets::BattlePet::PetBattleAbilityInfo ability;
|
|
ability.AbilityID = petData.AbilityIDs[j];
|
|
ability.CooldownRemaining = petData.AbilityCooldowns[j];
|
|
ability.LockdownRemaining = petData.AbilityLockdowns[j];
|
|
ability.AbilityIndex = static_cast<int8>(j);
|
|
ability.Pboid = teamIdx * PetBattles::MAX_PET_BATTLE_TEAM_SIZE + i;
|
|
petInfo.Abilities.push_back(ability);
|
|
}
|
|
|
|
// Populate auras currently active on this pet
|
|
for (PetBattles::PetBattleAura const& aura : petData.Auras)
|
|
{
|
|
WorldPackets::BattlePet::PetBattleAuraInfo auraInfo;
|
|
auraInfo.AbilityID = aura.AbilityID;
|
|
auraInfo.InstanceID = aura.AuraInstanceID;
|
|
auraInfo.RoundsRemaining = aura.RemainingRounds;
|
|
auraInfo.CurrentRound = aura.CurrentRound;
|
|
auraInfo.CasterPBOID = aura.CasterTeam * PetBattles::MAX_PET_BATTLE_TEAM_SIZE + aura.CasterPet;
|
|
petInfo.Auras.push_back(auraInfo);
|
|
}
|
|
|
|
// Populate States with base breed+species stats from DB2 (retail format)
|
|
petInfo.States.push_back({ BattlePets::STATE_STAT_POWER, petData.BasePower });
|
|
petInfo.States.push_back({ BattlePets::STATE_STAT_STAMINA, petData.BaseStamina });
|
|
petInfo.States.push_back({ BattlePets::STATE_STAT_SPEED, petData.BaseSpeed });
|
|
petInfo.States.push_back({ 40, 5 }); // CritChance = 5%
|
|
if (petData.PetType >= 0 && petData.PetType < PetBattles::PET_TYPE_COUNT)
|
|
petInfo.States.push_back({ uint32(44 + petData.PetType), 1 }); // Family passive flag
|
|
|
|
update.Pets.push_back(std::move(petInfo));
|
|
}
|
|
}
|
|
|
|
static void BuildPetBattleEnviros(std::array<WorldPackets::BattlePet::PetBattleEnviroInfo, 3>& enviros,
|
|
PetBattles::PetBattle const* battle)
|
|
{
|
|
for (uint8 i = 0; i < PetBattles::MAX_PET_BATTLE_ENVIRONMENTS; ++i)
|
|
{
|
|
PetBattles::PetBattleEnvironment const& env = battle->GetEnvironment(i);
|
|
if (env.WeatherType != PetBattles::PET_BATTLE_WEATHER_NONE && env.AuraInstanceID != 0)
|
|
{
|
|
WorldPackets::BattlePet::PetBattleAuraInfo auraInfo;
|
|
auraInfo.AbilityID = env.AbilityID;
|
|
auraInfo.InstanceID = env.AuraInstanceID;
|
|
auraInfo.RoundsRemaining = env.RemainingRounds;
|
|
auraInfo.CurrentRound = env.CurrentRound;
|
|
auraInfo.CasterPBOID = env.CasterTeam * PetBattles::MAX_PET_BATTLE_TEAM_SIZE
|
|
+ battle->GetTeam(env.CasterTeam).FrontPetIndex;
|
|
enviros[i].Auras.push_back(auraInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void BuildPetBattleRoundPlayerData(WorldPackets::BattlePet::PetBattleRoundPlayerData& roundData,
|
|
PetBattles::PetBattleTeamData const& team, PetBattles::PetBattle const* battle, uint8 teamIdx)
|
|
{
|
|
roundData.NextInputFlags = team.InputFlags;
|
|
roundData.NextTrapStatus = battle ? static_cast<int8>(battle->GetTrapStatus(teamIdx)) : static_cast<int8>(team.TrapStatus);
|
|
// RoundTimeSecs is 0 for PvE/NPC battles; only PvP uses a timer
|
|
roundData.RoundTimeSecs = (battle && (battle->GetBattleType() == PetBattles::PET_BATTLE_TYPE_PVP || battle->GetBattleType() == PetBattles::PET_BATTLE_TYPE_LFPB))
|
|
? PetBattles::PET_BATTLE_MAX_ROUND_TIME : 0;
|
|
}
|
|
|
|
static void BuildRoundCooldowns(std::vector<WorldPackets::BattlePet::PetBattleCooldownInfo>& cooldowns,
|
|
PetBattles::PetBattle const* battle)
|
|
{
|
|
for (uint8 t = 0; t < PetBattles::MAX_PET_BATTLE_PLAYERS; ++t)
|
|
{
|
|
PetBattles::PetBattleTeamData const& team = battle->GetTeam(t);
|
|
for (uint8 i = 0; i < team.PetCount; ++i)
|
|
{
|
|
for (uint8 j = 0; j < PetBattles::MAX_PET_BATTLE_ABILITIES; ++j)
|
|
{
|
|
if (team.Pets[i].AbilityIDs[j] == 0)
|
|
continue;
|
|
|
|
WorldPackets::BattlePet::PetBattleCooldownInfo cd;
|
|
cd.AbilityID = team.Pets[i].AbilityIDs[j];
|
|
cd.CooldownRemaining = team.Pets[i].AbilityCooldowns[j];
|
|
cd.LockdownRemaining = team.Pets[i].AbilityLockdowns[j];
|
|
cd.AbilityIndex = static_cast<int8>(j);
|
|
cd.Pboid = t * PetBattles::MAX_PET_BATTLE_TEAM_SIZE + i;
|
|
cooldowns.push_back(cd);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void BuildPetXDied(std::vector<int8>& petXDied, PetBattles::PetBattle const* battle)
|
|
{
|
|
for (uint8 t = 0; t < PetBattles::MAX_PET_BATTLE_PLAYERS; ++t)
|
|
{
|
|
PetBattles::PetBattleTeamData const& team = battle->GetTeam(t);
|
|
for (uint8 i = 0; i < team.PetCount; ++i)
|
|
{
|
|
if (battle->WasPetKilledThisRound(t, i))
|
|
petXDied.push_back(static_cast<int8>(t * PetBattles::MAX_PET_BATTLE_TEAM_SIZE + i));
|
|
}
|
|
}
|
|
}
|
|
|
|
static void BuildRoundEffects(std::vector<WorldPackets::BattlePet::PetBattleEffectInfo>& effectList,
|
|
PetBattles::PetBattle const* battle)
|
|
{
|
|
for (PetBattles::PetBattleRoundEffect const& roundEffect : battle->GetRoundEffects())
|
|
{
|
|
WorldPackets::BattlePet::PetBattleEffectInfo effect;
|
|
effect.AbilityEffectID = roundEffect.AbilityEffectID;
|
|
effect.Flags = roundEffect.Flags;
|
|
effect.SourceAuraInstanceID = 0;
|
|
effect.TurnInstanceID = 0;
|
|
// Wire offset 12 is the PetBattleEffectType ? client switches on this to process effects
|
|
// (SetHealth=0, AuraApply=1, PetSwap=4, SetState=6, etc.), NOT a sequential index
|
|
effect.EffectIndex = roundEffect.EffectType;
|
|
effect.CasterPBOID = static_cast<int32>(roundEffect.SourceTeam * PetBattles::MAX_PET_BATTLE_TEAM_SIZE + roundEffect.SourcePet);
|
|
effect.StackDepth = 0;
|
|
|
|
WorldPackets::BattlePet::PetBattleEffectTargetInfo target;
|
|
// Environment targets use PBOID_ENVIRONMENT_BASE + slot; pet targets use team * TEAM_SIZE + pet
|
|
if (roundEffect.TargetEnvSlot >= 0)
|
|
target.Remaining = static_cast<int32>(PetBattles::PBOID_ENVIRONMENT_BASE + roundEffect.TargetEnvSlot);
|
|
else
|
|
target.Remaining = static_cast<int32>(roundEffect.TargetTeam * PetBattles::MAX_PET_BATTLE_TEAM_SIZE + roundEffect.TargetPet);
|
|
|
|
// Map effect type to target type and variable-length params
|
|
// Target types: 0=none, 1=aura(4 i32), 2=state(2 i32), 3=health(1 i32),
|
|
// 4=stat(1 i32), 5=trigger(1 i32), 6=cooldown(3 i32), 7=broadcast(1 i32)
|
|
switch (roundEffect.EffectType)
|
|
{
|
|
case PetBattles::PET_BATTLE_EFFECT_SET_HEALTH:
|
|
case PetBattles::PET_BATTLE_EFFECT_SET_MAX_HEALTH:
|
|
target.Type = 3; // Health
|
|
target.Params.push_back(roundEffect.Param1);
|
|
break;
|
|
case PetBattles::PET_BATTLE_EFFECT_AURA_APPLY:
|
|
case PetBattles::PET_BATTLE_EFFECT_AURA_CANCEL:
|
|
case PetBattles::PET_BATTLE_EFFECT_AURA_CHANGE:
|
|
{
|
|
target.Type = 1; // Aura: 4 params
|
|
target.Params.push_back(roundEffect.Param1); // AuraInstanceID
|
|
target.Params.push_back(roundEffect.Param2); // AuraAbilityID
|
|
// Use pre-captured values from creation time (Param3/Param4)
|
|
// instead of live aura lookup ? TickAuras may have decremented
|
|
// or removed the aura before this serialization runs.
|
|
target.Params.push_back(roundEffect.Param3); // RoundsRemaining
|
|
target.Params.push_back(roundEffect.Param4); // CurrentRound
|
|
break;
|
|
}
|
|
case PetBattles::PET_BATTLE_EFFECT_SET_STATE:
|
|
target.Type = 2; // State
|
|
target.Params.push_back(roundEffect.Param1); // StateID
|
|
target.Params.push_back(roundEffect.Param2); // StateValue
|
|
break;
|
|
case PetBattles::PET_BATTLE_EFFECT_STATUS_CHANGE:
|
|
case PetBattles::PET_BATTLE_EFFECT_SET_SPEED:
|
|
case PetBattles::PET_BATTLE_EFFECT_SET_POWER:
|
|
target.Type = 4; // NewStatValue
|
|
target.Params.push_back(roundEffect.Param1);
|
|
break;
|
|
case PetBattles::PET_BATTLE_EFFECT_TRIGGER_ABILITY:
|
|
target.Type = 5; // TriggerAbilityID
|
|
target.Params.push_back(roundEffect.Param1);
|
|
break;
|
|
case PetBattles::PET_BATTLE_EFFECT_ABILITY_CHANGE:
|
|
target.Type = 6; // Cooldown
|
|
target.Params.push_back(roundEffect.Param1); // ChangedAbilityID
|
|
target.Params.push_back(roundEffect.Param2); // CooldownRemaining
|
|
target.Params.push_back(0); // LockdownRemaining
|
|
break;
|
|
case PetBattles::PET_BATTLE_EFFECT_NPC_EMOTE:
|
|
target.Type = 7; // BroadcastTextID
|
|
target.Params.push_back(roundEffect.Param1);
|
|
break;
|
|
case PetBattles::PET_BATTLE_EFFECT_AURA_PROCESSING_BEGIN:
|
|
case PetBattles::PET_BATTLE_EFFECT_AURA_PROCESSING_END:
|
|
target.Type = 0; // No data ? sentinel markers with PBOID 9
|
|
break;
|
|
default:
|
|
target.Type = 0; // No data
|
|
break;
|
|
}
|
|
|
|
effect.Targets.push_back(target);
|
|
effectList.push_back(std::move(effect));
|
|
}
|
|
}
|
|
|
|
void WorldSession::HandlePetBattleRequestWild(WorldPackets::BattlePet::PetBattleRequestWild& petBattleRequestWild)
|
|
{
|
|
Player* player = GetPlayer();
|
|
if (!player)
|
|
return;
|
|
|
|
// Validate player state
|
|
if (!player->IsAlive())
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_NOT_WHILE_DEAD;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
if (player->IsInCombat())
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_NOT_WHILE_IN_COMBAT;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
if (sPetBattleMgr->IsPlayerInBattle(player->GetGUID()))
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_IN_BATTLE;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
// Check that player has pets in battle slots
|
|
BattlePets::BattlePetMgr* petMgr = GetBattlePetMgr();
|
|
bool hasPet = false;
|
|
for (uint8 i = 0; i < uint8(BattlePets::BattlePetSlot::Count); ++i)
|
|
{
|
|
WorldPackets::BattlePet::BattlePetSlot* slot = petMgr->GetSlot(BattlePets::BattlePetSlot(i));
|
|
if (slot && !slot->Locked && !slot->Pet.Guid.IsEmpty())
|
|
{
|
|
BattlePets::BattlePet* pet = petMgr->GetPet(slot->Pet.Guid);
|
|
if (pet && pet->PacketInfo.Health > 0)
|
|
{
|
|
hasPet = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasPet)
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_INVALID_LOADOUT_ALL_DEAD;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
// Validate target creature
|
|
Creature* creature = ObjectAccessor::GetCreature(*player, petBattleRequestWild.TargetGUID);
|
|
if (!creature || !creature->IsWildBattlePet())
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_TARGET_INVALID;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
// Distance check ? player must be reasonably close to the creature
|
|
if (!player->IsWithinDistInMap(creature, 50.0f))
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_TARGET_OUT_OF_RANGE;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
// Validate that the creature has a valid BattlePetSpecies entry (Species=0 crashes client)
|
|
if (!BattlePets::BattlePetMgr::GetBattlePetSpeciesByCreature(creature->GetEntry()))
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_TARGET_INVALID;
|
|
SendPacket(failed.Write());
|
|
TC_LOG_ERROR("server.loading", "PetBattle: Creature {} (entry {}) has no BattlePetSpecies entry, rejecting battle",
|
|
creature->GetGUID().ToString(), creature->GetEntry());
|
|
return;
|
|
}
|
|
|
|
// Create the battle
|
|
PetBattles::PetBattle* battle = sPetBattleMgr->CreateWildBattle(player, petBattleRequestWild.TargetGUID);
|
|
if (!battle)
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_IN_BATTLE;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
// Freeze the wild creature during battle (stop movement, make non-interactable)
|
|
creature->SetUnitFlag(UNIT_FLAG_PACIFIED);
|
|
creature->SetUnitFlag(UNIT_FLAG_NON_ATTACKABLE);
|
|
creature->GetMotionMaster()->MoveIdle();
|
|
player->SetFacingToObject(creature);
|
|
|
|
// Send finalize location with OK result
|
|
// BattleOrigin = midpoint, players placed 5.0 units from center along facing axis (matching retail sniff)
|
|
static constexpr float PET_BATTLE_HALF_DISTANCE = 5.0f;
|
|
WorldPackets::BattlePet::PetBattleFinalizeLocation finalizeLocation;
|
|
finalizeLocation.Location.LocationResult = PetBattles::PET_BATTLE_REQUEST_FAIL_OK;
|
|
Position midpoint;
|
|
midpoint.m_positionX = (player->GetPositionX() + creature->GetPositionX()) / 2.0f;
|
|
midpoint.m_positionY = (player->GetPositionY() + creature->GetPositionY()) / 2.0f;
|
|
midpoint.m_positionZ = (player->GetPositionZ() + creature->GetPositionZ()) / 2.0f;
|
|
float facing = player->GetAbsoluteAngle(creature);
|
|
finalizeLocation.Location.BattleOrigin = midpoint;
|
|
finalizeLocation.Location.BattleFacing = facing;
|
|
// Player positions are 5.0 units from center along the facing axis
|
|
finalizeLocation.Location.PlayerPositions[0] = Position(midpoint.m_positionX - PET_BATTLE_HALF_DISTANCE * std::cos(facing),
|
|
midpoint.m_positionY - PET_BATTLE_HALF_DISTANCE * std::sin(facing),
|
|
midpoint.m_positionZ);
|
|
finalizeLocation.Location.PlayerPositions[1] = Position(midpoint.m_positionX + PET_BATTLE_HALF_DISTANCE * std::cos(facing),
|
|
midpoint.m_positionY + PET_BATTLE_HALF_DISTANCE * std::sin(facing),
|
|
midpoint.m_positionZ);
|
|
SendPacket(finalizeLocation.Write());
|
|
|
|
// Build and send initial update
|
|
WorldPackets::BattlePet::PetBattleInitialUpdate initialUpdate;
|
|
|
|
BuildPetBattlePlayerUpdate(initialUpdate.Players[0], battle->GetTeam(PetBattles::PET_BATTLE_TEAM_1), false, battle, PetBattles::PET_BATTLE_TEAM_1);
|
|
BuildPetBattlePlayerUpdate(initialUpdate.Players[1], battle->GetTeam(PetBattles::PET_BATTLE_TEAM_2), true, battle, PetBattles::PET_BATTLE_TEAM_2);
|
|
BuildPetBattleEnviros(initialUpdate.Enviros, battle);
|
|
|
|
initialUpdate.CurRound = battle->GetCurrentRound();
|
|
initialUpdate.CurPetBattleState = static_cast<int8>(battle->GetBattleState());
|
|
initialUpdate.NpcCreatureID = 0; // Wild battles have no NPC trainer; creature identified by InitialWildPetGUID
|
|
initialUpdate.NpcDisplayID = 0;
|
|
initialUpdate.InitialWildPetGUID = petBattleRequestWild.TargetGUID;
|
|
initialUpdate.IsPVP = false;
|
|
initialUpdate.CanAwardXP = true;
|
|
initialUpdate.WaitingForFrontPetsMaxSecs = 30;
|
|
initialUpdate.PvpMaxRoundTime = PetBattles::PET_BATTLE_MAX_ROUND_TIME;
|
|
|
|
WorldPacket const* pkt = initialUpdate.Write();
|
|
|
|
// Dump all critical fields for debugging
|
|
for (uint8 t = 0; t < 2; ++t)
|
|
{
|
|
auto const& pu = initialUpdate.Players[t];
|
|
TC_LOG_DEBUG("server.loading", "PetBattle InitialUpdate Player[{}]: CharID={} TrapAbilityID={} TrapStatus={} RoundTimeSecs={} FrontPet={} InputFlags={} PetCount={}",
|
|
t, pu.CharacterID.ToString(), pu.TrapAbilityID, pu.TrapStatus, pu.RoundTimeSecs, pu.FrontPet, pu.InputFlags, pu.Pets.size());
|
|
for (uint8 p = 0; p < pu.Pets.size(); ++p)
|
|
{
|
|
auto const& pet = pu.Pets[p];
|
|
TC_LOG_DEBUG("server.loading", " Pet[{}]: GUID={} Species={} Display={} Collar={} Lvl={} Xp={} HP={}/{} Pow={} Spd={} NpcTM={} Quality={} Status={} Slot={} Abilities={} Auras={} States={} Name='{}'",
|
|
p, pet.BattlePetGUID.ToString(), pet.SpeciesID, pet.DisplayID, pet.CollarID, pet.Level, pet.Xp,
|
|
pet.CurHealth, pet.MaxHealth, pet.Power, pet.Speed, pet.NpcTeamMemberID,
|
|
pet.BreedQuality, pet.StatusFlags, pet.Slot, pet.Abilities.size(), pet.Auras.size(), pet.States.size(), pet.CustomName);
|
|
for (uint8 a = 0; a < pet.Abilities.size(); ++a)
|
|
TC_LOG_DEBUG("server.loading", " Ability[{}]: ID={} CD={} LD={} Idx={} Pboid={}",
|
|
a, pet.Abilities[a].AbilityID, pet.Abilities[a].CooldownRemaining, pet.Abilities[a].LockdownRemaining, pet.Abilities[a].AbilityIndex, pet.Abilities[a].Pboid);
|
|
for (uint8 s = 0; s < pet.States.size(); ++s)
|
|
TC_LOG_DEBUG("server.loading", " State[{}]: ID={} Val={}",
|
|
s, pet.States[s].StateID, pet.States[s].StateValue);
|
|
}
|
|
}
|
|
TC_LOG_DEBUG("server.loading", "PetBattle InitialUpdate: CurRound={} State={} NpcCreature={} NpcDisplay={} WildGUID={} IsPVP={} CanAwardXP={} PktSize={}",
|
|
initialUpdate.CurRound, initialUpdate.CurPetBattleState, initialUpdate.NpcCreatureID, initialUpdate.NpcDisplayID,
|
|
initialUpdate.InitialWildPetGUID.ToString(), initialUpdate.IsPVP, initialUpdate.CanAwardXP, pkt->size());
|
|
|
|
SendPacket(pkt);
|
|
|
|
// Start the battle (sets state to ROUND_IN_PROGRESS, round=0)
|
|
battle->Start();
|
|
|
|
// Send SMSG_PET_BATTLE_FIRST_ROUND to unlock abilities for the client
|
|
// Retail sends this ~1 second after INITIAL_UPDATE; we send it immediately since
|
|
// we don't have intro animations. This packet tells the client abilities are available.
|
|
{
|
|
WorldPackets::BattlePet::PetBattleFirstRound firstRound;
|
|
firstRound.CurRound = 0;
|
|
firstRound.NextPetBattleState = static_cast<int8>(PetBattles::PET_BATTLE_STATE_ROUND_IN_PROGRESS);
|
|
|
|
// Unlock abilities for both teams (NextInputFlags=0 means no restrictions)
|
|
for (uint8 i = 0; i < PetBattles::MAX_PET_BATTLE_PLAYERS; ++i)
|
|
{
|
|
firstRound.Players[i].NextInputFlags = 0;
|
|
firstRound.Players[i].NextTrapStatus = static_cast<int8>(battle->GetTrapStatus(i));
|
|
firstRound.Players[i].RoundTimeSecs = 0;
|
|
}
|
|
// No effects, cooldowns, or deaths for the initial first-round packet
|
|
|
|
SendPacket(firstRound.Write());
|
|
}
|
|
|
|
// NOTE: Do NOT call GenerateWildTeamInput() here. The wild team AI submits
|
|
// its input when the player submits theirs (in HandlePetBattleInput).
|
|
|
|
TC_LOG_DEBUG("server.loading", "PetBattleHandler: Player {} started wild battle against {}",
|
|
player->GetGUID().ToString(), petBattleRequestWild.TargetGUID.ToString());
|
|
}
|
|
|
|
void WorldSession::StartNPCPetBattle(Creature* trainer)
|
|
{
|
|
Player* player = GetPlayer();
|
|
if (!player || !trainer)
|
|
return;
|
|
|
|
if (player->IsInCombat())
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_NOT_WHILE_IN_COMBAT;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
if (sPetBattleMgr->IsPlayerInBattle(player->GetGUID()))
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_IN_BATTLE;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
// Check that player has alive pets in battle slots
|
|
BattlePets::BattlePetMgr* petMgr = GetBattlePetMgr();
|
|
bool hasPet = false;
|
|
for (uint8 i = 0; i < uint8(BattlePets::BattlePetSlot::Count); ++i)
|
|
{
|
|
WorldPackets::BattlePet::BattlePetSlot* slot = petMgr->GetSlot(BattlePets::BattlePetSlot(i));
|
|
if (slot && !slot->Locked && !slot->Pet.Guid.IsEmpty())
|
|
{
|
|
BattlePets::BattlePet* pet = petMgr->GetPet(slot->Pet.Guid);
|
|
if (pet && pet->PacketInfo.Health > 0)
|
|
{
|
|
hasPet = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasPet)
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_INVALID_LOADOUT_ALL_DEAD;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
// Validate that NPC has a team configured
|
|
if (!sPetBattleMgr->GetNPCTeam(trainer->GetEntry()))
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_TARGET_INVALID;
|
|
SendPacket(failed.Write());
|
|
TC_LOG_ERROR("server.loading", "PetBattle: NPC trainer {} (entry {}) has no pet team configured",
|
|
trainer->GetGUID().ToString(), trainer->GetEntry());
|
|
return;
|
|
}
|
|
|
|
// Create the NPC battle
|
|
PetBattles::PetBattle* battle = sPetBattleMgr->CreateNPCBattle(player, trainer);
|
|
if (!battle)
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_IN_BATTLE;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
// Stop NPC movement during battle and face player toward trainer
|
|
trainer->GetMotionMaster()->MoveIdle();
|
|
player->SetFacingToObject(trainer);
|
|
|
|
// Send finalize location with OK result
|
|
// BattleOrigin = midpoint, players placed 5.0 units from center along facing axis (matching retail sniff)
|
|
static constexpr float PET_BATTLE_HALF_DISTANCE = 5.0f;
|
|
WorldPackets::BattlePet::PetBattleFinalizeLocation finalizeLocation;
|
|
finalizeLocation.Location.LocationResult = PetBattles::PET_BATTLE_REQUEST_FAIL_OK;
|
|
Position midpoint;
|
|
midpoint.m_positionX = (player->GetPositionX() + trainer->GetPositionX()) / 2.0f;
|
|
midpoint.m_positionY = (player->GetPositionY() + trainer->GetPositionY()) / 2.0f;
|
|
midpoint.m_positionZ = (player->GetPositionZ() + trainer->GetPositionZ()) / 2.0f;
|
|
float facing = player->GetAbsoluteAngle(trainer);
|
|
finalizeLocation.Location.BattleOrigin = midpoint;
|
|
finalizeLocation.Location.BattleFacing = facing;
|
|
finalizeLocation.Location.PlayerPositions[0] = Position(midpoint.m_positionX - PET_BATTLE_HALF_DISTANCE * std::cos(facing),
|
|
midpoint.m_positionY - PET_BATTLE_HALF_DISTANCE * std::sin(facing),
|
|
midpoint.m_positionZ);
|
|
finalizeLocation.Location.PlayerPositions[1] = Position(midpoint.m_positionX + PET_BATTLE_HALF_DISTANCE * std::cos(facing),
|
|
midpoint.m_positionY + PET_BATTLE_HALF_DISTANCE * std::sin(facing),
|
|
midpoint.m_positionZ);
|
|
SendPacket(finalizeLocation.Write());
|
|
|
|
// Build and send initial update
|
|
WorldPackets::BattlePet::PetBattleInitialUpdate initialUpdate;
|
|
|
|
BuildPetBattlePlayerUpdate(initialUpdate.Players[0], battle->GetTeam(PetBattles::PET_BATTLE_TEAM_1), false, battle, PetBattles::PET_BATTLE_TEAM_1);
|
|
BuildPetBattlePlayerUpdate(initialUpdate.Players[1], battle->GetTeam(PetBattles::PET_BATTLE_TEAM_2), false, battle, PetBattles::PET_BATTLE_TEAM_2);
|
|
BuildPetBattleEnviros(initialUpdate.Enviros, battle);
|
|
|
|
initialUpdate.CurRound = battle->GetCurrentRound();
|
|
initialUpdate.CurPetBattleState = static_cast<int8>(battle->GetBattleState());
|
|
initialUpdate.NpcCreatureID = trainer->GetEntry();
|
|
initialUpdate.NpcDisplayID = trainer->GetDisplayId();
|
|
initialUpdate.InitialWildPetGUID = ObjectGuid::Empty;
|
|
initialUpdate.IsPVP = false;
|
|
initialUpdate.CanAwardXP = true;
|
|
initialUpdate.WaitingForFrontPetsMaxSecs = 30;
|
|
initialUpdate.PvpMaxRoundTime = PetBattles::PET_BATTLE_MAX_ROUND_TIME;
|
|
|
|
SendPacket(initialUpdate.Write());
|
|
|
|
// Start the battle
|
|
battle->Start();
|
|
|
|
// Send first round to unlock abilities
|
|
{
|
|
WorldPackets::BattlePet::PetBattleFirstRound firstRound;
|
|
firstRound.CurRound = 0;
|
|
firstRound.NextPetBattleState = static_cast<int8>(PetBattles::PET_BATTLE_STATE_ROUND_IN_PROGRESS);
|
|
|
|
for (uint8 i = 0; i < PetBattles::MAX_PET_BATTLE_PLAYERS; ++i)
|
|
{
|
|
firstRound.Players[i].NextInputFlags = 0;
|
|
firstRound.Players[i].NextTrapStatus = static_cast<int8>(battle->GetTrapStatus(i));
|
|
firstRound.Players[i].RoundTimeSecs = 0;
|
|
}
|
|
|
|
SendPacket(firstRound.Write());
|
|
}
|
|
|
|
TC_LOG_DEBUG("server.loading", "PetBattleHandler: Player {} started NPC battle against trainer {} (entry {})",
|
|
player->GetGUID().ToString(), trainer->GetGUID().ToString(), trainer->GetEntry());
|
|
}
|
|
|
|
void WorldSession::HandlePetBattleInput(WorldPackets::BattlePet::PetBattleInput& petBattleInput)
|
|
{
|
|
Player* player = GetPlayer();
|
|
if (!player)
|
|
return;
|
|
|
|
PetBattles::PetBattle* battle = sPetBattleMgr->GetBattleByPlayer(player->GetGUID());
|
|
if (!battle)
|
|
return;
|
|
|
|
// Only accept input during active round ? not during pet replacement, final round, or finished
|
|
if (battle->GetBattleState() != PetBattles::PET_BATTLE_STATE_ROUND_IN_PROGRESS)
|
|
return;
|
|
|
|
// Validate MoveType range
|
|
if (petBattleInput.MoveType < 0 || petBattleInput.MoveType > static_cast<int32>(PetBattles::PET_BATTLE_MOVE_PASS))
|
|
return;
|
|
|
|
// Determine which team this player is
|
|
uint8 teamIdx = PetBattles::PET_BATTLE_TEAM_1;
|
|
if (battle->GetTeam(PetBattles::PET_BATTLE_TEAM_2).PlayerGUID == player->GetGUID())
|
|
teamIdx = PetBattles::PET_BATTLE_TEAM_2;
|
|
|
|
// Don't accept duplicate input for the same round
|
|
if (battle->GetTeam(teamIdx).HasInputThisRound)
|
|
return;
|
|
|
|
battle->SubmitInput(teamIdx,
|
|
PetBattles::PetBattleMoveType(petBattleInput.MoveType),
|
|
petBattleInput.AbilityID,
|
|
petBattleInput.NewFrontPetIndex);
|
|
|
|
// Auto-submit AI team input now that the player has committed
|
|
if (battle->GetBattleType() == PetBattles::PET_BATTLE_TYPE_PVE)
|
|
battle->GenerateWildTeamInput();
|
|
else if (battle->GetBattleType() == PetBattles::PET_BATTLE_TYPE_NPC)
|
|
battle->GenerateNPCTeamInput();
|
|
|
|
// Check if round should process
|
|
if (battle->BothTeamsReady())
|
|
{
|
|
battle->ProcessRound();
|
|
|
|
// Always send RoundResult with effects (including death effects)
|
|
// so the client plays animations. FinalRound is sent separately
|
|
// after a delay by PetBattle::Update() when the battle is ending.
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRoundResult roundResult;
|
|
roundResult.CurRound = battle->GetCurrentRound();
|
|
roundResult.NextPetBattleState = static_cast<int8>(battle->GetBattleState());
|
|
for (uint8 i = 0; i < PetBattles::MAX_PET_BATTLE_PLAYERS; ++i)
|
|
BuildPetBattleRoundPlayerData(roundResult.Players[i], battle->GetTeam(i), battle, i);
|
|
BuildRoundEffects(roundResult.Effects, battle);
|
|
BuildRoundCooldowns(roundResult.Cooldowns, battle);
|
|
BuildPetXDied(roundResult.PetXDied, battle);
|
|
|
|
TC_LOG_DEBUG("server.loading", "PetBattle ROUND_RESULT: Round={} State={} FinalRound={} Effects={} Cooldowns={} Deaths={} P0Flags={} P1Flags={}",
|
|
roundResult.CurRound, roundResult.NextPetBattleState,
|
|
battle->IsFinalRound(),
|
|
roundResult.Effects.size(), roundResult.Cooldowns.size(), roundResult.PetXDied.size(),
|
|
roundResult.Players[0].NextInputFlags, roundResult.Players[1].NextInputFlags);
|
|
|
|
// Dump each effect for debugging
|
|
for (std::size_t e = 0; e < roundResult.Effects.size(); ++e)
|
|
{
|
|
auto const& eff = roundResult.Effects[e];
|
|
TC_LOG_DEBUG("server.loading", " Effect[{}]: AbilEffID={} Flags=0x{:X} Idx={} CasterPBOID={} StackDepth={} Targets={}",
|
|
e, eff.AbilityEffectID, eff.Flags, eff.EffectIndex, eff.CasterPBOID, eff.StackDepth, eff.Targets.size());
|
|
for (std::size_t tgt = 0; tgt < eff.Targets.size(); ++tgt)
|
|
{
|
|
auto const& t = eff.Targets[tgt];
|
|
std::string paramStr;
|
|
for (int32 p : t.Params)
|
|
paramStr += std::to_string(p) + " ";
|
|
TC_LOG_DEBUG("server.loading", " Target[{}]: Type={} Remaining(PBOID)={} Params=[{}]",
|
|
tgt, t.Type, t.Remaining, paramStr);
|
|
}
|
|
}
|
|
|
|
if (Player* p1 = battle->GetPlayerForTeam(PetBattles::PET_BATTLE_TEAM_1))
|
|
p1->SendDirectMessage(roundResult.Write());
|
|
if (Player* p2 = battle->GetPlayerForTeam(PetBattles::PET_BATTLE_TEAM_2))
|
|
p2->SendDirectMessage(roundResult.Write());
|
|
}
|
|
|
|
// FinalRound packet is now sent after a delay by PetBattle::Update()
|
|
// to allow the client time to play death/capture animations first
|
|
}
|
|
}
|
|
|
|
void WorldSession::HandlePetBattleReplaceFrontPet(WorldPackets::BattlePet::PetBattleReplaceFrontPet& petBattleReplaceFrontPet)
|
|
{
|
|
Player* player = GetPlayer();
|
|
if (!player)
|
|
return;
|
|
|
|
PetBattles::PetBattle* battle = sPetBattleMgr->GetBattleByPlayer(player->GetGUID());
|
|
if (!battle)
|
|
return;
|
|
|
|
// Only allow replacement during WAITING_FOR_FRONT_PET or ROUND_IN_PROGRESS (voluntary swap)
|
|
if (battle->IsFinalRound() || battle->IsFinished())
|
|
return;
|
|
|
|
uint8 teamIdx = PetBattles::PET_BATTLE_TEAM_1;
|
|
if (battle->GetTeam(PetBattles::PET_BATTLE_TEAM_2).PlayerGUID == player->GetGUID())
|
|
teamIdx = PetBattles::PET_BATTLE_TEAM_2;
|
|
|
|
// Validate pet index
|
|
int8 newPetIdx = petBattleReplaceFrontPet.FrontPetIndex;
|
|
PetBattles::PetBattleTeamData& team = battle->GetTeam(teamIdx);
|
|
|
|
TC_LOG_DEBUG("server.loading", "PetBattle ReplaceFrontPet: team={} newPetIdx={} petCount={} battleState={} needsSwap={}",
|
|
teamIdx, newPetIdx, team.PetCount, int(battle->GetBattleState()), battle->NeedsFrontPetSwap(teamIdx));
|
|
|
|
if (newPetIdx < 0 || newPetIdx >= team.PetCount)
|
|
{
|
|
TC_LOG_DEBUG("server.loading", "PetBattle ReplaceFrontPet: REJECTED - invalid index");
|
|
return;
|
|
}
|
|
if (!team.Pets[newPetIdx].IsAlive())
|
|
{
|
|
TC_LOG_DEBUG("server.loading", "PetBattle ReplaceFrontPet: REJECTED - pet {} is dead (HP={})",
|
|
newPetIdx, team.Pets[newPetIdx].Health);
|
|
return;
|
|
}
|
|
|
|
TC_LOG_DEBUG("server.loading", "PetBattle ReplaceFrontPet: Swapping team {} front pet {} -> {}",
|
|
teamIdx, team.FrontPetIndex, newPetIdx);
|
|
|
|
// Set the new front pet and transition state back to ROUND_IN_PROGRESS
|
|
team.FrontPetIndex = newPetIdx;
|
|
team.HasInputThisRound = false;
|
|
battle->ClearNeedsFrontPetSwap(teamIdx);
|
|
battle->SetBattleState(PetBattles::PET_BATTLE_STATE_ROUND_IN_PROGRESS);
|
|
|
|
// Send REPLACEMENTS_MADE to acknowledge the swap
|
|
WorldPackets::BattlePet::PetBattleReplacementsMade replacements;
|
|
replacements.CurRound = battle->GetCurrentRound();
|
|
replacements.NextPetBattleState = static_cast<int8>(PetBattles::PET_BATTLE_STATE_ROUND_IN_PROGRESS);
|
|
for (uint8 i = 0; i < PetBattles::MAX_PET_BATTLE_PLAYERS; ++i)
|
|
{
|
|
replacements.Players[i].NextInputFlags = 0; // Abilities unlocked for next round
|
|
replacements.Players[i].NextTrapStatus = static_cast<int8>(battle->GetTrapStatus(i));
|
|
replacements.Players[i].RoundTimeSecs = 0;
|
|
}
|
|
|
|
// Include a PetSwap effect so client updates the active pet and ability bar
|
|
WorldPackets::BattlePet::PetBattleEffectInfo swapEffect;
|
|
swapEffect.AbilityEffectID = 0;
|
|
swapEffect.Flags = 0;
|
|
swapEffect.SourceAuraInstanceID = 0;
|
|
swapEffect.TurnInstanceID = 0;
|
|
swapEffect.EffectIndex = PetBattles::PET_BATTLE_EFFECT_PET_SWAP; // Wire offset 12 = EffectType, client case 4
|
|
swapEffect.CasterPBOID = static_cast<int32>(teamIdx * PetBattles::MAX_PET_BATTLE_TEAM_SIZE + newPetIdx);
|
|
swapEffect.StackDepth = 0;
|
|
WorldPackets::BattlePet::PetBattleEffectTargetInfo swapTarget;
|
|
swapTarget.Type = 0; // No extra params needed for PetSwap
|
|
swapTarget.Remaining = static_cast<int32>(teamIdx * PetBattles::MAX_PET_BATTLE_TEAM_SIZE + newPetIdx);
|
|
swapEffect.Targets.push_back(swapTarget);
|
|
replacements.Effects.push_back(std::move(swapEffect));
|
|
|
|
if (Player* p1 = battle->GetPlayerForTeam(PetBattles::PET_BATTLE_TEAM_1))
|
|
p1->SendDirectMessage(replacements.Write());
|
|
if (Player* p2 = battle->GetPlayerForTeam(PetBattles::PET_BATTLE_TEAM_2))
|
|
p2->SendDirectMessage(replacements.Write());
|
|
}
|
|
|
|
void WorldSession::HandlePetBattleQuitNotify(WorldPackets::BattlePet::PetBattleQuitNotify& /*petBattleQuitNotify*/)
|
|
{
|
|
Player* player = GetPlayer();
|
|
if (!player)
|
|
return;
|
|
|
|
PetBattles::PetBattle* battle = sPetBattleMgr->GetBattleByPlayer(player->GetGUID());
|
|
if (!battle)
|
|
return;
|
|
|
|
uint32 battleID = battle->GetBattleID();
|
|
|
|
uint8 teamIdx = PetBattles::PET_BATTLE_TEAM_1;
|
|
if (battle->GetTeam(PetBattles::PET_BATTLE_TEAM_2).PlayerGUID == player->GetGUID())
|
|
teamIdx = PetBattles::PET_BATTLE_TEAM_2;
|
|
|
|
battle->Forfeit(teamIdx);
|
|
|
|
// Forfeit sends FinalRound immediately (no death animation delay needed)
|
|
battle->SendFinalRoundPacket(true);
|
|
|
|
// Forfeit completes immediately (sends Finished packet, syncs health, sends journal)
|
|
battle->CompleteBattle();
|
|
|
|
// Remove the battle from tracking maps so player can start a new one
|
|
sPetBattleMgr->RemoveBattle(battleID);
|
|
}
|
|
|
|
void WorldSession::HandlePetBattleFinalNotify(WorldPackets::BattlePet::PetBattleFinalNotify& /*petBattleFinalNotify*/)
|
|
{
|
|
Player* player = GetPlayer();
|
|
if (!player)
|
|
return;
|
|
|
|
PetBattles::PetBattle* battle = sPetBattleMgr->GetBattleByPlayer(player->GetGUID());
|
|
if (!battle || !battle->IsFinalRound())
|
|
return;
|
|
|
|
// Ignore if FinalRound packet hasn't been sent yet (death animation still playing)
|
|
if (battle->HasPendingFinishDelay())
|
|
return;
|
|
|
|
// Transition FINAL_ROUND ? FINISHED (sends Finished packet, syncs health, sends journal)
|
|
battle->CompleteBattle();
|
|
|
|
// Remove the battle from tracking maps so player can start a new one
|
|
sPetBattleMgr->RemoveBattle(battle->GetBattleID());
|
|
}
|
|
|
|
void WorldSession::HandlePetBattleRequestPVP(WorldPackets::BattlePet::PetBattleRequestPVP& petBattleRequestPVP)
|
|
{
|
|
Player* player = GetPlayer();
|
|
if (!player)
|
|
return;
|
|
|
|
// Full validation
|
|
if (!player->IsAlive())
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_NOT_WHILE_DEAD;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
if (player->IsInCombat())
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_NOT_WHILE_IN_COMBAT;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
if (player->IsFlying())
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_NOT_WHILE_FLYING;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
if (sPetBattleMgr->IsPlayerInBattle(player->GetGUID()))
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_IN_BATTLE;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
// Check player has alive pets
|
|
BattlePets::BattlePetMgr* petMgr = GetBattlePetMgr();
|
|
bool hasPet = false;
|
|
for (uint8 i = 0; i < uint8(BattlePets::BattlePetSlot::Count); ++i)
|
|
{
|
|
WorldPackets::BattlePet::BattlePetSlot* slot = petMgr->GetSlot(BattlePets::BattlePetSlot(i));
|
|
if (slot && !slot->Locked && !slot->Pet.Guid.IsEmpty())
|
|
{
|
|
BattlePets::BattlePet* pet = petMgr->GetPet(slot->Pet.Guid);
|
|
if (pet && pet->PacketInfo.Health > 0)
|
|
{
|
|
hasPet = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasPet)
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_INVALID_LOADOUT_ALL_DEAD;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
Player* target = ObjectAccessor::FindPlayer(petBattleRequestPVP.TargetGUID);
|
|
if (!target)
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_TARGET_INVALID;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
if (sPetBattleMgr->IsPlayerInBattle(target->GetGUID()))
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_OPPONENT_NOT_AVAILABLE;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
// Send PVP challenge to target
|
|
WorldPackets::BattlePet::PetBattlePVPChallenge challenge;
|
|
challenge.ChallengerGUID = player->GetGUID();
|
|
challenge.Location.LocationResult = 0;
|
|
challenge.Location.BattleOrigin = player->GetPosition();
|
|
challenge.Location.BattleFacing = player->GetAbsoluteAngle(target);
|
|
challenge.Location.PlayerPositions[0] = player->GetPosition();
|
|
challenge.Location.PlayerPositions[1] = target->GetPosition();
|
|
target->SendDirectMessage(challenge.Write());
|
|
}
|
|
|
|
void WorldSession::HandleJoinPetBattleQueue(WorldPackets::BattlePet::JoinPetBattleQueue& /*joinPetBattleQueue*/)
|
|
{
|
|
Player* player = GetPlayer();
|
|
if (!player)
|
|
return;
|
|
|
|
if (sPetBattleMgr->IsPlayerInBattle(player->GetGUID()))
|
|
{
|
|
WorldPackets::BattlePet::PetBattleQueueStatus status;
|
|
status.Status = 0;
|
|
SendPacket(status.Write());
|
|
return;
|
|
}
|
|
|
|
// Validate that player has at least one alive pet in battle slots
|
|
{
|
|
BattlePets::BattlePetMgr* petMgr = GetBattlePetMgr();
|
|
bool hasPet = false;
|
|
for (uint8 i = 0; i < uint8(BattlePets::BattlePetSlot::Count); ++i)
|
|
{
|
|
WorldPackets::BattlePet::BattlePetSlot* slot = petMgr->GetSlot(BattlePets::BattlePetSlot(i));
|
|
if (slot && !slot->Locked && !slot->Pet.Guid.IsEmpty())
|
|
{
|
|
BattlePets::BattlePet* pet = petMgr->GetPet(slot->Pet.Guid);
|
|
if (pet && pet->PacketInfo.Health > 0)
|
|
{
|
|
hasPet = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!hasPet)
|
|
{
|
|
WorldPackets::BattlePet::PetBattleQueueStatus status;
|
|
status.Status = 0;
|
|
SendPacket(status.Write());
|
|
return;
|
|
}
|
|
}
|
|
|
|
sPetBattleMgr->JoinQueue(player->GetGUID());
|
|
|
|
WorldPackets::BattlePet::PetBattleQueueStatus status;
|
|
status.Status = 1; // Queued
|
|
SendPacket(status.Write());
|
|
}
|
|
|
|
void WorldSession::HandleLeavePetBattleQueue(WorldPackets::BattlePet::LeavePetBattleQueue& /*leavePetBattleQueue*/)
|
|
{
|
|
Player* player = GetPlayer();
|
|
if (!player)
|
|
return;
|
|
|
|
sPetBattleMgr->LeaveQueue(player->GetGUID());
|
|
|
|
WorldPackets::BattlePet::PetBattleQueueStatus status;
|
|
status.Status = 0; // Not in queue
|
|
SendPacket(status.Write());
|
|
}
|
|
|
|
void WorldSession::HandlePetBattleQueueProposeMatchResult(WorldPackets::BattlePet::PetBattleQueueProposeMatchResult& petBattleQueueProposeMatchResult)
|
|
{
|
|
Player* player = GetPlayer();
|
|
if (!player)
|
|
return;
|
|
|
|
sPetBattleMgr->HandleProposalResult(player->GetGUID(), petBattleQueueProposeMatchResult.Accepted);
|
|
}
|
|
|
|
void WorldSession::HandlePetBattleRequestUpdate(WorldPackets::BattlePet::PetBattleRequestUpdate& petBattleRequestUpdate)
|
|
{
|
|
Player* player = GetPlayer();
|
|
if (!player)
|
|
return;
|
|
|
|
// This packet is used for PvP challenge accept/decline
|
|
if (!petBattleRequestUpdate.Canceled)
|
|
{
|
|
// Accept PvP challenge
|
|
Player* challenger = ObjectAccessor::FindPlayer(petBattleRequestUpdate.TargetGUID);
|
|
if (!challenger)
|
|
return;
|
|
|
|
PetBattles::PetBattle* battle = sPetBattleMgr->CreatePvPBattle(challenger, player);
|
|
if (!battle)
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_IN_BATTLE;
|
|
SendPacket(failed.Write());
|
|
return;
|
|
}
|
|
|
|
// Send initial update to both players
|
|
WorldPackets::BattlePet::PetBattleInitialUpdate initialUpdate;
|
|
BuildPetBattlePlayerUpdate(initialUpdate.Players[0], battle->GetTeam(PetBattles::PET_BATTLE_TEAM_1), false, battle, PetBattles::PET_BATTLE_TEAM_1);
|
|
BuildPetBattlePlayerUpdate(initialUpdate.Players[1], battle->GetTeam(PetBattles::PET_BATTLE_TEAM_2), false, battle, PetBattles::PET_BATTLE_TEAM_2);
|
|
BuildPetBattleEnviros(initialUpdate.Enviros, battle);
|
|
initialUpdate.CurRound = battle->GetCurrentRound();
|
|
initialUpdate.CurPetBattleState = static_cast<int8>(battle->GetBattleState());
|
|
initialUpdate.IsPVP = true;
|
|
initialUpdate.CanAwardXP = false;
|
|
initialUpdate.ForfeitPenalty = 10;
|
|
initialUpdate.WaitingForFrontPetsMaxSecs = 30;
|
|
initialUpdate.PvpMaxRoundTime = PetBattles::PET_BATTLE_MAX_ROUND_TIME;
|
|
|
|
challenger->SendDirectMessage(initialUpdate.Write());
|
|
player->SendDirectMessage(initialUpdate.Write());
|
|
|
|
battle->Start();
|
|
}
|
|
else
|
|
{
|
|
// Decline PvP challenge
|
|
Player* challenger = ObjectAccessor::FindPlayer(petBattleRequestUpdate.TargetGUID);
|
|
if (challenger)
|
|
{
|
|
WorldPackets::BattlePet::PetBattleRequestFailed failed;
|
|
failed.Reason = PetBattles::PET_BATTLE_REQUEST_FAIL_DECLINED;
|
|
challenger->SendDirectMessage(failed.Write());
|
|
}
|
|
}
|
|
}
|
|
|
|
void WorldSession::HandlePetBattleScriptErrorNotify(WorldPackets::BattlePet::PetBattleScriptErrorNotify& /*petBattleScriptErrorNotify*/)
|
|
{
|
|
// Client reports script error - log only
|
|
TC_LOG_DEBUG("server.loading", "PetBattleHandler: Client reported pet battle script error");
|
|
}
|
|
|
|
void WorldSession::HandlePetBattleWildLocationFail(WorldPackets::BattlePet::PetBattleWildLocationFail& /*petBattleWildLocationFail*/)
|
|
{
|
|
// Client reports wild battle location failure - log only
|
|
TC_LOG_DEBUG("server.loading", "PetBattleHandler: Client reported wild pet battle location failure");
|
|
}
|