diff --git a/dGame/dComponents/Physics/ControllablePhysicsComponent.cpp b/dGame/dComponents/Physics/ControllablePhysicsComponent.cpp index 2e8efcea7..a04ffaa99 100644 --- a/dGame/dComponents/Physics/ControllablePhysicsComponent.cpp +++ b/dGame/dComponents/Physics/ControllablePhysicsComponent.cpp @@ -333,8 +333,7 @@ void ControllablePhysicsComponent::RemovePickupRadiusScale(float value) { void ControllablePhysicsComponent::AddSpeedboost(float value) { m_ActiveSpeedBoosts.push_back(value); - m_SpeedBoost = value; - SetSpeedMultiplier(value / 500.0f); // 500 being the base speed + UpdateSpeedBoost(); } void ControllablePhysicsComponent::RemoveSpeedboost(float value) { @@ -346,16 +345,35 @@ void ControllablePhysicsComponent::RemoveSpeedboost(float value) { return; } - // Recalculate speedboost since we removed one - m_SpeedBoost = 500.0f; // Entities without level progression (enemies, pets) go back to the normal multiplier of 1 - if (m_ActiveSpeedBoosts.empty()) { // no active speed boosts left, so return to base speed + UpdateSpeedBoost(); + Game::entityManager->SerializeEntity(m_Parent); +} + +void ControllablePhysicsComponent::UpdateSpeedBoost() { + // The client's rule (LWOControllablePhysComponent::msgAddRunspeedModifier 0x00be0af0, msgRemoveRunSpeedModifier + // 0x00be0720): a modifier over 1000 or of exactly the base 500 counts for nothing; with any slow (under 500) the + // lowest one is the speed, otherwise the highest boost is + constexpr float baseSpeed = 500.0f; + constexpr float maxModifier = 1000.0f; + float lowestSlow = baseSpeed; + float highestBoost = baseSpeed; + for (const auto boost : m_ActiveSpeedBoosts) { + if (boost > maxModifier) continue; + if (boost < lowestSlow) lowestSlow = boost; + if (boost > highestBoost) highestBoost = boost; + } + + if (lowestSlow < baseSpeed) { + m_SpeedBoost = lowestSlow; + } else if (highestBoost > baseSpeed) { + m_SpeedBoost = highestBoost; + } else { + // No modifier counts: entities without level progression (enemies, pets) go back to the normal multiplier of 1 + m_SpeedBoost = baseSpeed; auto* levelProgressionComponent = m_Parent->GetComponent(); if (levelProgressionComponent) m_SpeedBoost = levelProgressionComponent->GetSpeedBase(); - } else { // Used the last applied speedboost - m_SpeedBoost = m_ActiveSpeedBoosts.back(); } - SetSpeedMultiplier(m_SpeedBoost / 500.0f); // 500 being the base speed - Game::entityManager->SerializeEntity(m_Parent); + SetSpeedMultiplier(m_SpeedBoost / baseSpeed); } void ControllablePhysicsComponent::ActivateBubbleBuff(eBubbleType bubbleType, bool specialAnims) { diff --git a/dGame/dComponents/Physics/ControllablePhysicsComponent.h b/dGame/dComponents/Physics/ControllablePhysicsComponent.h index 5d5e8e78d..27765cdc5 100644 --- a/dGame/dComponents/Physics/ControllablePhysicsComponent.h +++ b/dGame/dComponents/Physics/ControllablePhysicsComponent.h @@ -395,6 +395,11 @@ private: */ bool m_IsTeleporting; + /** + * Works the speed out of the active speed boosts: the lowest slow if there is one, otherwise the highest boost + */ + void UpdateSpeedBoost(); + /** * The list of speed boosts for this entity */ diff --git a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp index 9d8846b9e..9fec20670 100644 --- a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp +++ b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp @@ -432,6 +432,40 @@ TEST_F(ReplicaConstructionTest, CharacterGuildChangeIsWrittenOnce) { player.SetCharacter(nullptr); } +// Run speed modifiers stack as in LWOControllablePhysComponent::msgAddRunspeedModifier (0x00be0af0) and +// msgRemoveRunSpeedModifier (0x00be0720): the lowest slow wins, otherwise the highest boost; 500 and anything over 1000 +// count for nothing +TEST_F(ReplicaConstructionTest, RunSpeedModifiersStackAsTheClientDoes) { + Entity entity(15, info); + auto* const physics = entity.AddComponent(-1); + EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 1.0f); + + physics->AddSpeedboost(750.0f); + EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 1.5f); + physics->AddSpeedboost(550.0f); // a smaller boost applied later does not replace the bigger one + EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 1.5f); + physics->AddSpeedboost(250.0f); // any slow beats every boost + EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 0.5f); + physics->AddSpeedboost(133.0f); + EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 133.0f / 500.0f); + physics->AddSpeedboost(350.0f); + EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 133.0f / 500.0f); + physics->AddSpeedboost(1500.0f); + physics->AddSpeedboost(500.0f); + EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 133.0f / 500.0f); + + physics->RemoveSpeedboost(133.0f); + EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 0.5f); + physics->RemoveSpeedboost(250.0f); + EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 0.7f); + physics->RemoveSpeedboost(350.0f); + EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 1.5f); + physics->RemoveSpeedboost(750.0f); + EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 1.1f); + physics->RemoveSpeedboost(550.0f); + EXPECT_FLOAT_EQ(physics->GetSpeedMultiplier(), 1.0f); // 1500 and 500 are left and count for nothing +} + // Live wrote the cheat block (gravity scale, speed multiplier) on construction only when one of them was changed: 216 // of 20,617 controllable-physics constructions had it (players at run speed 1.05, enemies with gravity 0), never 1/1. TEST_F(ReplicaConstructionTest, ControllablePhysicsConstructionWritesCheatsOnlyWhenChanged) {