mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-19 06:43:58 -07:00
The parabolic range formula in GetEffectiveParabolicRange was only computating height differences from manual HeightOffset values, completely ignoring actual terrain elevation. This meant units on hills received no tactical advantage despite the UI stat tooltip correctly showing extended ranges. Fixes #8889
439 lines
17 KiB
C++
439 lines
17 KiB
C++
/* Copyright (C) 2026 Wildfire Games.
|
|
* This file is part of 0 A.D.
|
|
*
|
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* 0 A.D. is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "lib/self_test.h"
|
|
|
|
#include "maths/Fixed.h"
|
|
#include "maths/FixedVector2D.h"
|
|
#include "maths/FixedVector3D.h"
|
|
#include "maths/Matrix3D.h"
|
|
#include "ps/CStr.h"
|
|
#include "ps/XML/Xeromyces.h"
|
|
#include "scriptinterface/ScriptInterface.h"
|
|
#include "simulation2/MessageTypes.h"
|
|
#include "simulation2/components/ICmpObstruction.h"
|
|
#include "simulation2/components/ICmpObstructionManager.h"
|
|
#include "simulation2/components/ICmpPosition.h"
|
|
#include "simulation2/components/ICmpRangeManager.h"
|
|
#include "simulation2/components/ICmpTerrain.h"
|
|
#include "simulation2/components/ICmpVision.h"
|
|
#include "simulation2/components/ICmpWaterManager.h"
|
|
#include "simulation2/helpers/Player.h"
|
|
#include "simulation2/helpers/Position.h"
|
|
#include "simulation2/system/Component.h"
|
|
#include "simulation2/system/ComponentTest.h"
|
|
#include "simulation2/system/Entity.h"
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <random>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
class MockTerrainRgm : public ICmpTerrain
|
|
{
|
|
public:
|
|
DEFAULT_MOCK_COMPONENT()
|
|
|
|
entity_pos_t GetGroundLevel(entity_pos_t x, entity_pos_t /*z*/) const override
|
|
{
|
|
// Simple slope: height increases with x
|
|
// height = x / 10
|
|
return x / 10;
|
|
}
|
|
|
|
float GetExactGroundLevel(float x, float /*z*/) const override
|
|
{
|
|
return x / 10.0f;
|
|
}
|
|
|
|
CFixedVector3D CalcNormal(entity_pos_t /*x*/, entity_pos_t /*z*/) const override
|
|
{
|
|
// Return a simple upward normal
|
|
return CFixedVector3D(fixed::Zero(), fixed::FromInt(1), fixed::Zero());
|
|
}
|
|
|
|
bool IsLoaded() const override
|
|
{
|
|
return true;
|
|
}
|
|
|
|
CVector3D CalcExactNormal(float /*x*/, float /*z*/) const override
|
|
{
|
|
return CVector3D(0.0f, 1.0f, 0.0f);
|
|
}
|
|
|
|
u16 GetTilesPerSide() const override
|
|
{
|
|
return 512;
|
|
}
|
|
|
|
u16 GetVerticesPerSide() const override
|
|
{
|
|
return 513;
|
|
}
|
|
|
|
u32 GetMapSize() const override
|
|
{
|
|
return 512;
|
|
}
|
|
|
|
void ReloadTerrain(bool /*ReloadWater*/) override
|
|
{
|
|
// Nothing needed for mock
|
|
}
|
|
|
|
void MakeDirty(i32 /*i0*/, i32 /*j0*/, i32 /*i1*/, i32 /*j1*/) override
|
|
{
|
|
// Nothing needed for mock
|
|
}
|
|
|
|
CTerrain* GetCTerrain() override
|
|
{
|
|
return nullptr;
|
|
}
|
|
};
|
|
|
|
class MockWaterManagerRgm : public ICmpWaterManager
|
|
{
|
|
public:
|
|
DEFAULT_MOCK_COMPONENT()
|
|
|
|
entity_pos_t GetWaterLevel(entity_pos_t /*x*/, entity_pos_t /*z*/) const override
|
|
{
|
|
return m_WaterLevel;
|
|
}
|
|
|
|
float GetExactWaterLevel(float /*x*/, float /*z*/) const override
|
|
{
|
|
return m_WaterLevel.ToFloat();
|
|
}
|
|
|
|
void RecomputeWaterData() override
|
|
{
|
|
// Nothing needed for mock
|
|
}
|
|
|
|
void SetWaterLevel(entity_pos_t level) override
|
|
{
|
|
m_WaterLevel = level;
|
|
}
|
|
|
|
private:
|
|
entity_pos_t m_WaterLevel{};
|
|
};
|
|
|
|
class MockVisionRgm : public ICmpVision
|
|
{
|
|
public:
|
|
DEFAULT_MOCK_COMPONENT()
|
|
|
|
entity_pos_t GetRange() const override { return entity_pos_t::FromInt(66); }
|
|
bool GetRevealShore() const override { return false; }
|
|
};
|
|
|
|
class MockPositionRgm : public ICmpPosition
|
|
{
|
|
public:
|
|
DEFAULT_MOCK_COMPONENT()
|
|
|
|
void SetTurretParent(entity_id_t /*id*/, const CFixedVector3D& /*pos*/) override {}
|
|
entity_id_t GetTurretParent() const override {return INVALID_ENTITY;}
|
|
void UpdateTurretPosition() override {}
|
|
std::set<entity_id_t>* GetTurrets() override { return nullptr; }
|
|
bool IsInWorld() const override { return m_InWorld; }
|
|
void MoveOutOfWorld() override { m_InWorld = false; }
|
|
void MoveTo(entity_pos_t /*x*/, entity_pos_t /*z*/) override { }
|
|
void MoveAndTurnTo(entity_pos_t /*x*/, entity_pos_t /*z*/, entity_angle_t /*a*/) override { }
|
|
void JumpTo(entity_pos_t /*x*/, entity_pos_t /*z*/) override { }
|
|
void SetHeightOffset(entity_pos_t dy) override { m_HeightOffset = dy; }
|
|
entity_pos_t GetHeightOffset() const override { return m_HeightOffset; }
|
|
void SetHeightFixed(entity_pos_t /*y*/) override { }
|
|
entity_pos_t GetHeightFixed() const override { return entity_pos_t::Zero(); }
|
|
entity_pos_t GetHeightAtFixed(entity_pos_t, entity_pos_t) const override { return entity_pos_t::Zero(); }
|
|
bool IsHeightRelative() const override { return true; }
|
|
void SetHeightRelative(bool /*relative*/) override { }
|
|
bool CanFloat() const override { return false; }
|
|
void SetFloating(bool /*flag*/) override { }
|
|
void SetActorFloating(bool /*flag*/) override { }
|
|
void SetActorAnchor(const CStr& /*anchor*/) override { }
|
|
void SetConstructionProgress(fixed /*progress*/) override { }
|
|
CFixedVector3D GetPosition() const override { return m_Pos; }
|
|
CFixedVector2D GetPosition2D() const override { return CFixedVector2D(m_Pos.X, m_Pos.Z); }
|
|
CFixedVector3D GetPreviousPosition() const override { return CFixedVector3D(); }
|
|
CFixedVector2D GetPreviousPosition2D() const override { return CFixedVector2D(); }
|
|
fixed GetTurnRate() const override { return fixed::Zero(); }
|
|
void TurnTo(entity_angle_t /*y*/) override { }
|
|
void SetYRotation(entity_angle_t /*y*/) override { }
|
|
void SetXZRotation(entity_angle_t /*x*/, entity_angle_t /*z*/) override { }
|
|
CFixedVector3D GetRotation() const override { return CFixedVector3D(); }
|
|
fixed GetDistanceTravelled() const override { return fixed::Zero(); }
|
|
void GetInterpolatedPosition2D(float /*frameOffset*/, float& x, float& z, float& rotY) const override { x = z = rotY = 0; }
|
|
CMatrix3D GetInterpolatedTransform(float /*frameOffset*/) const override { return CMatrix3D(); }
|
|
|
|
CFixedVector3D m_Pos;
|
|
entity_pos_t m_HeightOffset = entity_pos_t::Zero();
|
|
bool m_InWorld = true;
|
|
};
|
|
|
|
class MockObstructionRgm : public ICmpObstruction
|
|
{
|
|
public:
|
|
DEFAULT_MOCK_COMPONENT();
|
|
|
|
MockObstructionRgm(entity_pos_t s) : m_Size(s) {};
|
|
|
|
ICmpObstructionManager::tag_t GetObstruction() const override { return {}; };
|
|
bool GetObstructionSquare(ICmpObstructionManager::ObstructionSquare&) const override { return false; };
|
|
bool GetPreviousObstructionSquare(ICmpObstructionManager::ObstructionSquare&) const override { return false; };
|
|
entity_pos_t GetSize() const override { return m_Size; };
|
|
CFixedVector2D GetStaticSize() const override { return {}; };
|
|
EObstructionType GetObstructionType() const override { return {}; };
|
|
void SetUnitClearance(const entity_pos_t&) override {};
|
|
bool IsControlPersistent() const override { return {}; };
|
|
bool CheckShorePlacement() const override { return {}; };
|
|
EFoundationCheck CheckFoundation(const std::string&) const override { return {}; };
|
|
EFoundationCheck CheckFoundation(const std::string& , bool) const override { return {}; };
|
|
std::string CheckFoundation_wrapper(const std::string&, bool) const override { return {}; };
|
|
bool CheckDuplicateFoundation() const override { return {}; };
|
|
std::vector<entity_id_t> GetEntitiesByFlags(ICmpObstructionManager::flags_t) const override { return {}; };
|
|
std::vector<entity_id_t> GetEntitiesBlockingMovement() const override { return {}; };
|
|
std::vector<entity_id_t> GetEntitiesBlockingConstruction() const override { return {}; };
|
|
std::vector<entity_id_t> GetEntitiesDeletedUponConstruction() const override { return {}; };
|
|
void ResolveFoundationCollisions() const override {};
|
|
void SetActive(bool) override {};
|
|
void SetMovingFlag(bool) override {};
|
|
void SetDisableBlockMovementPathfinding(bool, bool, int32_t) override {};
|
|
bool GetBlockMovementFlag(bool) const override { return {}; };
|
|
void SetControlGroup(entity_id_t) override {};
|
|
entity_id_t GetControlGroup() const override { return {}; };
|
|
void SetControlGroup2(entity_id_t) override {};
|
|
entity_id_t GetControlGroup2() const override { return {}; };
|
|
private:
|
|
entity_pos_t m_Size;
|
|
};
|
|
|
|
class TestCmpRangeManager : public CxxTest::TestSuite
|
|
{
|
|
std::optional<CXeromycesEngine> xeromycesEngine;
|
|
public:
|
|
void setUp()
|
|
{
|
|
xeromycesEngine.emplace();
|
|
}
|
|
|
|
void tearDown()
|
|
{
|
|
xeromycesEngine.reset();
|
|
}
|
|
|
|
// TODO It would be nice to call Verify() with the shore revealing system
|
|
// but that means testing on an actual map, with water and land.
|
|
|
|
void test_basic()
|
|
{
|
|
ComponentTestHelper test(*g_ScriptContext);
|
|
|
|
ICmpRangeManager* cmp = test.Add<ICmpRangeManager>(CID_RangeManager, "", SYSTEM_ENTITY);
|
|
|
|
MockVisionRgm vision;
|
|
test.AddMock(100, IID_Vision, vision);
|
|
|
|
MockPositionRgm position;
|
|
test.AddMock(100, IID_Position, position);
|
|
|
|
// This tests that the incremental computation produces the correct result
|
|
// in various edge cases
|
|
|
|
cmp->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512));
|
|
cmp->Verify();
|
|
{ CMessageCreate msg(100); cmp->HandleMessage(msg, false); }
|
|
cmp->Verify();
|
|
{ CMessageOwnershipChanged msg(100, -1, 1); cmp->HandleMessage(msg, false); }
|
|
cmp->Verify();
|
|
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(247), entity_pos_t::FromDouble(257.95), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
|
cmp->Verify();
|
|
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(247), entity_pos_t::FromInt(253), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
|
cmp->Verify();
|
|
|
|
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
|
cmp->Verify();
|
|
|
|
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256)+entity_pos_t::Epsilon(), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
|
cmp->Verify();
|
|
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256)-entity_pos_t::Epsilon(), entity_pos_t::FromInt(256), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
|
cmp->Verify();
|
|
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256)+entity_pos_t::Epsilon(), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
|
cmp->Verify();
|
|
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(256), entity_pos_t::FromInt(256)-entity_pos_t::Epsilon(), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
|
cmp->Verify();
|
|
|
|
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(383), entity_pos_t::FromInt(84), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
|
cmp->Verify();
|
|
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromInt(348), entity_pos_t::FromInt(83), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
|
cmp->Verify();
|
|
|
|
std::mt19937 rng;
|
|
for (size_t i = 0; i < 1024; ++i)
|
|
{
|
|
double x = std::uniform_real_distribution<double>(0.0, 512.0)(rng);
|
|
double z = std::uniform_real_distribution<double>(0.0, 512.0)(rng);
|
|
{ CMessagePositionChanged msg(100, true, entity_pos_t::FromDouble(x), entity_pos_t::FromDouble(z), entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
|
cmp->Verify();
|
|
}
|
|
|
|
// Test OwnershipChange, GetEntitiesByPlayer, GetNonGaiaEntities
|
|
{
|
|
player_id_t previousOwner = -1;
|
|
for (player_id_t newOwner = 0; newOwner < 8; ++newOwner)
|
|
{
|
|
CMessageOwnershipChanged msg(100, previousOwner, newOwner);
|
|
cmp->HandleMessage(msg, false);
|
|
|
|
for (player_id_t i = 0; i < 8; ++i)
|
|
TS_ASSERT_EQUALS(cmp->GetEntitiesByPlayer(i).size(), i == newOwner ? 1 : 0);
|
|
|
|
TS_ASSERT_EQUALS(cmp->GetNonGaiaEntities().size(), newOwner > 0 ? 1 : 0);
|
|
previousOwner = newOwner;
|
|
}
|
|
}
|
|
}
|
|
|
|
void test_queries()
|
|
{
|
|
ComponentTestHelper test(*g_ScriptContext);
|
|
|
|
ICmpRangeManager* cmp = test.Add<ICmpRangeManager>(CID_RangeManager, "", SYSTEM_ENTITY);
|
|
|
|
MockVisionRgm vision, vision2;
|
|
MockPositionRgm position, position2;
|
|
MockObstructionRgm obs(fixed::FromInt(2)), obs2(fixed::Zero());
|
|
test.AddMock(100, IID_Vision, vision);
|
|
test.AddMock(100, IID_Position, position);
|
|
test.AddMock(100, IID_Obstruction, obs);
|
|
|
|
test.AddMock(101, IID_Vision, vision2);
|
|
test.AddMock(101, IID_Position, position2);
|
|
test.AddMock(101, IID_Obstruction, obs2);
|
|
|
|
cmp->SetBounds(entity_pos_t::FromInt(0), entity_pos_t::FromInt(0), entity_pos_t::FromInt(512), entity_pos_t::FromInt(512));
|
|
cmp->Verify();
|
|
{ CMessageCreate msg(100); cmp->HandleMessage(msg, false); }
|
|
{ CMessageCreate msg(101); cmp->HandleMessage(msg, false); }
|
|
|
|
{ CMessageOwnershipChanged msg(100, -1, 1); cmp->HandleMessage(msg, false); }
|
|
{ CMessageOwnershipChanged msg(101, -1, 1); cmp->HandleMessage(msg, false); }
|
|
|
|
auto move = [&cmp](entity_id_t ent, MockPositionRgm& pos, fixed x, fixed z) {
|
|
pos.m_Pos = CFixedVector3D(x, fixed::Zero(), z);
|
|
{ CMessagePositionChanged msg(ent, true, x, z, entity_angle_t::Zero()); cmp->HandleMessage(msg, false); }
|
|
};
|
|
|
|
move(100, position, fixed::FromInt(10), fixed::FromInt(10));
|
|
move(101, position2, fixed::FromInt(10), fixed::FromInt(20));
|
|
|
|
std::vector<entity_id_t> nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true);
|
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
|
|
nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0, true);
|
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
|
|
|
move(101, position2, fixed::FromInt(10), fixed::FromInt(10));
|
|
nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true);
|
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
|
nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0, true);
|
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
|
|
|
|
move(101, position2, fixed::FromInt(10), fixed::FromInt(13));
|
|
nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true);
|
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
|
nearby = cmp->ExecuteQuery(100, fixed::FromInt(4), fixed::FromInt(50), {1}, 0, true);
|
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
|
|
|
|
move(101, position2, fixed::FromInt(10), fixed::FromInt(15));
|
|
// In range thanks to self obstruction size.
|
|
nearby = cmp->ExecuteQuery(100, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true);
|
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
|
// In range thanks to target obstruction size.
|
|
nearby = cmp->ExecuteQuery(101, fixed::FromInt(0), fixed::FromInt(4), {1}, 0, true);
|
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{100});
|
|
|
|
// Trickier: min-range is closest-to-closest, but rotation may change the real distance.
|
|
nearby = cmp->ExecuteQuery(100, fixed::FromInt(2), fixed::FromInt(50), {1}, 0, true);
|
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
|
nearby = cmp->ExecuteQuery(100, fixed::FromInt(5), fixed::FromInt(50), {1}, 0, true);
|
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{101});
|
|
nearby = cmp->ExecuteQuery(100, fixed::FromInt(6), fixed::FromInt(50), {1}, 0, true);
|
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
|
|
nearby = cmp->ExecuteQuery(101, fixed::FromInt(5), fixed::FromInt(50), {1}, 0, true);
|
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{100});
|
|
nearby = cmp->ExecuteQuery(101, fixed::FromInt(6), fixed::FromInt(50), {1}, 0, true);
|
|
TS_ASSERT_EQUALS(nearby, std::vector<entity_id_t>{});
|
|
|
|
}
|
|
|
|
void test_ParabolicRangeBasic()
|
|
{
|
|
ComponentTestHelper test(*g_ScriptContext);
|
|
ICmpRangeManager* cmp = test.Add<ICmpRangeManager>(CID_RangeManager, "", SYSTEM_ENTITY);
|
|
|
|
// Add terrain and water components
|
|
MockTerrainRgm terrain;
|
|
test.AddMock(SYSTEM_ENTITY, IID_Terrain, terrain);
|
|
|
|
MockWaterManagerRgm waterManager;
|
|
test.AddMock(SYSTEM_ENTITY, IID_WaterManager, waterManager);
|
|
|
|
const entity_id_t source = 200;
|
|
const entity_id_t target = 201;
|
|
entity_pos_t range{fixed::FromInt(-3)};
|
|
entity_pos_t yOrigin{fixed::FromInt(-20)};
|
|
|
|
// Invalid range.
|
|
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), range);
|
|
|
|
// No source ICmpPosition.
|
|
range = fixed::FromInt(10);
|
|
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE);
|
|
|
|
// No target ICmpPosition.
|
|
MockPositionRgm cmpSourcePosition;
|
|
test.AddMock(source, IID_Position, cmpSourcePosition);
|
|
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE);
|
|
|
|
// Too much height difference.
|
|
MockPositionRgm cmpTargetPosition;
|
|
test.AddMock(target, IID_Position, cmpTargetPosition);
|
|
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), NEVER_IN_RANGE);
|
|
|
|
// If no offset we get the range.
|
|
range = fixed::FromInt(20);
|
|
yOrigin = fixed::Zero();
|
|
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), range);
|
|
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, fixed::Zero(), yOrigin), fixed::Zero());
|
|
|
|
// Normal case with yOrigin only (no terrain difference)
|
|
yOrigin = fixed::FromInt(5);
|
|
range = fixed::FromInt(10);
|
|
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), fixed::FromFloat(14.142136f));
|
|
|
|
// Big range.
|
|
range = fixed::FromInt(260);
|
|
TS_ASSERT_EQUALS(cmp->GetEffectiveParabolicRange(source, target, range, yOrigin), fixed::FromFloat(264.952820f));
|
|
}
|
|
};
|