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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-27 08:36:07 -05:00
parent eb7370ba3e
commit 4b13c98fb4
3 changed files with 38 additions and 18 deletions

View File

@@ -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<SwitchComponent>();
if (switchComponent) switchComponent->OnUse(m_Parent);
}
}

View File

@@ -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 };

View File

@@ -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);