fix(pets): pet digs wait for the owner and cost imagination

Issue 537. A pet that reached a dig dug it up by itself for free. Now,
as on live (2014 captures), the pet goes to the dig with state 0x500 and
the Go To Object ability while the owner gets the PR_DIG_TUTORIAL_01
tooltip; at the dig it waits with state 0x120 and the Dig At Position
ability, and the owner gets the pet action button (ShowPetActionButton
3). Pressing SHIFT or the button makes the client send RequestUse on the
pet (LWOPetControlComponent::msgPetCommand 0x00c5d220, "contextAction");
that costs PetAbilities.ImaginationCost (1 for digging), hides the
button, shows PR_DIG_TUTORIAL_03 and starts the dig. The button goes
away when the pet leaves the dig.

The dig is no longer written into the pet's serialized interaction:
live did not serialize one for digs or pet switches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-27 08:30:40 -05:00
parent f53ca0942b
commit eb7370ba3e
2 changed files with 175 additions and 14 deletions

View File

@@ -2,6 +2,7 @@
#include "DashboardNotify.h"
#include "GameMessages.h"
#include "PetMessages.h"
#include "PlayerMessages.h"
#include "InventoryMessages.h"
#include "EffectsMessages.h"
#include "ObjectMessages.h"
@@ -118,6 +119,9 @@ void PetComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpd
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;
}
@@ -308,7 +312,7 @@ void PetComponent::Update(float deltaTime) {
}
if (m_TreasureTime > 0) {
auto* treasure = Game::entityManager->GetEntity(m_Interaction);
auto* treasure = Game::entityManager->GetEntity(m_AbilityObject);
if (treasure == nullptr) {
m_TreasureTime = 0;
@@ -325,7 +329,7 @@ void PetComponent::Update(float deltaTime) {
treasure->Smash(m_Parent->GetObjectID());
m_Interaction = LWOOBJID_EMPTY;
m_AbilityObject = LWOOBJID_EMPTY;
m_TreasureTime = 0;
}
@@ -386,28 +390,26 @@ void PetComponent::Update(float deltaTime) {
Entity* closestTreasure = PetDigServer::GetClosestTreasure(position);
if (closestTreasure != nullptr && digUnlocked) {
// Skeleton Dragon Pat special case for bone digging
if (closestTreasure->GetLOT() == 12192 && m_Parent->GetLOT() != 13067) {
goto skipTreasure;
}
Entity* abilityObject = nullptr;
auto objectAbility = ePetAbilityType::Invalid;
bool atAbilityObject = false;
// 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 < 5 * 5) {
m_Interaction = closestTreasure->GetObjectID();
if (distance < 10 * 10) {
abilityObject = closestTreasure;
objectAbility = ePetAbilityType::DigAtPosition;
atAbilityObject = distance < 5 * 5;
Command(NiPoint3Constant::ZERO, LWOOBJID_EMPTY, 1, 202, true);
m_TreasureTime = 2;
} else if (distance < 10 * 10) {
haltDistance = 1;
destination = treasurePosition;
}
}
skipTreasure:
SetAbilityObject(abilityObject, objectAbility, atAbilityObject);
m_MovementAI->SetHaltDistance(haltDistance);
@@ -1166,6 +1168,114 @@ void PetComponent::Command(const NiPoint3& position, const LWOOBJID source, cons
}
}
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);
}
if (!atObject || m_AbilityButtonShown) return;
m_Ability = ability;
SetStatus(status | PET_STATE_USING_ABILITY | PET_STATE_AT_ABILITY_OBJECT);
Game::entityManager->SerializeEntity(m_Parent);
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;
}
}
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;
}