mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
Issue 547. The minigame put the player a guessed distance from wherever the pet had wandered to, in the direction of the player, which often left bricks off screen or the whole minigame over a drop. In the 2014 live captures the positions are the same for every try at the same pet: the pet's destination is where it spawned, and the player is teleported exactly 12 units along the direction the pet spawned facing, turned to face the pet. This matches the spawner in the level file for the Pet Ranch cat (spawned facing -43.4 degrees, player placed at spawn + (-8.24, 8.73) facing 136.6 degrees) and holds for the doberman, buffalo, triceratops, rabbit and the script spawned panda. The pet is now put back on that spot and the player placed that way; the heights come from the navmesh when there is one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1438 lines
41 KiB
C++
1438 lines
41 KiB
C++
#include "PetComponent.h"
|
|
#include "DashboardNotify.h"
|
|
#include "GameMessages.h"
|
|
#include "PetMessages.h"
|
|
#include "PlayerMessages.h"
|
|
#include "InventoryMessages.h"
|
|
#include "EffectsMessages.h"
|
|
#include "ObjectMessages.h"
|
|
#include "BrickDatabase.h"
|
|
#include "CDClientDatabase.h"
|
|
#include "CDTamingBuildPuzzleTable.h"
|
|
#include "ChatPackets.h"
|
|
#include "EntityManager.h"
|
|
#include "Character.h"
|
|
#include "CharacterComponent.h"
|
|
#include "InventoryComponent.h"
|
|
#include "Item.h"
|
|
#include "MissionComponent.h"
|
|
#include "User.h"
|
|
#include "SwitchComponent.h"
|
|
#include "DestroyableComponent.h"
|
|
#include "dpWorld.h"
|
|
#include "UserManager.h"
|
|
#include "PetDigServer.h"
|
|
#include "ObjectIDManager.h"
|
|
#include "eUnequippableActiveType.h"
|
|
#include "eTerminateType.h"
|
|
#include "ePetTamingNotifyType.h"
|
|
#include "eUseItemResponse.h"
|
|
#include "ePlayerFlag.h"
|
|
|
|
#include "GeneralUtils.h"
|
|
#include "Game.h"
|
|
#include "dConfig.h"
|
|
#include "dChatFilter.h"
|
|
#include "Database.h"
|
|
#include "EntityInfo.h"
|
|
#include "eMissionTaskType.h"
|
|
#include "RenderComponent.h"
|
|
#include "eObjectBits.h"
|
|
#include "eGameMasterLevel.h"
|
|
#include "eMissionState.h"
|
|
#include "dNavMesh.h"
|
|
#include "eGameActivity.h"
|
|
#include "eStateChangeType.h"
|
|
|
|
std::unordered_map<LWOOBJID, LWOOBJID> PetComponent::currentActivities{};
|
|
std::unordered_map<LWOOBJID, LWOOBJID> PetComponent::activePets{};
|
|
|
|
/**
|
|
* Maps all the pet lots to a flag indicating that the player has caught it. All basic pets have been guessed by ObjID
|
|
* while the faction ones could be checked using their respective missions.
|
|
*/
|
|
|
|
PetComponent::PetComponent(Entity* parentEntity, const int32_t componentID) : Component{ parentEntity, componentID } {
|
|
m_PetInfo = CDClientManager::GetTable<CDPetComponentTable>()->GetByID(componentID); // TODO: Make reference when safe
|
|
|
|
m_Interaction = LWOOBJID_EMPTY;
|
|
m_Owner = LWOOBJID_EMPTY;
|
|
m_ModerationStatus = 0;
|
|
m_Tamer = LWOOBJID_EMPTY;
|
|
m_ModelId = LWOOBJID_EMPTY;
|
|
m_Timer = 0;
|
|
m_TimerAway = 0;
|
|
m_DatabaseId = LWOOBJID_EMPTY;
|
|
m_Status = 67108866; // Tamable
|
|
m_Ability = ePetAbilityType::Invalid;
|
|
m_StartPosition = NiPoint3Constant::ZERO;
|
|
m_MovementAI = nullptr;
|
|
m_TreasureTime = 0;
|
|
|
|
std::string checkPreconditions = GeneralUtils::UTF16ToWTF8(parentEntity->GetVar<std::u16string>(u"CheckPrecondition"));
|
|
|
|
if (!checkPreconditions.empty()) {
|
|
SetPreconditions(checkPreconditions);
|
|
}
|
|
}
|
|
|
|
void PetComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
|
|
const bool tamed = m_Owner != LWOOBJID_EMPTY;
|
|
|
|
outBitStream.Write1(); // Always serialize as dirty for now
|
|
|
|
outBitStream.Write<uint32_t>(m_Status);
|
|
outBitStream.Write(tamed ? m_Ability : ePetAbilityType::Invalid); // Something with the overhead icon?
|
|
|
|
const bool interacting = m_Interaction != LWOOBJID_EMPTY;
|
|
|
|
outBitStream.Write(interacting);
|
|
if (interacting) {
|
|
outBitStream.Write(m_Interaction);
|
|
}
|
|
|
|
outBitStream.Write(tamed);
|
|
if (tamed) {
|
|
outBitStream.Write(m_Owner);
|
|
}
|
|
|
|
if (bIsInitialUpdate) {
|
|
outBitStream.Write(tamed);
|
|
if (tamed) {
|
|
outBitStream.Write(m_ModerationStatus);
|
|
|
|
const auto nameData = GeneralUtils::UTF8ToUTF16(m_Name);
|
|
const auto ownerNameData = GeneralUtils::UTF8ToUTF16(m_OwnerName);
|
|
|
|
outBitStream.Write<uint8_t>(nameData.size());
|
|
for (const auto c : nameData) {
|
|
outBitStream.Write(c);
|
|
}
|
|
|
|
outBitStream.Write<uint8_t>(ownerNameData.size());
|
|
for (const auto c : ownerNameData) {
|
|
outBitStream.Write(c);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void PetComponent::OnUse(Entity* originator) {
|
|
if (m_Owner != LWOOBJID_EMPTY) {
|
|
// The owner uses their pet with SHIFT or the pet menu's action button: the client sends RequestUse on the pet
|
|
// (LWOPetControlComponent::msgPetCommand, "contextAction")
|
|
if (originator->GetObjectID() == m_Owner) UseAbility();
|
|
return;
|
|
}
|
|
|
|
if (m_Tamer != LWOOBJID_EMPTY) {
|
|
auto* tamer = Game::entityManager->GetEntity(m_Tamer);
|
|
|
|
if (tamer != nullptr) {
|
|
return;
|
|
}
|
|
|
|
m_Tamer = LWOOBJID_EMPTY;
|
|
}
|
|
|
|
auto* const inventoryComponent = originator->GetComponent<InventoryComponent>();
|
|
if (inventoryComponent == nullptr) {
|
|
return;
|
|
}
|
|
|
|
if (m_Preconditions.has_value() && !m_Preconditions->Check(originator, true)) {
|
|
return;
|
|
}
|
|
|
|
auto* const movementAIComponent = m_Parent->GetComponent<MovementAIComponent>();
|
|
if (movementAIComponent != nullptr) {
|
|
movementAIComponent->Stop();
|
|
}
|
|
|
|
inventoryComponent->DespawnPet();
|
|
|
|
const auto* const entry = CDClientManager::GetTable<CDTamingBuildPuzzleTable>()->GetByLOT(m_Parent->GetLOT());
|
|
if (!entry) {
|
|
ChatPackets::SendSystemMessage(originator->GetSystemAddress(), u"Failed to find the puzzle minigame for this pet.");
|
|
return;
|
|
}
|
|
|
|
const auto* const destroyableComponent = originator->GetComponent<DestroyableComponent>();
|
|
if (destroyableComponent == nullptr) {
|
|
return;
|
|
}
|
|
|
|
const auto imagination = destroyableComponent->GetImagination();
|
|
if (imagination < entry->imaginationCost) {
|
|
return;
|
|
}
|
|
|
|
const auto& bricks = BrickDatabase::GetBricks(entry->validPieces);
|
|
if (bricks.empty()) {
|
|
ChatPackets::SendSystemMessage(originator->GetSystemAddress(), u"Failed to load the puzzle minigame for this pet.");
|
|
LOG("Couldn't find %s for minigame!", entry->validPieces.c_str());
|
|
|
|
return;
|
|
}
|
|
|
|
// Where live put the minigame (2014 captures, the same for every try at a pet): the pet goes back to where it
|
|
// spawned, facing the way it spawned, and the player is teleported 12 units in front of it, facing the pet.
|
|
if (m_StartPosition == NiPoint3Constant::ZERO) {
|
|
m_StartPosition = m_Parent->GetPosition();
|
|
m_StartRotation = m_Parent->GetRotation();
|
|
}
|
|
|
|
constexpr float TAMING_PLAYER_DISTANCE = 12.0f;
|
|
|
|
auto petPosition = m_StartPosition;
|
|
auto forward = QuatUtils::Forward(m_StartRotation);
|
|
forward.y = 0;
|
|
forward = forward.SquaredLength() > 0.0f ? forward.Unitize() : NiPoint3Constant::UNIT_Z;
|
|
auto position = petPosition + forward * TAMING_PLAYER_DISTANCE;
|
|
|
|
if (dpWorld::IsLoaded()) {
|
|
petPosition.y = dpWorld::GetNavMesh()->GetHeightAtPoint(petPosition);
|
|
position.y = dpWorld::GetNavMesh()->GetHeightAtPoint(position);
|
|
}
|
|
|
|
const auto rotation = QuatUtils::AxisAngle(NiPoint3Constant::UNIT_Y, glm::pi<float>()) * m_StartRotation;
|
|
|
|
m_Parent->SetPosition(petPosition);
|
|
m_Parent->SetRotation(m_StartRotation);
|
|
|
|
{
|
|
GameMessages::NotifyPetTamingMinigame msg;
|
|
msg.target = originator->GetObjectID();
|
|
msg.PetID = m_Parent->GetObjectID();
|
|
msg.PlayerTamingID = LWOOBJID_EMPTY;
|
|
msg.bForceTeleport = true;
|
|
msg.notifyType = ePetTamingNotifyType::BEGIN;
|
|
msg.petsDestPos = petPosition;
|
|
msg.telePos = position;
|
|
msg.teleRot = rotation;
|
|
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
{
|
|
GameMessages::NotifyPetTamingMinigame msg;
|
|
msg.target = m_Parent->GetObjectID();
|
|
msg.PetID = LWOOBJID_EMPTY;
|
|
msg.PlayerTamingID = originator->GetObjectID();
|
|
msg.bForceTeleport = false;
|
|
msg.notifyType = ePetTamingNotifyType::BEGIN;
|
|
msg.petsDestPos = NiPoint3Constant::ZERO;
|
|
msg.telePos = NiPoint3Constant::ZERO;
|
|
msg.teleRot = NiQuaternion(0.0f, 0.0f, 0.0f, 0.0f);
|
|
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
{
|
|
GameMessages::NotifyPetTamingPuzzleSelected msg;
|
|
msg.target = originator->GetObjectID();
|
|
msg.bricks = bricks;
|
|
msg.Send(originator->GetSystemAddress());
|
|
}
|
|
|
|
m_Tamer = originator->GetObjectID();
|
|
SetStatus(5);
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
currentActivities.insert_or_assign(m_Tamer, m_Parent->GetObjectID());
|
|
|
|
// Notify the start of a pet taming minigame
|
|
m_Parent->GetScript()->OnNotifyPetTamingMinigame(m_Parent, originator, ePetTamingNotifyType::BEGIN);
|
|
|
|
auto* characterComponent = originator->GetComponent<CharacterComponent>();
|
|
if (characterComponent != nullptr) {
|
|
characterComponent->SetCurrentActivity(eGameActivity::PET_TAMING);
|
|
Game::entityManager->SerializeEntity(originator);
|
|
}
|
|
}
|
|
|
|
void PetComponent::Update(float deltaTime) {
|
|
if (m_StartPosition == NiPoint3Constant::ZERO) {
|
|
m_StartPosition = m_Parent->GetPosition();
|
|
m_StartRotation = m_Parent->GetRotation();
|
|
}
|
|
|
|
if (m_Owner == LWOOBJID_EMPTY) {
|
|
if (m_Tamer != LWOOBJID_EMPTY) {
|
|
if (m_Timer > 0) {
|
|
m_Timer -= deltaTime;
|
|
|
|
if (m_Timer <= 0) {
|
|
m_Timer = 0;
|
|
|
|
ClientFailTamingMinigame();
|
|
}
|
|
}
|
|
} else {
|
|
if (m_Timer > 0) {
|
|
m_Timer -= deltaTime;
|
|
|
|
if (m_Timer <= 0) {
|
|
Wander();
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
} else {
|
|
m_Timer = 5;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Going back to the backpack, removed once the despawn effect has played
|
|
if (m_Despawning) return;
|
|
|
|
auto* owner = GetOwner();
|
|
|
|
if (owner == nullptr) {
|
|
m_Parent->Kill();
|
|
|
|
return;
|
|
}
|
|
|
|
m_MovementAI = m_Parent->GetComponent<MovementAIComponent>();
|
|
|
|
if (m_MovementAI == nullptr) {
|
|
return;
|
|
}
|
|
|
|
if (m_TreasureTime > 0) {
|
|
auto* treasure = Game::entityManager->GetEntity(m_AbilityObject);
|
|
|
|
if (treasure == nullptr) {
|
|
m_TreasureTime = 0;
|
|
|
|
return;
|
|
}
|
|
|
|
m_TreasureTime -= deltaTime;
|
|
|
|
m_MovementAI->Stop();
|
|
|
|
if (m_TreasureTime <= 0) {
|
|
m_Parent->SetOwnerOverride(m_Owner);
|
|
|
|
treasure->Smash(m_Parent->GetObjectID());
|
|
|
|
m_AbilityObject = LWOOBJID_EMPTY;
|
|
|
|
m_TreasureTime = 0;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
auto destination = owner->GetPosition();
|
|
NiPoint3 position = m_MovementAI->GetParent()->GetPosition();
|
|
|
|
float distanceToOwner = Vector3::DistanceSquared(position, destination);
|
|
|
|
if (distanceToOwner > 50 * 50 || m_TimerAway > 5) {
|
|
m_MovementAI->Warp(destination);
|
|
|
|
m_Timer = 1;
|
|
m_TimerAway = 0;
|
|
|
|
return;
|
|
}
|
|
|
|
if (distanceToOwner > 15 * 15 || std::abs(destination.y - position.y) >= 3) {
|
|
m_TimerAway += deltaTime;
|
|
} else {
|
|
m_TimerAway = 0;
|
|
}
|
|
|
|
if (m_Timer > 0) {
|
|
m_Timer -= deltaTime;
|
|
|
|
return;
|
|
}
|
|
|
|
SwitchComponent* closestSwitch = SwitchComponent::GetClosestSwitch(position);
|
|
|
|
float haltDistance = 5;
|
|
|
|
Entity* abilityObject = nullptr;
|
|
auto objectAbility = ePetAbilityType::Invalid;
|
|
bool atAbilityObject = false;
|
|
|
|
if (closestSwitch != nullptr && !closestSwitch->GetActive()) {
|
|
NiPoint3 switchPosition = closestSwitch->GetParentEntity()->GetPosition();
|
|
float distance = Vector3::DistanceSquared(position, switchPosition);
|
|
if (distance < 20 * 20) {
|
|
abilityObject = closestSwitch->GetParentEntity();
|
|
objectAbility = ePetAbilityType::JumpOnObject;
|
|
atAbilityObject = distance < 3 * 3;
|
|
|
|
haltDistance = 1;
|
|
|
|
destination = switchPosition;
|
|
}
|
|
}
|
|
|
|
auto* missionComponent = owner->GetComponent<MissionComponent>();
|
|
if (!missionComponent) return;
|
|
|
|
// Determine if the "Lost Tags" mission has been completed and digging has been unlocked
|
|
const bool digUnlocked = missionComponent->GetMissionState(842) == eMissionState::COMPLETE;
|
|
|
|
Entity* closestTreasure = PetDigServer::GetClosestTreasure(position);
|
|
|
|
// Skeleton Dragon Pat special case for bone digging
|
|
if (closestTreasure != nullptr && digUnlocked && (closestTreasure->GetLOT() != 12192 || m_Parent->GetLOT() == 13067)) {
|
|
NiPoint3 treasurePosition = closestTreasure->GetPosition();
|
|
float distance = Vector3::DistanceSquared(position, treasurePosition);
|
|
if (distance < 10 * 10) {
|
|
abilityObject = closestTreasure;
|
|
objectAbility = ePetAbilityType::DigAtPosition;
|
|
atAbilityObject = distance < 5 * 5;
|
|
|
|
haltDistance = 1;
|
|
|
|
destination = treasurePosition;
|
|
}
|
|
}
|
|
|
|
SetAbilityObject(abilityObject, objectAbility, atAbilityObject);
|
|
|
|
m_MovementAI->SetHaltDistance(haltDistance);
|
|
|
|
m_MovementAI->SetMaxSpeed(2.5f);
|
|
|
|
m_MovementAI->SetDestination(destination);
|
|
|
|
m_Timer = 1;
|
|
}
|
|
|
|
void PetComponent::TryBuild(uint32_t numBricks, bool clientFailed) {
|
|
if (m_Tamer == LWOOBJID_EMPTY) return;
|
|
|
|
auto* tamer = Game::entityManager->GetEntity(m_Tamer);
|
|
|
|
if (tamer == nullptr) {
|
|
m_Tamer = LWOOBJID_EMPTY;
|
|
|
|
return;
|
|
}
|
|
|
|
const auto* const entry = CDClientManager::GetTable<CDTamingBuildPuzzleTable>()->GetByLOT(m_Parent->GetLOT());
|
|
if (!entry) return;
|
|
|
|
auto* destroyableComponent = tamer->GetComponent<DestroyableComponent>();
|
|
|
|
if (destroyableComponent == nullptr) return;
|
|
|
|
auto imagination = destroyableComponent->GetImagination();
|
|
|
|
imagination -= entry->imaginationCost;
|
|
|
|
destroyableComponent->SetImagination(imagination);
|
|
|
|
Game::entityManager->SerializeEntity(tamer);
|
|
|
|
if (clientFailed) {
|
|
if (imagination < entry->imaginationCost) {
|
|
ClientFailTamingMinigame();
|
|
}
|
|
} else {
|
|
m_Timer = 0;
|
|
}
|
|
|
|
if (numBricks == 0) return;
|
|
|
|
GameMessages::PetTamingTryBuildResult msg;
|
|
msg.target = m_Tamer;
|
|
msg.bSuccess = !clientFailed;
|
|
msg.iNumCorrect = numBricks;
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
|
|
void PetComponent::NotifyTamingBuildSuccess(NiPoint3 position) {
|
|
if (m_Tamer == LWOOBJID_EMPTY) return;
|
|
|
|
auto* tamer = Game::entityManager->GetEntity(m_Tamer);
|
|
|
|
if (tamer == nullptr) {
|
|
m_Tamer = LWOOBJID_EMPTY;
|
|
|
|
return;
|
|
}
|
|
|
|
const auto* const entry = CDClientManager::GetTable<CDTamingBuildPuzzleTable>()->GetByLOT(m_Parent->GetLOT());
|
|
if (!entry) return;
|
|
|
|
GameMessages::PlayFXEffect(tamer->GetObjectID(), -1, u"petceleb", "").Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
RenderComponent::PlayAnimation(tamer, u"rebuild-celebrate");
|
|
|
|
EntityInfo info{};
|
|
info.lot = entry->puzzleModelLot;
|
|
info.pos = position;
|
|
info.rot = QuatUtils::IDENTITY;
|
|
info.spawnerID = tamer->GetObjectID();
|
|
|
|
auto* modelEntity = Game::entityManager->CreateEntity(info);
|
|
|
|
m_ModelId = modelEntity->GetObjectID();
|
|
|
|
Game::entityManager->ConstructEntity(modelEntity);
|
|
|
|
{
|
|
GameMessages::NotifyTamingModelLoadedOnServer msg;
|
|
msg.target = m_Tamer;
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
|
|
{
|
|
GameMessages::PetResponse msg;
|
|
msg.target = m_Tamer;
|
|
msg.ObjIDPet = m_Parent->GetObjectID();
|
|
msg.iPetCommandType = 0;
|
|
msg.iResponse = 10;
|
|
msg.iTypeID = 0;
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
|
|
auto* inventoryComponent = tamer->GetComponent<InventoryComponent>();
|
|
|
|
if (inventoryComponent == nullptr) {
|
|
return;
|
|
}
|
|
|
|
LWOOBJID petSubKey = ObjectIDManager::GetPersistentID();
|
|
const uint32_t maxTries = 100;
|
|
uint32_t tries = 0;
|
|
while (Database::Get()->GetPetNameInfo(petSubKey) && tries < maxTries) {
|
|
tries++;
|
|
LOG("Found a duplicate pet %llu, getting a new subKey", petSubKey);
|
|
petSubKey = ObjectIDManager::GetPersistentID();
|
|
}
|
|
|
|
if (tries >= maxTries) {
|
|
LOG("Failed to get a unique pet subKey after %i tries, aborting pet creation for player %i", maxTries, tamer->GetCharacter() ? tamer->GetCharacter()->GetID() : -1);
|
|
return;
|
|
}
|
|
|
|
m_DatabaseId = petSubKey;
|
|
|
|
std::string petName = tamer->GetCharacter()->GetName();
|
|
petName += "'s Pet";
|
|
|
|
{
|
|
GameMessages::AddPetToPlayer msg;
|
|
msg.target = m_Tamer;
|
|
msg.iElementalType = 0;
|
|
msg.name = GeneralUtils::UTF8ToUTF16(petName);
|
|
msg.petDBID = petSubKey;
|
|
msg.petLOT = m_Parent->GetLOT();
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
|
|
{
|
|
GameMessages::RegisterPetID msg;
|
|
msg.target = m_Tamer;
|
|
msg.objID = m_Parent->GetObjectID();
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
|
|
{
|
|
GameMessages::RegisterPetDBID msg;
|
|
msg.target = m_Tamer;
|
|
msg.petDBID = petSubKey;
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
|
|
inventoryComponent->AddItem(m_Parent->GetLOT(), 1, eLootSourceType::INVENTORY, eInventoryType::MODELS, {}, LWOOBJID_EMPTY, true, false, petSubKey);
|
|
auto* item = inventoryComponent->FindItemBySubKey(petSubKey, MODELS);
|
|
|
|
if (item == nullptr) {
|
|
return;
|
|
}
|
|
|
|
DatabasePet databasePet{};
|
|
|
|
databasePet.lot = m_Parent->GetLOT();
|
|
databasePet.moderationState = 1;
|
|
databasePet.name = petName;
|
|
|
|
inventoryComponent->SetDatabasePet(petSubKey, databasePet);
|
|
|
|
Activate(item, false, true);
|
|
|
|
m_Timer = 0;
|
|
|
|
{
|
|
GameMessages::NotifyPetTamingMinigame msg;
|
|
msg.target = m_Tamer;
|
|
msg.PetID = LWOOBJID_EMPTY;
|
|
msg.PlayerTamingID = LWOOBJID_EMPTY;
|
|
msg.bForceTeleport = false;
|
|
msg.notifyType = ePetTamingNotifyType::NAMINGPET;
|
|
msg.petsDestPos = NiPoint3Constant::ZERO;
|
|
msg.telePos = NiPoint3Constant::ZERO;
|
|
msg.teleRot = QuatUtils::IDENTITY;
|
|
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
// Triggers the catch a pet missions
|
|
constexpr auto PET_FLAG_BASE = 800;
|
|
tamer->GetCharacter()->SetPlayerFlag(PET_FLAG_BASE + m_ComponentID, true);
|
|
|
|
auto* missionComponent = tamer->GetComponent<MissionComponent>();
|
|
|
|
if (missionComponent != nullptr) {
|
|
missionComponent->Progress(eMissionTaskType::PET_TAMING, m_Parent->GetLOT());
|
|
}
|
|
|
|
SetStatus(1);
|
|
|
|
auto* characterComponent = tamer->GetComponent<CharacterComponent>();
|
|
if (characterComponent != nullptr) {
|
|
characterComponent->UpdatePlayerStatistic(PetsTamed);
|
|
}
|
|
}
|
|
|
|
void PetComponent::RequestSetPetName(std::u16string name) {
|
|
const bool autoRejectNames = UserManager::Instance()->GetMuteAutoRejectNames();
|
|
|
|
if (m_Tamer == LWOOBJID_EMPTY) {
|
|
if (m_Owner != LWOOBJID_EMPTY) {
|
|
auto* owner = GetOwner();
|
|
|
|
// If auto reject names is on, and the user is muted, force use of predefined name
|
|
if (autoRejectNames && owner && owner->GetCharacter() && owner->GetCharacter()->GetParentUser()->GetIsMuted()) {
|
|
m_ModerationStatus = 2; // Approved
|
|
std::string forcedName = "Pet";
|
|
Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ forcedName, static_cast<int32_t>(m_ModerationStatus), m_Owner, m_Parent->GetLOT() });
|
|
DashboardNotify::Changed("pet_names", m_DatabaseId);
|
|
{
|
|
GameMessages::SetPetName msg;
|
|
msg.target = m_Owner;
|
|
msg.name = GeneralUtils::UTF8ToUTF16(m_Name);
|
|
msg.petDBID = m_DatabaseId;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
{
|
|
GameMessages::SetPetNameModerated msg;
|
|
msg.target = m_Owner;
|
|
msg.PetDBID = m_DatabaseId;
|
|
msg.nModerationStatus = m_ModerationStatus;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
} else {
|
|
m_ModerationStatus = 1; // Pending
|
|
m_Name = "";
|
|
|
|
//Save our pet's new name to the db:
|
|
SetPetNameForModeration(GeneralUtils::UTF16ToWTF8(name), m_Owner);
|
|
|
|
{
|
|
GameMessages::SetPetName msg;
|
|
msg.target = m_Owner;
|
|
msg.name = GeneralUtils::UTF8ToUTF16(m_Name);
|
|
msg.petDBID = m_DatabaseId;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
{
|
|
GameMessages::SetPetNameModerated msg;
|
|
msg.target = m_Owner;
|
|
msg.PetDBID = m_DatabaseId;
|
|
msg.nModerationStatus = m_ModerationStatus;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
auto* tamer = Game::entityManager->GetEntity(m_Tamer);
|
|
|
|
if (tamer == nullptr) {
|
|
m_Tamer = LWOOBJID_EMPTY;
|
|
|
|
return;
|
|
}
|
|
|
|
LOG("Got set pet name (%s)", GeneralUtils::UTF16ToWTF8(name).c_str());
|
|
|
|
auto* inventoryComponent = tamer->GetComponent<InventoryComponent>();
|
|
|
|
if (inventoryComponent == nullptr) {
|
|
return;
|
|
}
|
|
|
|
// If auto reject names is on, and the user is muted, force use of predefined name ELSE proceed with normal name check
|
|
if (autoRejectNames && tamer->GetCharacter() && tamer->GetCharacter()->GetParentUser()->GetIsMuted()) {
|
|
m_ModerationStatus = 2; // Approved
|
|
m_Name = "";
|
|
std::string forcedName = "Pet";
|
|
|
|
Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ forcedName, static_cast<int32_t>(m_ModerationStatus), m_Tamer, m_Parent->GetLOT() });
|
|
DashboardNotify::Changed("pet_names", m_DatabaseId);
|
|
LOG("AccountID: %i is muted, forcing use of predefined pet name", tamer->GetCharacter()->GetParentUser()->GetAccountID());
|
|
} else {
|
|
m_ModerationStatus = 1; // Pending
|
|
m_Name = "";
|
|
|
|
//Save our pet's new name to the db:
|
|
SetPetNameForModeration(GeneralUtils::UTF16ToWTF8(name), m_Tamer);
|
|
}
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
std::u16string u16name = GeneralUtils::UTF8ToUTF16(m_Name);
|
|
std::u16string u16ownerName = GeneralUtils::UTF8ToUTF16(m_OwnerName);
|
|
{
|
|
GameMessages::SetPetName msg;
|
|
msg.target = m_Tamer;
|
|
msg.name = u16name;
|
|
msg.petDBID = m_DatabaseId;
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
{
|
|
GameMessages::SetPetName msg;
|
|
msg.target = m_Tamer;
|
|
msg.name = u16name;
|
|
msg.petDBID = LWOOBJID_EMPTY;
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
{
|
|
GameMessages::PetNameChanged msg;
|
|
msg.target = m_Parent->GetObjectID();
|
|
msg.moderationStatus = m_ModerationStatus;
|
|
msg.name = u16name;
|
|
msg.ownerName = u16ownerName;
|
|
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
{
|
|
GameMessages::SetPetNameModerated msg;
|
|
msg.target = m_Tamer;
|
|
msg.PetDBID = m_DatabaseId;
|
|
msg.nModerationStatus = m_ModerationStatus;
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
|
|
{
|
|
GameMessages::NotifyPetTamingMinigame msg;
|
|
msg.target = m_Tamer;
|
|
msg.PetID = m_Parent->GetObjectID();
|
|
msg.PlayerTamingID = m_Tamer;
|
|
msg.bForceTeleport = false;
|
|
msg.notifyType = ePetTamingNotifyType::SUCCESS;
|
|
msg.petsDestPos = NiPoint3Constant::ZERO;
|
|
msg.telePos = NiPoint3Constant::ZERO;
|
|
msg.teleRot = QuatUtils::IDENTITY;
|
|
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
auto* characterComponent = tamer->GetComponent<CharacterComponent>();
|
|
if (characterComponent != nullptr) {
|
|
characterComponent->SetCurrentActivity(eGameActivity::NONE);
|
|
Game::entityManager->SerializeEntity(tamer);
|
|
}
|
|
GameMessages::TerminateInteraction(m_Tamer, eTerminateType::FROM_INTERACTION, m_Parent->GetObjectID()).Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
auto* modelEntity = Game::entityManager->GetEntity(m_ModelId);
|
|
|
|
if (modelEntity != nullptr) {
|
|
modelEntity->Smash(m_Tamer);
|
|
}
|
|
|
|
currentActivities.erase(m_Tamer);
|
|
|
|
m_Tamer = LWOOBJID_EMPTY;
|
|
|
|
// Notify the end of a pet taming minigame
|
|
m_Parent->GetScript()->OnNotifyPetTamingMinigame(m_Parent, tamer, ePetTamingNotifyType::SUCCESS);
|
|
}
|
|
|
|
void PetComponent::ClientExitTamingMinigame(bool voluntaryExit) {
|
|
if (m_Tamer == LWOOBJID_EMPTY) return;
|
|
|
|
auto* tamer = Game::entityManager->GetEntity(m_Tamer);
|
|
|
|
if (tamer == nullptr) {
|
|
m_Tamer = LWOOBJID_EMPTY;
|
|
|
|
return;
|
|
}
|
|
|
|
{
|
|
GameMessages::NotifyPetTamingMinigame msg;
|
|
msg.target = m_Tamer;
|
|
msg.PetID = m_Parent->GetObjectID();
|
|
msg.PlayerTamingID = m_Tamer;
|
|
msg.bForceTeleport = false;
|
|
msg.notifyType = ePetTamingNotifyType::QUIT;
|
|
msg.petsDestPos = NiPoint3Constant::ZERO;
|
|
msg.telePos = NiPoint3Constant::ZERO;
|
|
msg.teleRot = QuatUtils::IDENTITY;
|
|
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
auto* characterComponent = tamer->GetComponent<CharacterComponent>();
|
|
if (characterComponent != nullptr) {
|
|
characterComponent->SetCurrentActivity(eGameActivity::NONE);
|
|
Game::entityManager->SerializeEntity(tamer);
|
|
}
|
|
{
|
|
GameMessages::NotifyTamingModelLoadedOnServer msg;
|
|
msg.target = m_Tamer;
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
|
|
GameMessages::TerminateInteraction(m_Tamer, eTerminateType::FROM_INTERACTION, m_Parent->GetObjectID()).Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
currentActivities.erase(m_Tamer);
|
|
|
|
SetStatus(67108866);
|
|
m_Tamer = LWOOBJID_EMPTY;
|
|
m_Timer = 0;
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
// Notify the end of a pet taming minigame
|
|
m_Parent->GetScript()->OnNotifyPetTamingMinigame(m_Parent, tamer, ePetTamingNotifyType::QUIT);
|
|
}
|
|
|
|
void PetComponent::StartTimer() {
|
|
const auto* const entry = CDClientManager::GetTable<CDTamingBuildPuzzleTable>()->GetByLOT(m_Parent->GetLOT());
|
|
if (!entry) return;
|
|
|
|
m_Timer = entry->timeLimit;
|
|
}
|
|
|
|
void PetComponent::ClientFailTamingMinigame() {
|
|
if (m_Tamer == LWOOBJID_EMPTY) return;
|
|
|
|
auto* tamer = Game::entityManager->GetEntity(m_Tamer);
|
|
|
|
if (tamer == nullptr) {
|
|
m_Tamer = LWOOBJID_EMPTY;
|
|
|
|
return;
|
|
}
|
|
|
|
{
|
|
GameMessages::NotifyPetTamingMinigame msg;
|
|
msg.target = m_Tamer;
|
|
msg.PetID = m_Parent->GetObjectID();
|
|
msg.PlayerTamingID = m_Tamer;
|
|
msg.bForceTeleport = false;
|
|
msg.notifyType = ePetTamingNotifyType::FAILED;
|
|
msg.petsDestPos = NiPoint3Constant::ZERO;
|
|
msg.telePos = NiPoint3Constant::ZERO;
|
|
msg.teleRot = QuatUtils::IDENTITY;
|
|
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
auto* characterComponent = tamer->GetComponent<CharacterComponent>();
|
|
if (characterComponent != nullptr) {
|
|
characterComponent->SetCurrentActivity(eGameActivity::NONE);
|
|
Game::entityManager->SerializeEntity(tamer);
|
|
}
|
|
{
|
|
GameMessages::NotifyTamingModelLoadedOnServer msg;
|
|
msg.target = m_Tamer;
|
|
msg.Send(tamer->GetSystemAddress());
|
|
}
|
|
|
|
GameMessages::TerminateInteraction(m_Tamer, eTerminateType::FROM_INTERACTION, m_Parent->GetObjectID()).Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
currentActivities.erase(m_Tamer);
|
|
|
|
SetStatus(67108866);
|
|
m_Tamer = LWOOBJID_EMPTY;
|
|
m_Timer = 0;
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
// Notify the end of a pet taming minigame
|
|
m_Parent->GetScript()->OnNotifyPetTamingMinigame(m_Parent, tamer, ePetTamingNotifyType::FAILED);
|
|
}
|
|
|
|
void PetComponent::Wander() {
|
|
m_MovementAI = m_Parent->GetComponent<MovementAIComponent>();
|
|
|
|
if (m_MovementAI == nullptr || !m_MovementAI->AtFinalWaypoint()) {
|
|
return;
|
|
}
|
|
|
|
m_MovementAI->SetHaltDistance(0);
|
|
|
|
const auto& info = m_MovementAI->GetInfo();
|
|
|
|
const auto div = static_cast<int>(info.wanderDelayMax);
|
|
m_Timer = (div == 0 ? 0 : GeneralUtils::GenerateRandomNumber<int>(0, div)) + info.wanderDelayMin; //set a random timer to stay put.
|
|
|
|
const float radius = info.wanderRadius * sqrt(static_cast<double>(GeneralUtils::GenerateRandomNumber<float>(0, 1))); //our wander radius + a bit of random range
|
|
const float theta = ((static_cast<double>(GeneralUtils::GenerateRandomNumber<float>(0, 1)) * 2 * PI));
|
|
|
|
const NiPoint3 delta =
|
|
{
|
|
radius * cos(theta),
|
|
0,
|
|
radius * sin(theta)
|
|
};
|
|
|
|
auto destination = m_StartPosition + delta;
|
|
|
|
if (dpWorld::IsLoaded()) {
|
|
destination.y = dpWorld::GetNavMesh()->GetHeightAtPoint(destination);
|
|
}
|
|
|
|
if (Vector3::DistanceSquared(destination, m_MovementAI->GetParent()->GetPosition()) < 2 * 2) {
|
|
m_MovementAI->Stop();
|
|
|
|
return;
|
|
}
|
|
|
|
m_MovementAI->SetMaxSpeed(m_PetInfo.sprintSpeed);
|
|
|
|
m_MovementAI->SetDestination(destination);
|
|
|
|
m_Timer += (m_MovementAI->GetParent()->GetPosition().x - destination.x) / m_PetInfo.sprintSpeed;
|
|
}
|
|
|
|
void PetComponent::Activate(Item* item, bool registerPet, bool fromTaming) {
|
|
m_ItemId = item->GetId();
|
|
m_DatabaseId = item->GetSubKey();
|
|
|
|
auto* inventoryComponent = item->GetInventory()->GetComponent();
|
|
|
|
if (inventoryComponent == nullptr) return;
|
|
|
|
inventoryComponent->DespawnPet();
|
|
|
|
m_Owner = inventoryComponent->GetParent()->GetObjectID();
|
|
AddDrainImaginationTimer(fromTaming);
|
|
|
|
auto* owner = GetOwner();
|
|
|
|
if (owner == nullptr) return;
|
|
|
|
if (registerPet) {
|
|
// Summoned from the backpack. Live constructed the pet in the spawning state, so the client plays its
|
|
// spawn animation, played the pet's "despawn" effect (the circles and stars) and then cleared the state.
|
|
SetStatus(1 | PET_STATE_SPAWNING);
|
|
m_Parent->AddCallbackTimer(RenderComponent::GetAnimationTime(m_Parent, "spawn"), [this]() {
|
|
GameMessages::PlayFXEffect(m_Parent->GetObjectID(), -1, u"despawn", "").Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
SetStatus(m_Status & ~PET_STATE_SPAWNING);
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
});
|
|
} else {
|
|
SetStatus(1);
|
|
}
|
|
|
|
auto databaseData = inventoryComponent->GetDatabasePet(m_DatabaseId);
|
|
|
|
m_ModerationStatus = databaseData.moderationState;
|
|
|
|
bool updatedModerationStatus = false;
|
|
|
|
//Load mod status from db:
|
|
if (m_ModerationStatus != 2) {
|
|
LoadPetNameFromModeration();
|
|
|
|
databaseData.name = m_Name;
|
|
databaseData.moderationState = m_ModerationStatus;
|
|
|
|
inventoryComponent->SetDatabasePet(m_DatabaseId, databaseData);
|
|
|
|
updatedModerationStatus = true;
|
|
} else {
|
|
m_Name = databaseData.name;
|
|
}
|
|
|
|
m_OwnerName = owner->GetCharacter()->GetName();
|
|
|
|
if (updatedModerationStatus) {
|
|
{
|
|
GameMessages::SetPetName msg;
|
|
msg.target = m_Owner;
|
|
msg.name = GeneralUtils::UTF8ToUTF16(m_Name);
|
|
msg.petDBID = m_DatabaseId;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
{
|
|
GameMessages::SetPetNameModerated msg;
|
|
msg.target = m_Owner;
|
|
msg.PetDBID = m_DatabaseId;
|
|
msg.nModerationStatus = m_ModerationStatus;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
}
|
|
|
|
GameMessages::MarkInventoryItemAsActive markActive;
|
|
markActive.target = m_Owner;
|
|
markActive.bActive = true;
|
|
markActive.iType = eUnequippableActiveType::PET;
|
|
markActive.itemID = m_ItemId;
|
|
markActive.Send(GetOwner()->GetSystemAddress());
|
|
|
|
activePets[m_Owner] = m_Parent->GetObjectID();
|
|
|
|
m_Timer = 3;
|
|
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
owner->GetCharacter()->SetPlayerFlag(ePlayerFlag::FIRST_MANUAL_PET_HIBERNATE, true);
|
|
|
|
if (registerPet) {
|
|
{
|
|
GameMessages::AddPetToPlayer msg;
|
|
msg.target = m_Owner;
|
|
msg.iElementalType = 0;
|
|
msg.name = GeneralUtils::UTF8ToUTF16(m_Name);
|
|
msg.petDBID = m_DatabaseId;
|
|
msg.petLOT = m_Parent->GetLOT();
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
|
|
{
|
|
GameMessages::RegisterPetID msg;
|
|
msg.target = m_Owner;
|
|
msg.objID = m_Parent->GetObjectID();
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
|
|
{
|
|
GameMessages::RegisterPetDBID msg;
|
|
msg.target = m_Owner;
|
|
msg.petDBID = m_DatabaseId;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
}
|
|
}
|
|
|
|
void PetComponent::AddDrainImaginationTimer(bool fromTaming) {
|
|
if (Game::config->GetValue("pets_take_imagination") != "1") return;
|
|
|
|
auto* playerEntity = Game::entityManager->GetEntity(m_Owner);
|
|
if (!playerEntity) {
|
|
LOG("owner was null or didnt exist!");
|
|
return;
|
|
}
|
|
|
|
auto playerDestroyableComponent = playerEntity->GetComponent<DestroyableComponent>();
|
|
if (!playerDestroyableComponent) return;
|
|
|
|
// Drain by 1 when you summon pet or when this method is called, but not when we have just tamed this pet.
|
|
if (!fromTaming) playerDestroyableComponent->Imagine(-1);
|
|
|
|
// Set this to a variable so when this is called back from the player the timer doesn't fire off.
|
|
m_Parent->AddCallbackTimer(m_PetInfo.imaginationDrainRate, [this]() {
|
|
const auto* owner = Game::entityManager->GetEntity(m_Owner);
|
|
if (!owner) {
|
|
LOG("owner was null or didnt exist!");
|
|
return;
|
|
}
|
|
|
|
const auto* playerDestroyableComponent = owner->GetComponent<DestroyableComponent>();
|
|
if (!playerDestroyableComponent) return;
|
|
|
|
// Already going back to the backpack
|
|
if (m_Despawning) return;
|
|
|
|
// If we are out of imagination despawn the pet.
|
|
if (playerDestroyableComponent->GetImagination() == 0) {
|
|
this->Deactivate();
|
|
auto playerEntity = playerDestroyableComponent->GetParent();
|
|
if (!playerEntity) return;
|
|
|
|
GameMessages::UseItemRequirementsResponse requirementsResponse;
|
|
requirementsResponse.target = playerEntity->GetObjectID();
|
|
requirementsResponse.eUseResponse = eUseItemResponse::NoImaginationForPet;
|
|
requirementsResponse.SendToClient(playerEntity->GetSystemAddress());
|
|
// The pet is gone: no more draining
|
|
return;
|
|
}
|
|
|
|
this->AddDrainImaginationTimer();
|
|
});
|
|
}
|
|
|
|
void PetComponent::Deactivate() {
|
|
if (m_Despawning) return;
|
|
m_Despawning = true;
|
|
|
|
GameMessages::PlayFXEffect(m_Parent->GetObjectID(), -1, u"despawn", "").Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
|
|
activePets.erase(m_Owner);
|
|
|
|
auto* const movementAI = m_Parent->GetComponent<MovementAIComponent>();
|
|
if (movementAI) movementAI->Stop();
|
|
|
|
// Live removed the pet only once its despawn effect had played; removing it at once cut the effect short
|
|
m_Parent->AddCallbackTimer(RenderComponent::GetAnimationTime(m_Parent, "despawn"), [this]() {
|
|
m_Parent->Kill();
|
|
});
|
|
|
|
auto* owner = GetOwner();
|
|
|
|
if (owner == nullptr) return;
|
|
|
|
// The client looks the item up by this ID to grey it out again (LWOInventoryComponent_Common::SendMessage), and
|
|
// items get new IDs when they move, so use the ID the pet's item has now
|
|
auto* const inventoryComponent = owner->GetComponent<InventoryComponent>();
|
|
const auto* const item = inventoryComponent ? inventoryComponent->FindItemBySubKey(m_DatabaseId) : nullptr;
|
|
|
|
GameMessages::MarkInventoryItemAsActive markActive;
|
|
markActive.target = m_Owner;
|
|
markActive.bActive = false;
|
|
markActive.iType = eUnequippableActiveType::PET;
|
|
markActive.itemID = item ? item->GetId() : m_ItemId;
|
|
markActive.Send(owner->GetSystemAddress());
|
|
|
|
{
|
|
GameMessages::RegisterPetID msg;
|
|
msg.target = m_Owner;
|
|
msg.objID = LWOOBJID_EMPTY;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
|
|
{
|
|
GameMessages::RegisterPetDBID msg;
|
|
msg.target = m_Owner;
|
|
msg.petDBID = LWOOBJID_EMPTY;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
|
|
{
|
|
GameMessages::ShowPetActionButton msg;
|
|
msg.target = m_Owner;
|
|
msg.ButtonLabel = ePetAbilityType::Invalid;
|
|
msg.bShow = false;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
}
|
|
|
|
void PetComponent::Release() {
|
|
auto* const owner = GetOwner();
|
|
if (!owner) return;
|
|
auto* const inventoryComponent = owner->GetComponent<InventoryComponent>();
|
|
|
|
if (inventoryComponent == nullptr) {
|
|
return;
|
|
}
|
|
|
|
Deactivate();
|
|
|
|
inventoryComponent->RemoveDatabasePet(m_DatabaseId);
|
|
|
|
auto* const item = inventoryComponent->FindItemBySubKey(m_DatabaseId);
|
|
|
|
if (item) item->SetCount(0, false, false);
|
|
}
|
|
|
|
void PetComponent::Command(const NiPoint3& position, const LWOOBJID source, const int32_t commandType, const int32_t typeId, const bool overrideObey) {
|
|
auto* owner = GetOwner();
|
|
if (!owner) return;
|
|
|
|
if (commandType == 1) {
|
|
// Emotes
|
|
GameMessages::PlayEmote playEmote;
|
|
playEmote.target = m_Parent->GetObjectID();
|
|
playEmote.emoteID = typeId;
|
|
playEmote.targetID = owner->GetObjectID();
|
|
playEmote.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
GameMessages::EmotePlayed msg;
|
|
msg.target = owner->GetObjectID();
|
|
msg.emoteID = typeId;
|
|
msg.targetID = 0; // Or set to the intended target entity's ID, or 0 if no target
|
|
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
} else if (commandType == 3) {
|
|
// Follow me, ???
|
|
} else if (commandType == 6) {
|
|
// TODO: Go to player
|
|
}
|
|
|
|
if (owner->GetGMLevel() >= eGameMasterLevel::DEVELOPER) {
|
|
ChatPackets::SendSystemMessage(owner->GetSystemAddress(), u"Commmand Type: " + (GeneralUtils::to_u16string(commandType)) + u" - Type Id: " + (GeneralUtils::to_u16string(typeId)));
|
|
}
|
|
}
|
|
|
|
void PetComponent::SetAbilityObject(Entity* object, const ePetAbilityType ability, const bool atObject) {
|
|
const auto objectId = object ? object->GetObjectID() : LWOOBJID_EMPTY;
|
|
const auto status = m_Status & ~(PET_STATE_AT_ABILITY_OBJECT | PET_STATE_USING_ABILITY | PET_STATE_GOING_TO_ABILITY_OBJECT);
|
|
|
|
// What live did (2014 captures): on the way the pet has state 0x500 and the Go To Object ability, the tutorial
|
|
// for the object is shown; at the object it has 0x120 and the object's ability, and the owner gets the button.
|
|
if (objectId != m_AbilityObject) {
|
|
HideAbilityButton();
|
|
m_AbilityObject = objectId;
|
|
|
|
if (!object) {
|
|
m_Ability = ePetAbilityType::Invalid;
|
|
SetStatus(status);
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
return;
|
|
}
|
|
|
|
m_Ability = ePetAbilityType::GoToObject;
|
|
SetStatus(status | PET_STATE_USING_ABILITY | PET_STATE_GOING_TO_ABILITY_OBJECT);
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
if (ability == ePetAbilityType::DigAtPosition) SendHelp(eHelpType::PR_DIG_TUTORIAL_01);
|
|
else if (ability == ePetAbilityType::JumpOnObject) SendHelp(eHelpType::PR_BOUNCER_TUTORIAL_01);
|
|
}
|
|
|
|
if (!atObject || m_AbilityButtonShown) return;
|
|
|
|
m_Ability = ability;
|
|
SetStatus(status | PET_STATE_USING_ABILITY | PET_STATE_AT_ABILITY_OBJECT);
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
if (ability == ePetAbilityType::JumpOnObject) {
|
|
// On the pet switch, as live did: the pet gets excited and the switch engages
|
|
RenderComponent::PlayAnimation(m_Parent, u"excited");
|
|
RenderComponent::PlayAnimation(object, u"engaged");
|
|
SendHelp(eHelpType::PR_BOUNCER_TUTORIAL_03);
|
|
}
|
|
|
|
auto* const owner = GetOwner();
|
|
if (!owner) return;
|
|
|
|
GameMessages::ShowPetActionButton msg;
|
|
msg.target = m_Owner;
|
|
msg.ButtonLabel = ability;
|
|
msg.bShow = true;
|
|
msg.Send(owner->GetSystemAddress());
|
|
m_AbilityButtonShown = true;
|
|
}
|
|
|
|
void PetComponent::UseAbility() {
|
|
if (!m_AbilityButtonShown || m_Despawning) return;
|
|
|
|
auto* const object = Game::entityManager->GetEntity(m_AbilityObject);
|
|
auto* const owner = GetOwner();
|
|
if (!object || !owner) return;
|
|
|
|
auto* const destroyableComponent = owner->GetComponent<DestroyableComponent>();
|
|
if (!destroyableComponent) return;
|
|
|
|
// Using the ability costs imagination (PetAbilities: 2 to jump on a pet switch, 1 to dig)
|
|
const auto cost = GetAbilityImaginationCost(m_Ability);
|
|
if (destroyableComponent->GetImagination() < cost) return;
|
|
destroyableComponent->Imagine(-cost);
|
|
|
|
const auto ability = m_Ability;
|
|
HideAbilityButton();
|
|
|
|
if (ability == ePetAbilityType::DigAtPosition) {
|
|
SendHelp(eHelpType::PR_DIG_TUTORIAL_03);
|
|
|
|
// Live dropped the ability states as soon as the owner had the pet dig
|
|
m_Ability = ePetAbilityType::Invalid;
|
|
SetStatus(m_Status & ~(PET_STATE_AT_ABILITY_OBJECT | PET_STATE_USING_ABILITY | PET_STATE_GOING_TO_ABILITY_OBJECT));
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
Command(NiPoint3Constant::ZERO, LWOOBJID_EMPTY, 1, 202, true);
|
|
|
|
m_TreasureTime = 2;
|
|
} else if (ability == ePetAbilityType::JumpOnObject) {
|
|
// The pet jumps on the switch, which turns its bouncer on; the pet is done with the switch then
|
|
m_Ability = ePetAbilityType::Invalid;
|
|
m_AbilityObject = LWOOBJID_EMPTY;
|
|
SetStatus(m_Status & ~(PET_STATE_AT_ABILITY_OBJECT | PET_STATE_USING_ABILITY | PET_STATE_GOING_TO_ABILITY_OBJECT));
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
|
|
RenderComponent::PlayAnimation(m_Parent, u"jump");
|
|
SendHelp(eHelpType::PR_TOOLTIP_1ST_PET_JUMPED_ON_SWITCH);
|
|
|
|
auto* const switchComponent = object->GetComponent<SwitchComponent>();
|
|
if (switchComponent) switchComponent->OnUse(m_Parent);
|
|
}
|
|
}
|
|
|
|
void PetComponent::HideAbilityButton() {
|
|
if (!m_AbilityButtonShown) return;
|
|
m_AbilityButtonShown = false;
|
|
|
|
auto* const owner = GetOwner();
|
|
if (!owner) return;
|
|
|
|
GameMessages::ShowPetActionButton msg;
|
|
msg.target = m_Owner;
|
|
msg.ButtonLabel = m_Ability;
|
|
msg.bShow = false;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
|
|
void PetComponent::SendHelp(const eHelpType helpId) const {
|
|
// Sent every time, as live did; the client shows each tooltip only once
|
|
auto* const owner = GetOwner();
|
|
if (!owner) return;
|
|
|
|
GameMessages::Help msg;
|
|
msg.target = m_Owner;
|
|
msg.helpId = helpId;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
|
|
int32_t PetComponent::GetAbilityImaginationCost(const ePetAbilityType ability) {
|
|
auto query = CDClientDatabase::CreatePreppedStmt("SELECT ImaginationCost FROM PetAbilities WHERE id = ?;");
|
|
query.bind(1, static_cast<int32_t>(ability));
|
|
|
|
auto result = query.execQuery();
|
|
if (result.eof() || result.fieldIsNull("ImaginationCost")) return 0;
|
|
|
|
return result.getIntField("ImaginationCost");
|
|
}
|
|
|
|
LWOOBJID PetComponent::GetOwnerId() const {
|
|
return m_Owner;
|
|
}
|
|
|
|
Entity* PetComponent::GetOwner() const {
|
|
return Game::entityManager->GetEntity(m_Owner);
|
|
}
|
|
|
|
LWOOBJID PetComponent::GetDatabaseId() const {
|
|
return m_DatabaseId;
|
|
}
|
|
|
|
LWOOBJID PetComponent::GetInteraction() const {
|
|
return m_Interaction;
|
|
}
|
|
|
|
LWOOBJID PetComponent::GetItemId() const {
|
|
return m_ItemId;
|
|
}
|
|
|
|
uint32_t PetComponent::GetStatus() const {
|
|
return m_Status;
|
|
}
|
|
|
|
ePetAbilityType PetComponent::GetAbility() const {
|
|
return m_Ability;
|
|
}
|
|
|
|
void PetComponent::SetInteraction(LWOOBJID value) {
|
|
m_Interaction = value;
|
|
}
|
|
|
|
void PetComponent::SetStatus(uint32_t value) {
|
|
m_Status = value;
|
|
}
|
|
|
|
void PetComponent::SetAbility(ePetAbilityType value) {
|
|
m_Ability = value;
|
|
}
|
|
|
|
PetComponent* PetComponent::GetTamingPet(LWOOBJID tamer) {
|
|
const auto& pair = currentActivities.find(tamer);
|
|
|
|
if (pair == currentActivities.end()) {
|
|
return nullptr;
|
|
}
|
|
|
|
auto* entity = Game::entityManager->GetEntity(pair->second);
|
|
|
|
if (entity == nullptr) {
|
|
currentActivities.erase(tamer);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
return entity->GetComponent<PetComponent>();
|
|
}
|
|
|
|
PetComponent* PetComponent::GetActivePet(LWOOBJID owner) {
|
|
const auto& pair = activePets.find(owner);
|
|
|
|
if (pair == activePets.end()) {
|
|
return nullptr;
|
|
}
|
|
|
|
auto* entity = Game::entityManager->GetEntity(pair->second);
|
|
|
|
if (entity == nullptr) {
|
|
activePets.erase(owner);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
return entity->GetComponent<PetComponent>();
|
|
}
|
|
|
|
Entity* PetComponent::GetParentEntity() const {
|
|
return m_Parent;
|
|
}
|
|
|
|
PetComponent::~PetComponent() {
|
|
m_Owner = LWOOBJID_EMPTY;
|
|
}
|
|
|
|
void PetComponent::SetPetNameForModeration(const std::string& petName, const LWOOBJID owner) {
|
|
int approved = 1; //default, in mod
|
|
|
|
//Make sure that the name isn't already auto-approved:
|
|
if (Game::chatFilter->IsSentenceOkay(petName, eGameMasterLevel::CIVILIAN).empty()) {
|
|
approved = 2; //approved
|
|
}
|
|
|
|
//Save to db:
|
|
Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ petName, approved, owner, m_Parent->GetLOT() });
|
|
DashboardNotify::Changed("pet_names", m_DatabaseId);
|
|
}
|
|
|
|
void PetComponent::LoadPetNameFromModeration() {
|
|
auto petNameInfo = Database::Get()->GetPetNameInfo(m_DatabaseId);
|
|
if (petNameInfo) {
|
|
m_ModerationStatus = petNameInfo->approvalStatus;
|
|
if (m_ModerationStatus == 2) {
|
|
m_Name = petNameInfo->petName;
|
|
}
|
|
}
|
|
}
|
|
|
|
void PetComponent::SetPreconditions(const std::string& preconditions) {
|
|
m_Preconditions = std::make_optional<PreconditionExpression>(preconditions);
|
|
}
|
|
|
|
void PetComponent::ApplyNameModeration(const bool approved) {
|
|
auto* owner = GetOwner();
|
|
if (approved) {
|
|
LoadPetNameFromModeration();
|
|
} else {
|
|
// Rejected names are removed; the pet goes back to having no name until a new one is chosen
|
|
m_ModerationStatus = 0;
|
|
m_Name = "";
|
|
}
|
|
if (owner) {
|
|
if (auto* inventoryComponent = owner->GetComponent<InventoryComponent>()) {
|
|
auto data = inventoryComponent->GetDatabasePet(m_DatabaseId);
|
|
data.name = m_Name;
|
|
data.moderationState = m_ModerationStatus;
|
|
inventoryComponent->SetDatabasePet(m_DatabaseId, data);
|
|
}
|
|
{
|
|
GameMessages::SetPetName msg;
|
|
msg.target = m_Owner;
|
|
msg.name = GeneralUtils::UTF8ToUTF16(m_Name);
|
|
msg.petDBID = m_DatabaseId;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
{
|
|
GameMessages::SetPetNameModerated msg;
|
|
msg.target = m_Owner;
|
|
msg.PetDBID = m_DatabaseId;
|
|
msg.nModerationStatus = m_ModerationStatus;
|
|
msg.Send(owner->GetSystemAddress());
|
|
}
|
|
}
|
|
{
|
|
GameMessages::PetNameChanged msg;
|
|
msg.target = m_Parent->GetObjectID();
|
|
msg.moderationStatus = m_ModerationStatus;
|
|
msg.name = GeneralUtils::UTF8ToUTF16(m_Name);
|
|
msg.ownerName = GeneralUtils::UTF8ToUTF16(m_OwnerName);
|
|
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|