From 4b13c98fb4475cdf4266e26f634205bde0b2ea0d Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 08:36:07 -0500 Subject: [PATCH] fix(pets): pet bouncers wait for the owner and cost imagination Issue 536. A pet that reached a pet switch jumped on it by itself for free. Now it works like digs, following what live did (2014 captures): on the way the pet has state 0x500 and the owner gets PR_BOUNCER_TUTORIAL_01; on the switch the pet has state 0x120 and the Jump On Object ability, plays "excited" while the switch plays "engaged", and the owner gets the pet action button (ShowPetActionButton 2) and PR_BOUNCER_TUTORIAL_03. Using it costs PetAbilities.ImaginationCost (2), the pet plays "jump", the owner gets PR_TOOLTIP_1ST_PET_JUMPED_ON_SWITCH and the switch turns its bouncer on. The switch plays "launch" then, as on live, instead of "engaged". The pet switch is no longer written into the pet's serialized interaction (it was never cleared). Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/PetComponent.cpp | 49 +++++++++++++++++++-------- dGame/dComponents/PetComponent.h | 4 +-- dGame/dComponents/SwitchComponent.cpp | 3 +- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/dGame/dComponents/PetComponent.cpp b/dGame/dComponents/PetComponent.cpp index 48551c47d..63a045b1f 100644 --- a/dGame/dComponents/PetComponent.cpp +++ b/dGame/dComponents/PetComponent.cpp @@ -367,18 +367,21 @@ void PetComponent::Update(float deltaTime) { float haltDistance = 5; - if (closestSwitch != nullptr) { - if (!closestSwitch->GetActive()) { - NiPoint3 switchPosition = closestSwitch->GetParentEntity()->GetPosition(); - float distance = Vector3::DistanceSquared(position, switchPosition); - if (distance < 3 * 3) { - m_Interaction = closestSwitch->GetParentEntity()->GetObjectID(); - closestSwitch->OnUse(m_Parent); - } else if (distance < 20 * 20) { - haltDistance = 1; + Entity* abilityObject = nullptr; + auto objectAbility = ePetAbilityType::Invalid; + bool atAbilityObject = false; - destination = switchPosition; - } + 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; } } @@ -390,10 +393,6 @@ void PetComponent::Update(float deltaTime) { Entity* closestTreasure = PetDigServer::GetClosestTreasure(position); - 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(); @@ -1190,6 +1189,7 @@ void PetComponent::SetAbilityObject(Entity* object, const ePetAbilityType abilit 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; @@ -1198,6 +1198,13 @@ void PetComponent::SetAbilityObject(Entity* object, const ePetAbilityType abilit 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; @@ -1238,6 +1245,18 @@ void PetComponent::UseAbility() { 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(); + if (switchComponent) switchComponent->OnUse(m_Parent); } } diff --git a/dGame/dComponents/PetComponent.h b/dGame/dComponents/PetComponent.h index d9a40dd22..d18e3a9f0 100644 --- a/dGame/dComponents/PetComponent.h +++ b/dGame/dComponents/PetComponent.h @@ -232,7 +232,7 @@ public: private: /** - * Sets the object the pet goes to use its ability on (a dig), or none. The pet first goes to it; once it is there + * Sets the object the pet goes to use its ability on (a pet switch or a dig), or none. The pet first goes to it; once it is there * its owner gets the pet's action button, which uses the ability. * @param object the object, nullptr for none * @param ability the ability the object is for @@ -399,7 +399,7 @@ private: bool m_Despawning{ false }; /** - * The object the pet goes to use its ability on (a dig), if any. Live did not serialize it as the interaction. + * The object the pet goes to use its ability on (a pet switch or a dig), if any. Live did not serialize it as the interaction. */ LWOOBJID m_AbilityObject{ LWOOBJID_EMPTY }; diff --git a/dGame/dComponents/SwitchComponent.cpp b/dGame/dComponents/SwitchComponent.cpp index 07f989877..0d7f6755b 100644 --- a/dGame/dComponents/SwitchComponent.cpp +++ b/dGame/dComponents/SwitchComponent.cpp @@ -80,7 +80,8 @@ void SwitchComponent::EntityEnter(Entity* entity) { if (m_PetBouncer != nullptr) { GameMessages::PlayFXEffect(m_Parent->GetObjectID(), 2602, u"pettriggeractive", "BounceEffect").Send(UNASSIGNED_SYSTEM_ADDRESS); - RenderComponent::PlayAnimation(m_Parent, u"engaged"); + // Live played "launch" here; "engaged" is played when a pet arrives on the switch + RenderComponent::PlayAnimation(m_Parent, u"launch"); m_PetBouncer->SetPetBouncerEnabled(true); } else { Game::entityManager->SerializeEntity(m_Parent);