projectile_attack_aim projectile_attack_roll( dispersion_sources dispersion, double range,
        double target_size )
{
    projectile_attack_aim aim;

    // dispersion is a measure of the dispersion of shots due to the gun + shooter characteristics
    // i.e. it is independent of any particular shot

    // shot_dispersion is the actual dispersion for this particular shot, i.e.
    // the error angle between where the shot was aimed and where this one actually went
    // NB: some cases pass dispersion == 0 for a "never misses" shot e.g. bio_magnet,
    aim.dispersion = dispersion.roll();

    // an isosceles triangle is formed by the intended and actual target tiles
    aim.missed_by_tiles = iso_tangent( range, aim.dispersion );

    // fraction we missed a monster target by (0.0 = perfect hit, 1.0 = miss)
    if( target_size > 0.0 ) {
        aim.missed_by = std::min( 1.0, aim.missed_by_tiles / target_size );
    } else {
        // Special case 0 size targets, just to be safe from 0.0/0.0 NaNs
        aim.missed_by = 1.0;
    }

    return aim;
}
#include "catch/catch.hpp"

#include "game.h"
#include "npc.h"
#include "item_factory.h"

static void test_internal( const npc& who, const item &gun )
{
    THEN( "the effective range is correctly calcuated" ) {
        // calculate range for 50% chance of critical hit at arbitrary recoil
        double recoil = rng_float( 0, 1000 );
        double range = who.gun_current_range( gun, recoil, 50, accuracy_critical );

        // calculate actual accuracy at the given range
        double dispersion = ( who.get_weapon_dispersion( gun ) + recoil ) / 2;
        double missed_by = iso_tangent( range, dispersion );

        INFO( "Recoil: " << recoil );
        INFO( "Range: " << range );
        INFO( "Dispersion: " << dispersion );

        // require inverse calculation to agree with tolerance of 0.1%
        REQUIRE( std::abs( missed_by - accuracy_critical ) < accuracy_critical / 1000 );
    }

    THEN( "the snapshot range is less than the effective range" ) {
        REQUIRE( who.gun_engagement_range( gun, player::engagement::snapshot ) <=
                 who.gun_engagement_range( gun, player::engagement::effective ) );
    }

    THEN( "the effective range is less than maximum range" ) {