Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[READY] Ranged balance unit test #21468

Merged
merged 36 commits into from Sep 27, 2017
Merged
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a27531f
Make some ballistics methods visible so we can use them in tests.
kevingranade Aug 2, 2017
41fe5bd
Extract map test helpers to a seperate module.
kevingranade Aug 2, 2017
6857af5
Add wald error calculation to statistics object.
kevingranade Aug 2, 2017
659e011
Add ranged balance test.
kevingranade Aug 2, 2017
efaeb5a
Extract equipping NPC.
kevingranade Aug 17, 2017
d8188da
Give test shooters gear to encumber them correctly.
kevingranade Aug 17, 2017
64bf89e
Loosen quickdraw requirements.
kevingranade Aug 18, 2017
830b8b0
Don't alow high perception to negate sight_dispersion penalty.
kevingranade Aug 30, 2017
cfefcbe
Inflate MAX_RECOIL, recoil accumulation and recoil recovery to resolv…
kevingranade Aug 18, 2017
376a66a
Refactor firing test to expose dispersion object.
kevingranade Aug 24, 2017
565d93f
Switch from an aim ratio to an aim time for firing test.
kevingranade Aug 24, 2017
a998a10
Add ostream insertion operator for accuracy test.
kevingranade Aug 27, 2017
23b3459
Restore dex and perception-based ranged penalties.
kevingranade Aug 31, 2017
a494b7b
Remove ranged soft cap.
kevingranade Sep 1, 2017
d673fc7
Add gun mods to accuracy test.
kevingranade Sep 2, 2017
d3df289
Add test to output range thresholds.
kevingranade Sep 2, 2017
aa4738a
Prototype adjustments to bring ranged balance into line.
kevingranade Sep 2, 2017
6cf1076
Increase confidence interval required for statistical validity.
kevingranade Sep 3, 2017
0eb6bdf
Add fast shooting tests.
kevingranade Sep 7, 2017
1b1aaae
Add upper limit for fast shooting test.
kevingranade Sep 9, 2017
8a008a9
Fix the upper aim speed tests.
kevingranade Sep 9, 2017
f4b6529
Extract calculation of various aim speed modifiers to helper methods.
kevingranade Sep 15, 2017
ed95c53
Enlarge info panel of status menu.
kevingranade Sep 16, 2017
4ed686d
Fix NPC aiming confidence estimate.
kevingranade Sep 16, 2017
c6c8100
Fliped aim_cost to aim_speed.
kevingranade Sep 16, 2017
dc2633c
Added line to hand encumbrance info about adjustment to aim speed.
kevingranade Sep 16, 2017
c97255f
Factor aim type list generation out of target_ui.
kevingranade Sep 18, 2017
5ec583c
Emit more feedback about accuracy and aim time in item descriptions o…
kevingranade Sep 18, 2017
8f9ab52
Rescale set points for aiming to match new dispersion levels.
kevingranade Sep 18, 2017
2a42e3d
Reset recoil when switching targets during burst fire.
kevingranade Sep 18, 2017
f216151
Extract steadiness bar from ranged chance print function.
kevingranade Sep 19, 2017
21c6417
Add dynamic feedback to aim menu.
kevingranade Sep 19, 2017
3799457
Add time to fire to aiming menu.
kevingranade Sep 21, 2017
78cdcf0
Fix accuracy estimation code by using dispersion max instead of average.
kevingranade Sep 26, 2017
07b94a2
Tweak some stat handling.
kevingranade Sep 26, 2017
ae61a67
Fix steadiness calculations.
kevingranade Sep 26, 2017
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -52,7 +52,7 @@
"location": "rail",
"mod_targets": [ "smg", "rifle", "pistol", "shotgun", "crossbow", "launcher" ],
"sight_dispersion": 360,
"aim_cost": 2,
"aim_speed": 8,
"min_skills": [ [ "weapon", 2 ], [ "gun", 1 ] ]
},
{
@@ -14,7 +14,7 @@
"location": "sights",
"mod_targets": [ "bow" ],
"sight_dispersion": 45,
"aim_cost": 6
"aim_speed": 4
},
{
"id": "holo_sight",
@@ -30,7 +30,7 @@
"location": "sights",
"mod_targets": [ "smg", "rifle", "shotgun", "crossbow", "pistol", "launcher" ],
"sight_dispersion": 60,
"aim_cost": 6,
"aim_speed": 6,
"min_skills": [ [ "weapon", 2 ], [ "gun", 3 ] ],
"flags": [ "DISABLE_SIGHTS" ]
},
@@ -50,7 +50,7 @@
"location": "sights",
"mod_targets": [ "smg", "rifle", "pistol", "shotgun", "crossbow", "launcher" ],
"sight_dispersion": 120,
"aim_cost": 6,
"aim_speed": 6,
"min_skills": [ [ "weapon", 2 ], [ "gun", 1 ] ],
"flags": [ "DISABLE_SIGHTS" ]
},
@@ -68,7 +68,7 @@
"location": "sights",
"mod_targets": [ "pistol", "crossbow" ],
"sight_dispersion": 30,
"aim_cost": 10,
"aim_speed": 2,
"min_skills": [ [ "weapon", 2 ], [ "gun", 4 ] ],
"flags": [ "DISABLE_SIGHTS" ]
},
@@ -87,7 +87,7 @@
"location": "sights",
"mod_targets": [ "smg", "rifle", "shotgun", "pistol", "crossbow", "launcher" ],
"sight_dispersion": 90,
"aim_cost": 4,
"aim_speed": 6,
"min_skills": [ [ "weapon", 2 ], [ "gun", 2 ] ],
"flags": [ "DISABLE_SIGHTS" ]
},
@@ -106,7 +106,7 @@
"mod_targets": [ "rifle", "crossbow", "launcher" ],
"install_time": 30000,
"sight_dispersion": 0,
"aim_cost": 10,
"aim_speed": 0,
"min_skills": [ [ "weapon", 2 ], [ "gun", 4 ] ],
"flags": [ "DISABLE_SIGHTS", "ZOOM" ]
},
@@ -120,7 +120,7 @@
"volume": 0,
"price": 0,
"material": [ "superalloy", "ceramic" ],
"proportional": { "aim_cost": 0.8 },
"aim_speed": 2,
"flags": [ "ZOOM", "IRREMOVABLE" ]
}
]
@@ -164,7 +164,7 @@
"location": "underbarrel",
"mod_targets": [ "smg", "rifle", "pistol", "shotgun", "crossbow", "bow", "launcher" ],
"sight_dispersion": 300,
"aim_cost": 1,
"aim_speed": 9,
"min_skills": [ [ "weapon", 2 ], [ "gun", 1 ] ]
},
{
@@ -219,6 +219,8 @@ classes = {
{ name = "pick_name", rval = nil, args = { "bool" } },
{ name = "random_bad_trait", rval = "trait_id", args = { } },
{ name = "random_good_trait", rval = "trait_id", args = { } },
{ name = "ranged_dex_mod", rval = "int", args = { } },
{ name = "ranged_per_mod", rval = "int", args = { } },
{ name = "recalc_hp", rval = nil, args = { } },
{ name = "recalc_sight_limits", rval = nil, args = { } },
{ name = "remove_mission_items", rval = nil, args = { "int" } },
@@ -722,6 +724,8 @@ classes = {
{ name = "process_effects", rval = nil, args = { } },
{ name = "process_turn", rval = nil, args = { } },
{ name = "purifiable", rval = "bool", args = { "trait_id" } },
{ name = "ranged_dex_mod", rval = "int", args = { } },
{ name = "ranged_per_mod", rval = "int", args = { } },
{ name = "reach_attack", rval = nil, args = { "tripoint" } },
{ name = "read", rval = "bool", args = { "int" } },
{ name = "read_speed", rval = "int", args = { "bool" } },
@@ -113,14 +113,7 @@ static size_t blood_trail_len( int damage )
return 0;
}

/** Aim result for a single projectile attack */
struct projectile_attack_aim {
double missed_by; ///< Hit quality, where 0.0 is a perfect hit and 1.0 is a miss
double missed_by_tiles; ///< Number of tiles the attack missed by
double dispersion; ///< Dispersion of this particular shot in arcminutes
};

static projectile_attack_aim projectile_attack_roll( dispersion_sources dispersion, double range,
projectile_attack_aim projectile_attack_roll( dispersion_sources dispersion, double range,
double target_size )
{
projectile_attack_aim aim;
@@ -9,6 +9,19 @@ struct dealt_projectile_attack;
struct projectile;
struct tripoint;

/** Aim result for a single projectile attack */
struct projectile_attack_aim {
double missed_by; ///< Hit quality, where 0.0 is a perfect hit and 1.0 is a miss
double missed_by_tiles; ///< Number of tiles the attack missed by
double dispersion; ///< Dispersion of this particular shot in arcminutes
};

/**
* Evaluates dispersion sources, range, and target to determine attack trajectory.
**/
projectile_attack_aim projectile_attack_roll( dispersion_sources dispersion, double range,
double target_size );

/**
* Fires a projectile at the target point from the source point with total_dispersion
* dispersion.
@@ -63,6 +63,17 @@ constexpr int DAYS( int n )
return n * HOURS( 24 );
}

/**
* Convert ticks to seconds.
*
* @param ticks number of ticks
* @returns Time in seconds
*/
constexpr int TICKS_TO_SECONDS( int ticks )
{
return static_cast<int>( static_cast<float>( ticks ) / 16.67 );
}

/** How much light moon provides per lit-up quarter (Full-moon light is four times this value) */
#define MOONLIGHT_PER_QUARTER 2.25

@@ -180,70 +180,129 @@ void Character::mod_stat( const std::string &stat, float modifier )

int Character::effective_dispersion( int dispersion ) const
{
/** @EFFECT_PER improves effectiveness of gun sights */
dispersion += ( 10 - per_cur ) * 15;
/** @EFFECT_PER penalizes sight dispersion when low. */
dispersion += ranged_per_mod();

dispersion += encumb( bp_eyes );

return std::max( dispersion, 0 );
}

double Character::aim_per_move( const item& gun, double recoil ) const
std::pair<int, int> Character::get_best_sight( const item &gun, double recoil ) const
{
if( !gun.is_gun() ) {
return 0;
}

// get fastest sight that can be used to improve aim further below @ref recoil
int cost = INT_MAX;
// Get fastest sight that can be used to improve aim further below @ref recoil.
int sight_speed_modifier = INT_MIN;
int limit = 0;
if( !gun.has_flag( "DISABLE_SIGHTS" ) && effective_dispersion( gun.type->gun->sight_dispersion ) < recoil ) {
cost = std::max( std::min( gun.volume() / 250_ml, 8 ), 1 );
sight_speed_modifier = 6;
limit = effective_dispersion( gun.type->gun->sight_dispersion );
}

for( const auto e : gun.gunmods() ) {
const auto mod = e->type->gunmod.get();
if( mod->sight_dispersion < 0 || mod->aim_cost <= 0 ) {
if( mod->sight_dispersion < 0 || mod->aim_speed < 0 ) {
continue; // skip gunmods which don't provide a sight
}
if( effective_dispersion( mod->sight_dispersion ) < recoil && mod->aim_cost < cost ) {
cost = mod->aim_cost;
if( effective_dispersion( mod->sight_dispersion ) < recoil && mod->aim_speed > sight_speed_modifier ) {
sight_speed_modifier = mod->aim_speed;
limit = effective_dispersion( mod->sight_dispersion );
}
}
return std::make_pair( sight_speed_modifier, limit );
}

if( cost == INT_MAX ) {
return 0; // no suitable sights (already at maxium aim)
double Character::aim_speed_skill_modifier( const skill_id &gun_skill ) const
{
double skill_mult = 1.0;
if( gun_skill == "pistol" ) {
skill_mult = 2.0;
} else if( gun_skill == "rifle" ) {
skill_mult = 0.9;
}

// each 5 points (combined) of hand encumbrance increases aim cost by one unit
cost += round ( ( encumb( bp_hand_l ) + encumb( bp_hand_r ) ) / 10.0 );

/** @EFFECT_DEX increases aiming speed */
cost += 8 - dex_cur;

/** @EFFECT_PISTOL increases aiming speed for pistols */
/** @EFFECT_SMG increases aiming speed for SMGs */
/** @EFFECT_RIFLE increases aiming speed for rifles */
/** @EFFECT_SHOTGUN increases aiming speed for shotguns */
/** @EFFECT_LAUNCHER increases aiming speed for launchers */
cost += ( ( MAX_SKILL / 2 ) - get_skill_level( gun.gun_skill() ) ) * 2;
return skill_mult * std::min( MAX_SKILL, static_cast<int>( get_skill_level( gun_skill ) ) );
}

double Character::aim_speed_dex_modifier() const
{
return get_dex() - 8;
}

double Character::aim_speed_encumbrance_modifier() const
{
return ( encumb( bp_hand_l ) + encumb( bp_hand_r ) ) / 10.0;
}

double Character::aim_cap_from_volume( const item &gun ) const
{
skill_id gun_skill = gun.gun_skill();
double aim_cap = std::min( 49.0, 49.0 - static_cast<float>( gun.volume() / 75_ml ) );
// TODO: also scale with skill level.
if( gun_skill == "smg" ) {
aim_cap = std::max( 12.0, aim_cap );
} else if( gun_skill == "shotgun" ) {
aim_cap = std::max( 12.0, aim_cap );
} else if( gun_skill == "pistol" ) {
aim_cap = std::max( 15.0, aim_cap * 1.25 );
} else if( gun_skill == "rifle" ) {
aim_cap = std::max( 7.0, aim_cap - 5.0 );
} else { // Launchers, etc.
aim_cap = std::max( 10.0, aim_cap );
}
return aim_cap;
}

double Character::aim_per_move( const item &gun, double recoil ) const
{
if( !gun.is_gun() ) {
return 0.0;
}

std::pair<int, int> best_sight = get_best_sight( gun, recoil );
int sight_speed_modifier = best_sight.first;
int limit = best_sight.second;
if( sight_speed_modifier == INT_MIN ) {
// No suitable sights (already at maxium aim).
return 0;
}

// Overal strategy for determining aim speed is to sum the factors that contribute to it,
// then scale that speed by current recoil level.
// Player capabilities make aiming faster, and aim speed slows down as it approaches 0.
// Base speed is non-zero to prevent extreme rate changes as aim speed approaches 0.
double aim_speed = 10.0;

skill_id gun_skill = gun.gun_skill();
// Ranges [0 - 10]
aim_speed += aim_speed_skill_modifier( gun_skill );

cost = std::max( cost, 1 );
// Range [0 - 12]
/** @EFFECT_DEX increases aiming speed */
aim_speed += aim_speed_dex_modifier();

// Range [0 - 10]
aim_speed += sight_speed_modifier;

// Each 5 points (combined) of hand encumbrance decreases aim speed by one unit.
aim_speed -= aim_speed_encumbrance_modifier();

// constant at which one unit of aim cost ~75 moves
// (presuming aiming from nil to maximum aim via single sight at DEX 8)
int k = 25;
aim_speed = std::min( aim_speed, aim_cap_from_volume( gun ) );

// calculate rate (b) from the exponential function y = a(1-b)^x where a is recoil
double improv = 1.0 - pow( 0.5, 1.0 / ( cost * k ) );
// Just a raw scaling factor.
aim_speed *= 6.5;

// minimum improvment is 0.1MoA
double aim = std::max( recoil * improv, 0.1 );
// Scale rate logistically as recoil goes from MAX_RECOIL to 0.
aim_speed *= 1.0 - logarithmic_range( 0, MAX_RECOIL, recoil );

// never improve by more than the currently used sights permit
return std::min( aim, recoil - limit );
// Minimum improvment is 5MoA. This mostly puts a cap on how long aiming for sniping takes.
aim_speed = std::max( aim_speed, 5.0 );

// Never improve by more than the currently used sights permit.
return std::min( aim_speed, recoil - limit );
}

bool Character::move_effects(bool attacking)
@@ -1563,6 +1622,18 @@ int Character::get_int_bonus() const
return int_bonus;
}

int Character::ranged_dex_mod() const
{
///\EFFECT_DEX <20 increases ranged penalty
return std::max( ( 20.0 - get_dex() ) * 2.25, 0.0 );
}

int Character::ranged_per_mod() const
{
///\EFFECT_PER <20 increases ranged aiming penalty.
return std::max( ( 20.0 - get_per() ) * 2.25, 0.0 );
}

int Character::get_healthy() const
{
return healthy;
@@ -61,6 +61,14 @@ struct encumbrance_data {
}
};

struct aim_type {
std::string name;
std::string action;
std::string help;
bool has_threshold;
int threshold;
};

class Character : public Creature, public visitable<Character>
{
public:
@@ -104,6 +112,10 @@ class Character : public Creature, public visitable<Character>
virtual int get_per_bonus() const;
virtual int get_int_bonus() const;

// Penalty modifiers applied for ranged attacks due to low stats
virtual int ranged_dex_mod() const;
virtual int ranged_per_mod() const;

/** Setters for stats exclusive to characters */
virtual void set_str_bonus(int nstr);
virtual void set_dex_bonus(int ndex);
@@ -152,6 +164,14 @@ class Character : public Creature, public visitable<Character>
/* Adjusts provided sight dispersion to account for player stats */
int effective_dispersion( int dispersion ) const;

/* Accessors for aspects of aim speed. */
std::vector<aim_type> get_aim_types( const item &gun ) const;
std::pair<int, int> get_best_sight( const item &gun, double recoil ) const;
double aim_speed_skill_modifier( const skill_id &gun_skill ) const;
double aim_speed_dex_modifier() const;
double aim_speed_encumbrance_modifier() const;
double aim_cap_from_volume( const item &gun ) const;

/* Calculate aim improvement per move spent aiming at a given @ref recoil */
double aim_per_move( const item &gun, double recoil ) const;

@@ -2,6 +2,7 @@
#ifndef DISPERSION_H
#define DISPERSION_H

#include<iosfwd>
#include <vector>

class dispersion_sources
@@ -25,6 +26,8 @@ class dispersion_sources
double roll() const;
double max() const;
double avg() const;

friend std::ostream &operator<<( std::ostream &stream, const dispersion_sources &sources );
};

#endif
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.