diff --git a/src/ballistics.cpp b/src/ballistics.cpp index 6c3a80944feb..74767bf8caff 100644 --- a/src/ballistics.cpp +++ b/src/ballistics.cpp @@ -10,6 +10,7 @@ #include "avatar.h" #include "calendar.h" +#include "cata_utility.h" // for normal_cdf #include "creature.h" #include "damage.h" #include "debug.h" @@ -455,3 +456,25 @@ dealt_projectile_attack projectile_attack( const projectile &proj_arg, const tri return attack; } + +namespace ranged +{ + +double hit_chance( const dispersion_sources &dispersion, double range, double target_size, + double missed_by ) +{ + if( range <= 0 ) { + return 1.0; + } + + double missed_by_tiles = missed_by * target_size; + + // T = (2*D**2 * (1 - cos V)) ** 0.5 (from iso_tangent) + // cos V = 1 - T**2 / (2*D**2) + double cosV = 1 - missed_by_tiles * missed_by_tiles / ( 2 * range * range ); + double needed_dispersion = ( cosV < -1.0 ? M_PI : acos( cosV ) ) * 180 * 60 / M_PI; + + return normal_cdf( needed_dispersion, dispersion.avg(), dispersion.avg() / 2 ); +} + +} diff --git a/src/ballistics.h b/src/ballistics.h index b77ea40ef5c6..00936a77ac3d 100644 --- a/src/ballistics.h +++ b/src/ballistics.h @@ -34,4 +34,20 @@ dealt_projectile_attack projectile_attack( const projectile &proj_arg, const tri const tripoint &target_arg, const dispersion_sources &dispersion, Creature *origin = nullptr, const vehicle *in_veh = nullptr ); +namespace ranged +{ + +/** + * The chance that a fired shot reaches required accuracy - by default grazing shot. + * + * @param dispersion accuracy of the shot. Must be a purely normal distribution. + * @param range distance between the shooter and the target. + * @param target_size size of the target, in the range (0, 1]. + * @param missed_by maximum degree of miss, in the range (0, 1]. Effectively a multiplier on @param target_size. + */ +double hit_chance( const dispersion_sources &dispersion, double range, double target_size, + double missed_by = 1.0 ); + +} // namespace ranged + #endif // CATA_SRC_BALLISTICS_H diff --git a/src/cata_utility.cpp b/src/cata_utility.cpp index 1264b6289e94..337fa9e324d1 100644 --- a/src/cata_utility.cpp +++ b/src/cata_utility.cpp @@ -173,6 +173,11 @@ double logarithmic_range( int min, int max, int pos ) return ( raw_logistic - LOGI_MIN ) / LOGI_RANGE; } +double normal_cdf( double x, double mean, double stddev ) +{ + return 0.5 * ( 1.0 + std::erf( ( x - mean ) / ( stddev * M_SQRT2 ) ) ); +} + int bound_mod_to_vals( int val, int mod, int max, int min ) { if( val + mod > max && max != 0 ) { diff --git a/src/cata_utility.h b/src/cata_utility.h index 402bf684f373..0a39a08269a7 100644 --- a/src/cata_utility.h +++ b/src/cata_utility.h @@ -130,6 +130,17 @@ double logarithmic( double t ); */ double logarithmic_range( int min, int max, int pos ); +/** + * Cumulative distribution function of a certain normal distribution. + * + * @param x point at which the CDF of the distribution is measured + * @param mean mean of the normal distribution + * @param stddev standard deviation of the normal distribution + * + * @return The probability that a random point from the distribution will be lesser than @param x + */ +double normal_cdf( double x, double mean, double stddev ); + /** * Clamp the value of a modifier in order to bound the resulting value * diff --git a/tests/throwing_test.cpp b/tests/throwing_test.cpp index 1abbc7e144d8..6e8b2236f57d 100644 --- a/tests/throwing_test.cpp +++ b/tests/throwing_test.cpp @@ -6,9 +6,11 @@ #include #include "avatar.h" +#include "ballistics.h" #include "calendar.h" #include "catch/catch.hpp" #include "damage.h" +#include "dispersion.h" #include "game.h" #include "game_constants.h" #include "inventory.h" @@ -105,6 +107,19 @@ static void test_throwing_player_versus( monster &mon = spawn_test_monster( mon_id, monster_start ); mon.set_moves( 0 ); + double actual_hit_chance = ranged::hit_chance( + dispersion_sources( p.throwing_dispersion( it, &mon ) ), + range, mon.ranged_target_size() ); + if( std::fabs( actual_hit_chance - hit_thresh.midpoint ) > hit_thresh.epsilon / 2.0 ) { + CAPTURE( hit_thresh.midpoint ); + CAPTURE( hit_thresh.epsilon / 2.0 ); + CAPTURE( actual_hit_chance ); + CAPTURE( range ); + CAPTURE( mon.ranged_target_size() ); + FAIL_CHECK( "Expected and calculated midpoints must be within epsilon/2 or the test is too fragile" ); + return; + } + auto atk = p.throw_item( mon.pos(), it ); data.hits.add( atk.hit_critter != nullptr ); data.dmg.add( atk.dealt_dam.total_damage() ); @@ -175,16 +190,16 @@ TEST_CASE( "basic_throwing_sanity_tests", "[throwing],[balance]" ) test_throwing_player_versus( p, "mon_zombie", "rock", 5, lo_skill_base_stats, { 0.77, 0.10 }, { 5.5, 2 } ); test_throwing_player_versus( p, "mon_zombie", "rock", 10, lo_skill_base_stats, { 0.27, 0.10 }, { 2, 2 } ); test_throwing_player_versus( p, "mon_zombie", "rock", 15, lo_skill_base_stats, { 0.13, 0.10 }, { 1, 2 } ); - test_throwing_player_versus( p, "mon_zombie", "rock", 20, lo_skill_base_stats, { 0.03, 0.10 }, { 0.5, 2 } ); + test_throwing_player_versus( p, "mon_zombie", "rock", 20, lo_skill_base_stats, { 0.095, 0.10 }, { 0.5, 2 } ); test_throwing_player_versus( p, "mon_zombie", "rock", 25, lo_skill_base_stats, { 0.03, 0.10 }, { 0.5, 2 } ); - test_throwing_player_versus( p, "mon_zombie", "rock", 30, lo_skill_base_stats, { 0.03, 0.10 }, { 0.5, 2 } ); + test_throwing_player_versus( p, "mon_zombie", "rock", 30, lo_skill_base_stats, { 0.06, 0.10 }, { 0.5, 2 } ); } SECTION( "test_player_vs_zombie_javelin_iron_basestats" ) { - test_throwing_player_versus( p, "mon_zombie", "javelin_iron", 1, lo_skill_base_stats, { 0.95, 0.10 }, { 28, 5 } ); + test_throwing_player_versus( p, "mon_zombie", "javelin_iron", 1, lo_skill_base_stats, { 1.00, 0.10 }, { 28, 5 } ); test_throwing_player_versus( p, "mon_zombie", "javelin_iron", 5, lo_skill_base_stats, { 0.64, 0.10 }, { 13, 3 } ); test_throwing_player_versus( p, "mon_zombie", "javelin_iron", 10, lo_skill_base_stats, { 0.20, 0.10 }, { 4, 2 } ); - test_throwing_player_versus( p, "mon_zombie", "javelin_iron", 15, lo_skill_base_stats, { 0.03, 0.10 }, { 1.29, 3 } ); + test_throwing_player_versus( p, "mon_zombie", "javelin_iron", 15, lo_skill_base_stats, { 0.11, 0.10 }, { 1.29, 3 } ); test_throwing_player_versus( p, "mon_zombie", "javelin_iron", 20, lo_skill_base_stats, { 0.03, 0.10 }, { 1.66, 2 } ); test_throwing_player_versus( p, "mon_zombie", "javelin_iron", 25, lo_skill_base_stats, { 0.03, 0.10 }, { 1.0, 2 } ); } @@ -195,7 +210,7 @@ TEST_CASE( "basic_throwing_sanity_tests", "[throwing],[balance]" ) test_throwing_player_versus( p, "mon_zombie", "rock", 10, hi_skill_athlete_stats, { 1.00, 0.10 }, { 16.27, 6 } ); test_throwing_player_versus( p, "mon_zombie", "rock", 15, hi_skill_athlete_stats, { 0.97, 0.10 }, { 12.83, 4 } ); test_throwing_player_versus( p, "mon_zombie", "rock", 20, hi_skill_athlete_stats, { 0.82, 0.10 }, { 9.10, 4 } ); - test_throwing_player_versus( p, "mon_zombie", "rock", 25, hi_skill_athlete_stats, { 0.64, 0.10 }, { 6.54, 4 } ); + test_throwing_player_versus( p, "mon_zombie", "rock", 25, hi_skill_athlete_stats, { 0.58, 0.10 }, { 6.54, 4 } ); test_throwing_player_versus( p, "mon_zombie", "rock", 30, hi_skill_athlete_stats, { 0.47, 0.10 }, { 4.90, 3 } ); } @@ -205,7 +220,7 @@ TEST_CASE( "basic_throwing_sanity_tests", "[throwing],[balance]" ) test_throwing_player_versus( p, "mon_zombie", "javelin_iron", 10, hi_skill_athlete_stats, { 1.00, 0.10 }, { 34.16, 8 } ); test_throwing_player_versus( p, "mon_zombie", "javelin_iron", 15, hi_skill_athlete_stats, { 0.97, 0.10 }, { 25.21, 6 } ); test_throwing_player_versus( p, "mon_zombie", "javelin_iron", 20, hi_skill_athlete_stats, { 0.82, 0.10 }, { 18.90, 5 } ); - test_throwing_player_versus( p, "mon_zombie", "javelin_iron", 25, hi_skill_athlete_stats, { 0.63, 0.10 }, { 13.59, 5 } ); + test_throwing_player_versus( p, "mon_zombie", "javelin_iron", 25, hi_skill_athlete_stats, { 0.58, 0.10 }, { 13.59, 5 } ); test_throwing_player_versus( p, "mon_zombie", "javelin_iron", 30, hi_skill_athlete_stats, { 0.48, 0.10 }, { 10.00, 4 } ); } } @@ -220,7 +235,7 @@ TEST_CASE( "throwing_skill_impact_test", "[throwing],[balance]" ) // the throwing skill has while the sanity tests are more explicit. SECTION( "mid_skill_basestats_rock" ) { test_throwing_player_versus( p, "mon_zombie", "rock", 5, mid_skill_base_stats, { 1.00, 0.10 }, { 12, 6 } ); - test_throwing_player_versus( p, "mon_zombie", "rock", 10, mid_skill_base_stats, { 0.86, 0.10 }, { 7, 4 } ); + test_throwing_player_versus( p, "mon_zombie", "rock", 10, mid_skill_base_stats, { 0.92, 0.10 }, { 7, 4 } ); test_throwing_player_versus( p, "mon_zombie", "rock", 15, mid_skill_base_stats, { 0.62, 0.10 }, { 5, 2 } ); }