From aa08c7cb653f4a84a6368b9f92c89388ea959ab3 Mon Sep 17 00:00:00 2001 From: orange Date: Tue, 17 Mar 2026 20:43:26 +0300 Subject: [PATCH] improved projectile prediction --- .../proj_pred_engine_legacy.hpp | 12 +- tests/general/unit_test_prediction.cpp | 173 ++++++++++++++++++ 2 files changed, 180 insertions(+), 5 deletions(-) diff --git a/include/omath/projectile_prediction/proj_pred_engine_legacy.hpp b/include/omath/projectile_prediction/proj_pred_engine_legacy.hpp index 3740248..eb2b983 100644 --- a/include/omath/projectile_prediction/proj_pred_engine_legacy.hpp +++ b/include/omath/projectile_prediction/proj_pred_engine_legacy.hpp @@ -71,7 +71,7 @@ namespace omath::projectile_prediction if (!solution) return std::nullopt; - const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin, solution->predicted_target_position); + const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin + projectile.m_launch_offset, solution->predicted_target_position); return AimAngles{solution->pitch, yaw}; } @@ -129,10 +129,12 @@ namespace omath::projectile_prediction { const auto bullet_gravity = m_gravity_constant * projectile.m_gravity_scale; - if (bullet_gravity == 0.f) - return EngineTrait::calc_direct_pitch_angle(projectile.m_origin, target_position); + const auto launch_origin = projectile.m_origin + projectile.m_launch_offset; - const auto delta = target_position - projectile.m_origin; + if (bullet_gravity == 0.f) + return EngineTrait::calc_direct_pitch_angle(launch_origin, target_position); + + const auto delta = target_position - launch_origin; const auto distance2d = EngineTrait::calc_vector_2d_distance(delta); const auto distance2d_sqr = distance2d * distance2d; @@ -155,7 +157,7 @@ namespace omath::projectile_prediction bool is_projectile_reached_target(const Vector3& target_position, const Projectile& projectile, const float pitch, const float time) const noexcept { - const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin, target_position); + const auto yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin + projectile.m_launch_offset, target_position); const auto projectile_position = EngineTrait::predict_projectile_position(projectile, pitch, yaw, time, m_gravity_constant); diff --git a/tests/general/unit_test_prediction.cpp b/tests/general/unit_test_prediction.cpp index e3153ed..dc66d41 100644 --- a/tests/general/unit_test_prediction.cpp +++ b/tests/general/unit_test_prediction.cpp @@ -105,6 +105,179 @@ TEST(UnitTestPrediction, AimAnglesMatchAimPoint_WithLaunchOffset) expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f); } +// Helper: simulate projectile flight using aim_angles and verify it reaches the target. +// Steps the projectile forward in small increments, simultaneously predicts target position, +// and checks that the minimum distance is within hit_tolerance. +static void expect_projectile_hits_target(const omath::projectile_prediction::Projectile& proj, + const omath::projectile_prediction::Target& target, + float gravity, float engine_step, float max_time, float engine_tolerance, + float hit_tolerance, float sim_step = 1.f / 2000.f) +{ + using Trait = omath::source_engine::PredEngineTrait; + const omath::projectile_prediction::ProjPredEngineLegacy engine(gravity, engine_step, max_time, engine_tolerance); + + const auto aim_angles = engine.maybe_calculate_aim_angles(proj, target); + ASSERT_TRUE(aim_angles.has_value()) << "engine must find a solution"; + + float min_dist = std::numeric_limits::max(); + float best_time = 0.f; + + for (float t = 0.f; t <= max_time; t += sim_step) + { + const auto proj_pos = Trait::predict_projectile_position(proj, aim_angles->pitch, aim_angles->yaw, t, gravity); + const auto tgt_pos = Trait::predict_target_position(target, t, gravity); + const float dist = proj_pos.distance_to(tgt_pos); + + if (dist < min_dist) + { + min_dist = dist; + best_time = t; + } + + // Early exit once distance starts increasing significantly after approaching + if (dist > min_dist + hit_tolerance * 10.f && min_dist < hit_tolerance * 100.f) + break; + } + + EXPECT_LE(min_dist, hit_tolerance) + << "Projectile must reach target. Closest approach: " << min_dist + << " at t=" << best_time; +} + +// ── Simulation hit tests: no launch offset ───────────────────────────────── + +TEST(ProjectileSimulation, HitsStaticTarget_NoOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + constexpr omath::projectile_prediction::Projectile proj = { + .m_origin = {3, 2, 1}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); +} + +TEST(ProjectileSimulation, HitsMovingTarget_NoOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {500, 100, 0}, .m_velocity = {-50, 20, 0}, .m_is_airborne = false}; + constexpr omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_speed = 3000, .m_gravity_scale = 1.0}; + + expect_projectile_hits_target(proj, target, 800, 1.f / 500.f, 30, 10.f, 15.f); +} + +TEST(ProjectileSimulation, HitsAirborneTarget_NoOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {200, 50, 300}, .m_velocity = {10, -5, -20}, .m_is_airborne = true}; + constexpr omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_speed = 4000, .m_gravity_scale = 0.5}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 10.f, 15.f); +} + +TEST(ProjectileSimulation, HitsHighTarget_NoOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {10, 0, 500}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + constexpr omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.3}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); +} + +TEST(ProjectileSimulation, HitsNegativeYawTarget_NoOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {-200, -150, 10}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + constexpr omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); +} + +// ── Simulation hit tests: with launch offset ──────────────────────────────── + +TEST(ProjectileSimulation, HitsStaticTarget_SmallOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {200, 0, 50}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + const omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_offset = {5, 0, -3}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); +} + +TEST(ProjectileSimulation, HitsStaticTarget_LargeXOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {300, 100, 0}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + const omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_offset = {20, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); +} + +TEST(ProjectileSimulation, HitsStaticTarget_LargeYOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {150, -200, 30}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + const omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_offset = {0, 15, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); +} + +TEST(ProjectileSimulation, HitsStaticTarget_LargeZOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {100, 0, 200}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + const omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_offset = {0, 0, -10}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); +} + +TEST(ProjectileSimulation, HitsStaticTarget_AllAxesOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {250, 80, 60}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + const omath::projectile_prediction::Projectile proj = { + .m_origin = {10, 5, 20}, .m_launch_offset = {8, -4, -6}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); +} + +TEST(ProjectileSimulation, HitsMovingTarget_WithOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {400, 0, 50}, .m_velocity = {-30, 10, 5}, .m_is_airborne = false}; + const omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_offset = {10, -5, 2}, .m_launch_speed = 3000, .m_gravity_scale = 0.8}; + + expect_projectile_hits_target(proj, target, 800, 1.f / 500.f, 30, 10.f, 15.f); +} + +TEST(ProjectileSimulation, HitsAirborneTarget_WithOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {150, 80, 250}, .m_velocity = {5, -10, -30}, .m_is_airborne = true}; + const omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 50}, .m_launch_offset = {3, 7, -5}, .m_launch_speed = 4000, .m_gravity_scale = 0.5}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 10.f, 15.f); +} + +TEST(ProjectileSimulation, HitsNegativeYawTarget_WithOffset) +{ + constexpr omath::projectile_prediction::Target target{ + .m_origin = {-200, -150, 10}, .m_velocity = {0, 0, 0}, .m_is_airborne = false}; + const omath::projectile_prediction::Projectile proj = { + .m_origin = {0, 0, 0}, .m_launch_offset = {-5, 3, 2}, .m_launch_speed = 5000, .m_gravity_scale = 0.4}; + + expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f); +} + TEST(UnitTestPrediction, AimAnglesReturnsNulloptWhenNoSolution) { constexpr omath::projectile_prediction::Target target{