Skip to content
Permalink
Tree: 7ee93bc46d
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
1695 lines (1462 sloc) 66.2 KB
#include "ranged.h"
#include "ballistics.h"
#include "cata_utility.h"
#include "dispersion.h"
#include "game.h"
#include "map.h"
#include "debug.h"
#include "output.h"
#include "line.h"
#include "skill.h"
#include "string_formatter.h"
#include "rng.h"
#include "item.h"
#include "options.h"
#include "action.h"
#include "input.h"
#include "messages.h"
#include "projectile.h"
#include "sounds.h"
#include "translations.h"
#include "monster.h"
#include "npc.h"
#include "trap.h"
#include "itype.h"
#include "vehicle.h"
#include "field.h"
#include "mtype.h"
#include <algorithm>
#include <vector>
#include <string>
#include <cmath>
const skill_id skill_throw( "throw" );
const skill_id skill_gun( "gun" );
const skill_id skill_driving( "driving" );
const skill_id skill_dodge( "dodge" );
const skill_id skill_launcher( "launcher" );
const efftype_id effect_on_roof( "on_roof" );
const efftype_id effect_hit_by_player( "hit_by_player" );
static const trait_id trait_TRIGGERHAPPY( "TRIGGERHAPPY" );
static const trait_id trait_HOLLOW_BONES( "HOLLOW_BONES" );
static const trait_id trait_LIGHT_BONES( "LIGHT_BONES" );
static projectile make_gun_projectile( const item &gun );
int time_to_fire( const Character &p, const itype &firing );
static void cycle_action( item& weap, const tripoint &pos );
void make_gun_sound_effect(player &p, bool burst, item *weapon);
extern bool is_valid_in_w_terrain(int, int);
static double occupied_tile_fraction( m_size target_size )
{
switch( target_size ) {
case MS_TINY:
return 0.1;
case MS_SMALL:
return 0.25;
case MS_MEDIUM:
return 0.5;
case MS_LARGE:
return 0.75;
case MS_HUGE:
return 1.0;
}
return 0.5;
}
double Creature::ranged_target_size() const
{
return occupied_tile_fraction( get_size() );
}
int range_with_even_chance_of_good_hit( int dispersion )
{
// Empirically determined by "synthetic_range_test" in tests/ranged_balance.cpp.
static const std::array<int, 59> dispersion_for_even_chance_of_good_hit = {{
1731, 859, 573, 421, 341, 286, 245, 214, 191, 175,
151, 143, 129, 118, 114, 107, 101, 94, 90, 78,
78, 78, 74, 71, 68, 66, 62, 61, 59, 57,
46, 46, 46, 46, 46, 46, 45, 45, 44, 42,
41, 41, 39, 39, 38, 37, 36, 35, 34, 34,
33, 33, 32, 30, 30, 30, 30, 29, 28
}};
int even_chance_range = 0;
while( static_cast<unsigned>( even_chance_range ) <
dispersion_for_even_chance_of_good_hit.size() &&
dispersion < dispersion_for_even_chance_of_good_hit[ even_chance_range ] ) {
even_chance_range++;
}
return even_chance_range;
}
int player::gun_engagement_moves( const item &gun, int target, int start ) const
{
int mv = 0;
double penalty = start;
while( penalty > target ) {
double adj = aim_per_move( gun, penalty );
if( adj <= 0 ) {
break;
}
penalty -= adj;
mv++;
}
return mv;
}
bool player::handle_gun_damage( item &it )
{
if( !it.is_gun() ) {
debugmsg( "Tried to handle_gun_damage of a non-gun %s", it.tname().c_str() );
return false;
}
const auto &curammo_effects = it.ammo_effects();
const islot_gun *firing = it.type->gun.get();
// Here we check if we're underwater and whether we should misfire.
// As a result this causes no damage to the firearm, note that some guns are waterproof
// and so are immune to this effect, note also that WATERPROOF_GUN status does not
// mean the gun will actually be accurate underwater.
if (is_underwater() && !it.has_flag("WATERPROOF_GUN") && one_in(firing->durability)) {
add_msg_player_or_npc(_("Your %s misfires with a wet click!"),
_("<npcname>'s %s misfires with a wet click!"),
it.tname().c_str());
return false;
// Here we check for a chance for the weapon to suffer a mechanical malfunction.
// Note that some weapons never jam up 'NEVER_JAMS' and thus are immune to this
// effect as current guns have a durability between 5 and 9 this results in
// a chance of mechanical failure between 1/64 and 1/1024 on any given shot.
// the malfunction may cause damage, but never enough to push the weapon beyond 'shattered'
} else if ((one_in(2 << firing->durability)) && !it.has_flag("NEVER_JAMS")) {
add_msg_player_or_npc(_("Your %s malfunctions!"),
_("<npcname>'s %s malfunctions!"),
it.tname().c_str());
if( it.damage() < it.max_damage() && one_in( 4 * firing->durability ) ) {
add_msg_player_or_npc(m_bad, _("Your %s is damaged by the mechanical malfunction!"),
_("<npcname>'s %s is damaged by the mechanical malfunction!"),
it.tname().c_str());
// Don't increment until after the message
it.inc_damage();
}
return false;
// Here we check for a chance for the weapon to suffer a misfire due to
// using OEM bullets. Note that these misfires cause no damage to the weapon and
// some types of ammunition are immune to this effect via the NEVER_MISFIRES effect.
} else if (!curammo_effects.count("NEVER_MISFIRES") && one_in(1728)) {
add_msg_player_or_npc(_("Your %s misfires with a dry click!"),
_("<npcname>'s %s misfires with a dry click!"),
it.tname().c_str());
return false;
// Here we check for a chance for the weapon to suffer a misfire due to
// using player-made 'RECYCLED' bullets. Note that not all forms of
// player-made ammunition have this effect the misfire may cause damage, but never
// enough to push the weapon beyond 'shattered'.
} else if (curammo_effects.count("RECYCLED") && one_in(256)) {
add_msg_player_or_npc(_("Your %s misfires with a muffled click!"),
_("<npcname>'s %s misfires with a muffled click!"),
it.tname().c_str());
if( it.damage() < it.max_damage() && one_in( firing->durability ) ) {
add_msg_player_or_npc(m_bad, _("Your %s is damaged by the misfired round!"),
_("<npcname>'s %s is damaged by the misfired round!"),
it.tname().c_str());
// Don't increment until after the message
it.inc_damage();
}
return false;
}
return true;
}
int player::fire_gun( const tripoint &target, int shots )
{
return fire_gun( target, shots, weapon );
}
int player::fire_gun( const tripoint &target, int shots, item& gun )
{
if( !gun.is_gun() ) {
debugmsg( "%s tried to fire non-gun (%s).", name.c_str(), gun.tname().c_str() );
return 0;
}
// Number of shots to fire is limited by the ammount of remaining ammo
if( gun.ammo_required() ) {
shots = std::min( shots, int( gun.ammo_remaining() / gun.ammo_required() ) );
}
// cap our maximum burst size by the amount of UPS power left
if( !gun.has_flag( "VEHICLE" ) && gun.get_gun_ups_drain() > 0 ) {
shots = std::min( shots, int( charges_of( "UPS" ) / gun.get_gun_ups_drain() ) );
}
if( shots <= 0 ) {
debugmsg( "Attempted to fire zero or negative shots using %s", gun.tname().c_str() );
}
// usage of any attached bipod is dependent upon terrain
bool bipod = g->m.has_flag_ter_or_furn( "MOUNTABLE", pos() );
if( !bipod ) {
auto veh = g->m.veh_at( pos() );
bipod = veh && veh->has_part( pos(), "MOUNTABLE" );
}
// Up to 50% of recoil can be delayed until end of burst dependent upon relevant skill
/** @EFFECT_PISTOL delays effects of recoil during autoamtic fire */
/** @EFFECT_SMG delays effects of recoil during automatic fire */
/** @EFFECT_RIFLE delays effects of recoil during automatic fire */
/** @EFFECT_SHOTGUN delays effects of recoil during automatic fire */
double absorb = std::min( int( get_skill_level( gun.gun_skill() ) ), MAX_SKILL ) / double( MAX_SKILL * 2 );
tripoint aim = target;
int curshot = 0;
int hits = 0; // total shots on target
int delay = 0; // delayed recoil that has yet to be applied
while( curshot != shots ) {
if( !handle_gun_damage( gun ) ) {
break;
}
dispersion_sources dispersion = get_weapon_dispersion( gun );
dispersion.add_range( recoil_total() );
// If this is a vehicle mounted turret, which vehicle is it mounted on?
const vehicle *in_veh = has_effect( effect_on_roof ) ? g->m.veh_at( pos() ) : nullptr;
auto shot = projectile_attack( make_gun_projectile( gun ), pos(), aim, dispersion, this, in_veh );
curshot++;
int qty = gun.gun_recoil( *this, bipod );
delay += qty * absorb;
// Temporaraly scale by 5x as we adjust MAX_RECOIL.
recoil += 5.0 * ( qty * ( 1.0 - absorb ) );
make_gun_sound_effect( *this, shots > 1, &gun );
sfx::generate_gun_sound( *this, gun );
cycle_action( gun, pos() );
if( gun.ammo_consume( gun.ammo_required(), pos() ) != gun.ammo_required() ) {
debugmsg( "Unexpected shortage of ammo whilst firing %s", gun.tname().c_str() );
break;
}
if( !gun.has_flag( "VEHICLE" ) ) {
use_charges( "UPS", gun.get_gun_ups_drain() );
}
if( shot.missed_by <= .1 ) {
lifetime_stats.headshots++; // @todo check head existence for headshot
}
if( shot.hit_critter ) {
hits++;
}
if( gun.gun_skill() == skill_launcher ) {
continue; // skip retargeting for launchers
}
// If burst firing and we killed the target then try to retarget
const auto critter = g->critter_at( aim, true );
if( !critter || critter->is_dead_state() ) {
// Reset recoil, remainder of burst will be wild.
recoil = MAX_RECOIL;
// Find suitable targets that are in range, hostile and near any previous target
auto hostiles = get_targetable_creatures( gun.gun_range( this ) );
hostiles.erase( std::remove_if( hostiles.begin(), hostiles.end(), [&]( const Creature *z ) {
if( rl_dist( z->pos(), aim ) > get_skill_level( skill_gun ) ) {
return true; /** @EFFECT_GUN increases range of automatic retargeting during burst fire */
} else if( z->is_dead_state() ) {
return true;
} else if( has_trait( trait_id( "TRIGGERHAPPY" ) ) && one_in( 10 ) ) {
return false; // Trigger happy sometimes doesn't care who we shoot
} else {
return attitude_to( *z ) != A_HOSTILE;
}
} ), hostiles.end() );
if( hostiles.empty() || hostiles.front()->is_dead_state() ) {
break; // We ran out of suitable targets
} else if( !one_in( 7 - get_skill_level( skill_gun ) ) ) {
break; /** @EFFECT_GUN increases chance of firing multiple times in a burst */
}
aim = random_entry( hostiles )->pos();
}
}
// apply delayed recoil
recoil += delay;
// Cap
recoil = std::min( MAX_RECOIL, recoil );
// Use different amounts of time depending on the type of gun and our skill
moves -= time_to_fire( *this, *gun.type );
// Practice the base gun skill proportionally to number of hits, but always by one.
practice( skill_gun, ( hits + 1 ) * 5 );
// launchers train weapon skill for both hits and misses.
int practice_units = gun.gun_skill() == skill_launcher ? curshot : hits;
practice( gun.gun_skill(), ( practice_units + 1 ) * 5 );
return curshot;
}
// @todo Method
int throw_cost( const player &c, const item &to_throw )
{
// Very similar to player::attack_speed
// @todo Extract into a function?
// Differences:
// Dex is more (2x) important for throwing speed
// At 10 skill, the cost is down to 0.75%, not 0.66%
const int base_move_cost = to_throw.attack_time() / 2;
const int throw_skill = std::min<int>( MAX_SKILL, c.get_skill_level( skill_throw ) );
///\EFFECT_THROW increases throwing speed
const int skill_cost = ( int )( base_move_cost * ( 20 - throw_skill ) / 20 );
///\EFFECT_DEX increases throwing speed
const int dexbonus = c.get_dex();
const int encumbrance_penalty = c.encumb( bp_torso ) +
( c.encumb( bp_hand_l ) + c.encumb( bp_hand_r ) ) / 2;
const float stamina_ratio = ( float )c.stamina / c.get_stamina_max();
const float stamina_penalty = 1.0 + std::max( ( 0.25f - stamina_ratio ) * 4.0f, 0.0f );
int move_cost = base_move_cost;
// Stamina penalty only affects base/2 and encumbrance parts of the cost
move_cost += encumbrance_penalty;
move_cost *= stamina_penalty;
move_cost += skill_cost;
move_cost -= dexbonus;
if( c.has_trait( trait_LIGHT_BONES ) ) {
move_cost *= .9;
}
if( c.has_trait( trait_HOLLOW_BONES ) ) {
move_cost *= .8;
}
return std::max( 25, move_cost );
}
int Character::throw_dispersion_per_dodge( bool add_encumbrance ) const
{
// +200 per dodge point at 0 dexterity
// +100 at 8, +80 at 12, +66.6 at 16, +57 at 20, +50 at 24
// Each 10 encumbrance on either hand is like -1 dex (can bring penalty to +400 per dodge)
// Maybe @todo Only use one hand
const int encumbrance = add_encumbrance ? encumb( bp_hand_l ) + encumb( bp_hand_r ) : 0;
///\EFFECT_DEX increases throwing accuracy against targets with good dodge stat
float effective_dex = 2 + get_dex() / 4.0f - ( encumbrance ) / 40.0f;
return static_cast<int>( 100.0f / std::max( 1.0f, effective_dex ) );
}
// Perfect situation gives us 1000 dispersion at lvl 0
// This goes down linearly to 250 dispersion at lvl 10
int Character::throwing_dispersion( const item &to_throw, Creature *critter ) const
{
units::mass weight = to_throw.weight();
units::volume volume = to_throw.volume();
if( to_throw.count_by_charges() && to_throw.charges > 1 ) {
weight /= to_throw.charges;
volume /= to_throw.charges;
}
int throw_difficulty = 1000;
// 1000 penalty for every liter after the first
// @todo Except javelin type items
throw_difficulty += std::max<int>( 0, units::to_milliliter( volume - 1000_ml ) );
// 1 penalty for gram above str*100 grams (at 0 skill)
///\EFFECT_STR decreases throwing dispersion when throwing heavy objects
throw_difficulty += std::max( 0, weight / 1_gram - get_str() * 100 );
// Dispersion from difficult throws goes from 100% at lvl 0 to 25% at lvl 10
///\EFFECT_THROW increases throwing accuracy
const int throw_skill = std::min<int>( MAX_SKILL, get_skill_level( skill_throw ) );
int dispersion = 10 * throw_difficulty / ( 3 * throw_skill + 10 );
// If the target is a creature, it moves around and ruins aim
// @todo Inform projectile functions if the attacker actually aims for the critter or just the tile
if( critter != nullptr ) {
// It's easier to dodge at close range (thrower needs to adjust more)
// Dodge x10 at point blank, x5 at 1 dist, then flat
float effective_dodge = critter->get_dodge() * std::max( 1, 10 - 5 * rl_dist( pos(), critter->pos() ) );
dispersion += throw_dispersion_per_dodge( true ) * effective_dodge;
}
// 1 perception per 1 eye encumbrance
///\EFFECT_PER decreases throwing accuracy penalty from eye encumbrance
dispersion += std::max( 0, ( encumb( bp_eyes ) - get_per() ) * 10 );
return std::max( 0, dispersion );
}
dealt_projectile_attack player::throw_item( const tripoint &target, const item &to_throw )
{
// Copy the item, we may alter it before throwing
item thrown = to_throw;
const int move_cost = throw_cost( *this, to_throw );
mod_moves( -move_cost );
units::volume volume = to_throw.volume();
units::mass weight = to_throw.weight();
const int stamina_cost = ( ( weight / 100_gram ) + 20 ) * -1;
mod_stat( "stamina", stamina_cost );
const skill_id &skill_used = skill_throw;
const int skill_level = std::min<int>( MAX_SKILL, get_skill_level( skill_throw ) );
// We'll be constructing a projectile
projectile proj;
proj.impact = thrown.base_damage_thrown();
proj.speed = 10 + skill_level;
auto &impact = proj.impact;
auto &proj_effects = proj.proj_effects;
static const std::set<material_id> ferric = { material_id( "iron" ), material_id( "steel" ) };
bool do_railgun = has_active_bionic( bionic_id( "bio_railgun" ) ) && thrown.made_of_any( ferric );
// The damage dealt due to item's weight and player's strength
// Up to str/2 or weight/100g (lower), so 10 str is 5 damage before multipliers
// Railgun doubles the effective strength
///\EFFECT_STR increases throwing damage
impact.add_damage( DT_BASH, std::min( weight / 100.0_gram, do_railgun ? get_str() : ( get_str() / 2.0 ) ) );
if( thrown.has_flag( "ACT_ON_RANGED_HIT" ) ) {
proj_effects.insert( "ACT_ON_RANGED_HIT" );
thrown.active = true;
}
// Item will shatter upon landing, destroying the item, dealing damage, and making noise
/** @EFFECT_STR increases chance of shattering thrown glass items (NEGATIVE) */
const bool shatter = !thrown.active && thrown.made_of( material_id( "glass" ) ) &&
rng( 0, units::to_milliliter( 2000_ml - volume ) ) < get_str() * 100;
// Add some flags to the projectile
if( weight > 500_gram ) {
proj_effects.insert( "HEAVY_HIT" );
}
proj_effects.insert( "NO_ITEM_DAMAGE" );
if( thrown.active ) {
// Can't have molotovs embed into mons
// Mons don't have inventory processing
proj_effects.insert( "NO_EMBED" );
}
if( do_railgun ) {
proj_effects.insert( "LIGHTNING" );
}
if( volume > 500_ml ) {
proj_effects.insert( "WIDE" );
}
// Deal extra cut damage if the item breaks
if( shatter ) {
impact.add_damage( DT_CUT, units::to_milliliter( volume ) / 500.0f );
proj_effects.insert( "SHATTER_SELF" );
}
// Some minor (skill/2) armor piercing for skillfull throws
// Not as much as in melee, though
for( damage_unit &du : impact.damage_units ) {
du.res_pen += skill_level / 2.0f;
}
// Put the item into the projectile
proj.set_drop( std::move( thrown ) );
if( thrown.has_flag( "CUSTOM_EXPLOSION" ) ) {
proj.set_custom_explosion( thrown.type->explosion );
}
float range = rl_dist( pos(), target );
proj.range = range;
// Prevent light items from landing immediately
proj.momentum_loss = std::min( impact.total_damage() / 10.0f, 1.0f );
int skill_lvl = get_skill_level( skill_used );
// Avoid awarding tons of xp for lucky throws against hard to hit targets
const float range_factor = std::min<float>( range, skill_lvl + 3 );
// We're aiming to get a damaging hit, not just an accurate one - reward proper weapons
const float damage_factor = 5.0f * sqrt( proj.impact.total_damage() / 5.0f );
// This should generally have values below ~20*sqrt(skill_lvl)
const float final_xp_mult = range_factor * damage_factor;
Creature *critter = g->critter_at( target, true );
const dispersion_sources dispersion = throwing_dispersion( thrown, critter );
auto dealt_attack = projectile_attack( proj, pos(), target, dispersion, this );
const double missed_by = dealt_attack.missed_by;
if( missed_by <= 0.1 && dealt_attack.hit_critter != nullptr ) {
practice( skill_used, final_xp_mult, MAX_SKILL );
// TODO: Check target for existence of head
lifetime_stats.headshots++;
} else if( dealt_attack.hit_critter != nullptr && missed_by > 0.0f ) {
practice( skill_used, final_xp_mult / ( 1.0f + missed_by ), MAX_SKILL );
} else {
// Pure grindy practice - cap gain at lvl 2
practice( skill_used, 5, 2 );
}
return dealt_attack;
}
static std::string print_recoil( const player &p)
{
if( p.weapon.is_gun() ) {
const int val = p.recoil_total();
const int min_recoil = p.effective_dispersion( p.weapon.sight_dispersion() );
const int recoil_range = MAX_RECOIL - min_recoil;
const char *color_name = "c_light_gray";
if( val >= min_recoil + ( recoil_range * 2 / 3 ) ) {
color_name = "c_red";
} else if( val >= min_recoil + ( recoil_range / 2 ) ) {
color_name = "c_light_red";
} else if( val >= min_recoil + ( recoil_range / 4 ) ) {
color_name = "c_yellow";
}
return string_format("<color_%s>%s</color>", color_name, _("Recoil"));
}
return std::string();
}
// Draws the static portions of the targeting menu,
// returns the number of lines used to draw instructions.
static int draw_targeting_window( WINDOW *w_target, const std::string &name, player &p, target_mode mode,
input_context &ctxt, const std::vector<aim_type> &aim_types,
bool switch_mode, bool switch_ammo, bool tiny )
{
draw_border(w_target);
// Draw the "title" of the window.
mvwprintz(w_target, 0, 2, c_white, "< ");
std::string title;
switch( mode ) {
case TARGET_MODE_FIRE:
case TARGET_MODE_TURRET_MANUAL:
title = string_format( _( "Firing %s %s" ), name.c_str(), print_recoil( p ).c_str() );
break;
case TARGET_MODE_THROW:
title = string_format( _( "Throwing %s" ), name.c_str() );
break;
default:
title = _( "Set target" );
}
trim_and_print( w_target, 0, 4, getmaxx(w_target) - 7, c_red, "%s", title.c_str() );
wprintz(w_target, c_white, " >");
// Draw the help contents at the bottom of the window, leaving room for monster description
// and aiming status to be drawn dynamically.
// The - 2 accounts for the window border.
// If tiny is set we're critically low on space, let the final line overwrite the border.
int text_y = getmaxy(w_target) - ( tiny ? 1 : 2 );
if( is_mouse_enabled() ) {
// Reserve a line for mouse instructions.
--text_y;
}
// Reserve lines for aiming and firing instructions.
if( mode == TARGET_MODE_FIRE ) {
text_y -= ( 3 + aim_types.size() );
} else {
text_y -= 2;
}
text_y -= switch_mode ? 1 : 0;
text_y -= switch_ammo ? 1 : 0;
// The -1 is the -2 from above, but adjusted since this is a total, not an index.
int lines_used = getmaxy( w_target ) - 1 - text_y;
mvwprintz(w_target, text_y++, 1, c_white, _("Move cursor to target with directional keys"));
auto const front_or = [&](std::string const &s, char const fallback) {
auto const keys = ctxt.keys_bound_to(s);
return keys.empty() ? fallback : keys.front();
};
if( mode == TARGET_MODE_FIRE || mode == TARGET_MODE_TURRET_MANUAL ) {
mvwprintz( w_target, text_y++, 1, c_white, _("%c %c Cycle targets; %c to fire."),
front_or("PREV_TARGET", ' '), front_or("NEXT_TARGET", ' '), front_or("FIRE", ' ') );
mvwprintz( w_target, text_y++, 1, c_white, _("%c target self; %c toggle snap-to-target"),
front_or("CENTER", ' ' ), front_or("TOGGLE_SNAP_TO_TARGET", ' ') );
}
if( mode == TARGET_MODE_FIRE ) {
mvwprintz( w_target, text_y++, 1, c_white, _( "%c to steady your aim. " ), front_or( "AIM", ' ' ) );
for( const auto &e : aim_types ) {
if( e.has_threshold){
mvwprintz( w_target, text_y++, 1, c_white, e.help.c_str(), front_or( e.action, ' ') );
}
}
mvwprintz( w_target, text_y++, 1, c_white, _( "%c to switch aiming modes." ), front_or( "SWITCH_AIM", ' ' ) );
}
if( switch_mode ) {
mvwprintz( w_target, text_y++, 1, c_white, _( "%c to switch firing modes." ), front_or( "SWITCH_MODE", ' ' ) );
}
if( switch_ammo) {
mvwprintz( w_target, text_y++, 1, c_white, _( "%c to switch ammo." ), front_or( "SWITCH_AMMO", ' ' ) );
}
if( is_mouse_enabled() ) {
mvwprintz(w_target, text_y++, 1, c_white,
_("Mouse: LMB: Target, Wheel: Cycle, RMB: Fire"));
}
return lines_used;
}
static int find_target( const std::vector<Creature *> &t, const tripoint &tpos ) {
for( size_t i = 0; i < t.size(); ++i ) {
if( t[i]->pos() == tpos ) {
return int( i );
}
}
return -1;
}
static int do_aim( player &p, const std::vector<Creature *> &t, int cur_target,
const item &relevant, const tripoint &tpos )
{
// If we've changed targets, reset aim, unless it's above the minimum.
if( size_t( cur_target ) >= t.size() || t[cur_target]->pos() != tpos ) {
cur_target = find_target( t, tpos );
// TODO: find radial offset between targets and
// spend move points swinging the gun around.
p.recoil = MAX_RECOIL;
}
const double aim_amount = p.aim_per_move( relevant, p.recoil );
if( aim_amount > 0 ) {
// Increase aim at the cost of moves
p.mod_moves( -1 );
p.recoil = std::max( 0.0, p.recoil - aim_amount );
} else {
// If aim is already maxed, we're just waiting, so pass the turn.
p.set_moves( 0 );
}
return cur_target;
}
struct confidence_rating {
double aim_level;
char symbol;
std::string label;
};
static int print_steadiness( WINDOW *w, int line_number, double steadiness )
{
const int window_width = getmaxx( w ) - 2; // Window width minus borders.
if( get_option<std::string>( "ACCURACY_DISPLAY" ) == "numbers" ) {
std::string steadiness_s = string_format( "%s: %d%%", _( "Steadiness" ),
(int)( 100.0 * steadiness ) );
mvwprintw( w, line_number++, 1, "%s", steadiness_s.c_str() );
} else {
const std::string &steadiness_bar = get_labeled_bar( steadiness, window_width,
_( "Steadiness" ), '*' );
mvwprintw( w, line_number++, 1, steadiness_bar.c_str() );
}
return line_number;
}
static double confidence_estimate( int range, double target_size, dispersion_sources dispersion )
{
// This is a rough estimate of accuracy based on a linear distribution across min and max
// dispersion. It is highly inaccurate probability-wise, but this is intentional, the player
// is not doing gaussian integration in their head while aiming. The result gives the player
// correct relative measures of chance to hit, and corresponds with the actual distribution at
// min, max, and mean.
const double max_lateral_offset = iso_tangent( range, dispersion.max() );
return 1 / ( max_lateral_offset / target_size );
}
static std::vector<aim_type> get_default_aim_type()
{
std::vector<aim_type> aim_types;
aim_types.push_back( aim_type { "", "", "", false, 0 } ); // dummy aim type for unaimed shots
return aim_types;
}
static int print_ranged_chance( const player &p, WINDOW *w, int line_number, target_mode mode,
const item &ranged_weapon, dispersion_sources dispersion,
const std::vector<confidence_rating> &confidence_config,
double range, double target_size, int recoil = 0 )
{
const int window_width = getmaxx( w ) - 2; // Window width minus borders.
std::string display_type = get_option<std::string>( "ACCURACY_DISPLAY" );
std::vector<aim_type> aim_types;
if ( mode == TARGET_MODE_THROW ) {
aim_types = get_default_aim_type();
} else {
aim_types = p.get_aim_types( ranged_weapon );
}
if( display_type != "numbers" ) {
mvwprintw( w, line_number++, 1, _( "Symbols: * = Headshot + = Hit | = Graze" ) );
}
for( const aim_type type : aim_types ) {
dispersion_sources current_dispersion = dispersion;
int threshold = MAX_RECOIL;
std::string label = _( "Current Aim" );
if( type.has_threshold ) {
label = type.name;
threshold = type.threshold;
current_dispersion.add_range( threshold );
} else {
current_dispersion.add_range( recoil );
}
int moves_to_fire;
if ( mode == TARGET_MODE_THROW ) {
moves_to_fire = throw_cost( p, ranged_weapon );
} else {
moves_to_fire = p.gun_engagement_moves( ranged_weapon, threshold, recoil ) + time_to_fire( p, *ranged_weapon.type );
}
mvwprintw( w, line_number++, 1, _( "%s: Moves to fire: %d" ), label.c_str(), moves_to_fire );
double confidence = confidence_estimate( range, target_size, current_dispersion );
if( display_type == "numbers" ) {
int last_chance = 0;
std::string confidence_s = enumerate_as_string( confidence_config.begin(), confidence_config.end(),
[&]( const confidence_rating &config ) {
// @todo Consider not printing 0 chances, but only if you can print something (at least miss 100% or so)
int chance = std::min<int>( 100, 100.0 * ( config.aim_level * confidence ) ) - last_chance;
last_chance += chance;
return string_format( "%s: %3d%%", config.label.c_str(), chance );
}, false );
line_number += fold_and_print_from( w, line_number, 1, window_width, 0,
c_white, confidence_s );
} else {
// Extract pairs from tuples, because get_labeled_bar expects pairs
std::vector<std::pair<double, char>> confidence_ratings;
std::transform( confidence_config.begin(), confidence_config.end(), std::back_inserter( confidence_ratings ),
[&]( const confidence_rating &config ) {
return std::make_pair( config.aim_level, config.symbol );
} );
const std::string &confidence_bar = get_labeled_bar( confidence, window_width, "",
confidence_ratings.begin(),
confidence_ratings.end() );
mvwprintw( w, line_number++, 1, confidence_bar.c_str() );
}
}
return line_number;
}
static int print_aim( const player &p, WINDOW *w, int line_number, item *weapon,
Creature &target, double predicted_recoil ) {
// This is absolute accuracy for the player.
// TODO: push the calculations duplicated from Creature::deal_projectile_attack() and
// Creature::projectile_attack() into shared methods.
// Dodge doesn't affect gun attacks
dispersion_sources dispersion = p.get_weapon_dispersion( *weapon );
dispersion.add_range( p.recoil_vehicle() );
const double min_dispersion = p.effective_dispersion( p.weapon.sight_dispersion() );
const double steadiness_range = MAX_RECOIL - min_dispersion;
// This is a relative measure of how steady the player's aim is,
// 0 is the best the player can do.
const double steady_score = std::max( 0.0, predicted_recoil - min_dispersion );
// Fairly arbitrary cap on steadiness...
const double steadiness = 1.0 - ( steady_score / steadiness_range );
const double target_size = target.ranged_target_size();
// This could be extracted, to allow more/less verbose displays
static const std::vector<confidence_rating> confidence_config = {{
{ accuracy_headshot, '*', _( "Head" ) },
{ accuracy_goodhit, '+', _( "Hit" ) },
{ accuracy_grazing, '|', _( "Graze" ) }
}};
const double range = rl_dist( p.pos(), target.pos() );
line_number = print_steadiness( w, line_number, steadiness );
return print_ranged_chance( p, w, line_number, TARGET_MODE_FIRE, *weapon, dispersion, confidence_config,
range, target_size, predicted_recoil );
}
static int draw_turret_aim( const player &p, WINDOW *w, int line_number, const tripoint &targ )
{
vehicle *veh = g->m.veh_at( p.pos() );
if( veh == nullptr ) {
debugmsg( "Tried to aim turret while outside vehicle" );
return line_number;
}
// fetch and display list of turrets that are ready to fire at the target
auto turrets = veh->turrets( targ );
mvwprintw( w, line_number++, 1, _("Turrets in range: %d"), turrets.size() );
for( const auto e : turrets ) {
mvwprintw( w, line_number++, 1, "* %s", e->name().c_str() );
}
return line_number;
}
static int draw_throw_aim( const player &p, WINDOW *w, int line_number,
const item *weapon, const tripoint &target_pos )
{
Creature *target = g->critter_at( target_pos, true );
if( target != nullptr && !p.sees( *target ) ) {
target = nullptr;
}
const dispersion_sources dispersion = p.throwing_dispersion( *weapon, target );
const double range = rl_dist( p.pos(), target_pos );
const double target_size = target != nullptr ? target->ranged_target_size() : 1.0f;
static const std::vector<confidence_rating> confidence_config_critter = {{
{ accuracy_headshot, '*', _( "Headshot" ) },
{ accuracy_goodhit, '+', _( "Hit" ) },
{ accuracy_grazing, '|', _( "Graze" ) }
}};
static const std::vector<confidence_rating> confidence_config_object = {{
{ accuracy_grazing, '*', _( "Hit" ) }
}};
const auto &confidence_config = target != nullptr ?
confidence_config_critter : confidence_config_object;
return print_ranged_chance( p, w, line_number, TARGET_MODE_THROW, *weapon, dispersion, confidence_config,
range, target_size );
}
std::vector<tripoint> target_handler::target_ui( player &pc, const targeting_data &args )
{
return target_ui( pc, args.mode, args.relevant, args.range,
args.ammo, args.on_mode_change, args.on_ammo_change );
}
std::vector<aim_type> Character::get_aim_types( const item &gun ) const
{
std::vector<aim_type> aim_types = get_default_aim_type();
if( !gun.is_gun() ) {
return aim_types;
}
int sight_dispersion = effective_dispersion( gun.sight_dispersion() );
// Aiming thresholds are dependent on weapon sight dispersion, attempting to place thresholds
// at 10%, 5% and 0% of the difference between MAX_RECOIL and sight dispersion.
std::vector<int> thresholds = {
static_cast<int>( ( ( MAX_RECOIL - sight_dispersion ) / 10.0 ) + sight_dispersion ),
static_cast<int>( ( ( MAX_RECOIL - sight_dispersion ) / 20.0 ) + sight_dispersion ),
static_cast<int>( sight_dispersion ) };
std::vector<int>::iterator thresholds_it;
// Remove duplicate thresholds.
thresholds_it = std::adjacent_find( thresholds.begin(), thresholds.end() );
while( thresholds_it != thresholds.end() ) {
thresholds.erase( thresholds_it );
thresholds_it = std::adjacent_find( thresholds.begin(), thresholds.end() );
}
thresholds_it = thresholds.begin();
aim_types.push_back( aim_type { _( "Regular Aim" ), "AIMED_SHOT", _( "%c to aim and fire." ),
true, *thresholds_it } );
thresholds_it++;
if( thresholds_it != thresholds.end() ) {
aim_types.push_back( aim_type { _( "Careful Aim" ), "CAREFUL_SHOT",
_( "%c to take careful aim and fire." ), true,
*thresholds_it } );
thresholds_it++;
}
if( thresholds_it != thresholds.end() ) {
aim_types.push_back( aim_type { _( "Precise Aim" ), "PRECISE_SHOT",
_( "%c to take precise aim and fire." ), true,
*thresholds_it } );
}
return aim_types;
}
// @todo: Shunt redundant drawing code elsewhere
std::vector<tripoint> target_handler::target_ui( player &pc, target_mode mode,
item *relevant, int range, const itype *ammo,
const target_callback &on_mode_change,
const target_callback &on_ammo_change )
{
// @todo: this should return a reference to a static vector which is cleared on each call.
static const std::vector<tripoint> empty_result{};
std::vector<tripoint> ret;
int sight_dispersion = 0;
if ( !relevant ) {
relevant = &pc.weapon;
} else {
sight_dispersion = pc.effective_dispersion( relevant->sight_dispersion() );
}
tripoint src = pc.pos();
tripoint dst = pc.pos();
std::vector<Creature *> t;
int target;
auto update_targets = [&]( int range, std::vector<Creature *>& targets, int &idx, tripoint &dst ) {
targets = pc.get_targetable_creatures( range );
targets.erase( std::remove_if( targets.begin(), targets.end(), [&]( const Creature *e ) {
return pc.attitude_to( *e ) == Creature::Attitude::A_FRIENDLY;
} ), targets.end() );
if( targets.empty() ) {
idx = -1;
return;
}
std::sort( targets.begin(), targets.end(), [&]( const Creature *lhs, const Creature *rhs ) {
return rl_dist( lhs->pos(), pc.pos() ) < rl_dist( rhs->pos(), pc.pos() );
} );
// @todo: last_target should be member of target_handler
const auto found = std::find( targets.begin(), targets.end(), g->last_target.lock().get() );
idx = found != targets.end() ? std::distance( targets.begin(), found ) : 0;
dst = targets[ target ]->pos();
};
update_targets( range, t, target, dst );
bool compact = TERMY < 41;
bool tiny = TERMY < 31;
// Defaut to the maximum window size we can use.
int height = 31;
int top = use_narrow_sidebar() ? getbegy( g->w_messages ) : getbegy( g->w_minimap ) + getmaxy( g->w_minimap );
if( tiny ) {
// If we're extremely short on space, use the whole sidebar.
top = 0;
height = TERMY;
} else if( compact ) {
// Cover up more low-value ui elements if we're tight on space.
top -= 4;
height = 25;
} else {
// Cover up the redundant weapon line.
top -= 1;
}
catacurses::window w_target = catacurses::newwin( height, getmaxx( g->w_messages ), top, getbegx( g->w_messages ) );
input_context ctxt("TARGET");
ctxt.set_iso(true);
// "ANY_INPUT" should be added before any real help strings
// Or strings will be written on window border.
ctxt.register_action( "ANY_INPUT" );
ctxt.register_directions();
ctxt.register_action( "COORDINATE" );
ctxt.register_action( "SELECT" );
ctxt.register_action( "FIRE" );
ctxt.register_action( "NEXT_TARGET" );
ctxt.register_action( "PREV_TARGET" );
ctxt.register_action( "LEVEL_UP" );
ctxt.register_action( "LEVEL_DOWN" );
ctxt.register_action( "CENTER" );
ctxt.register_action( "TOGGLE_SNAP_TO_TARGET" );
ctxt.register_action( "HELP_KEYBINDINGS" );
ctxt.register_action( "QUIT" );
ctxt.register_action( "SWITCH_MODE" );
ctxt.register_action( "SWITCH_AMMO" );
if( mode == TARGET_MODE_FIRE ) {
ctxt.register_action( "AIM" );
ctxt.register_action( "SWITCH_AIM" );
}
std::vector<aim_type> aim_types;
std::vector<aim_type>::iterator aim_mode;
if( mode == TARGET_MODE_FIRE ) {
aim_types = pc.get_aim_types( *relevant );
for( aim_type &type : aim_types ) {
if( type.has_threshold ) {
ctxt.register_action( type.action );
}
}
aim_mode = aim_types.begin();
}
int num_instruction_lines = draw_targeting_window( w_target, relevant ? relevant->tname() : "",
pc, mode, ctxt, aim_types,
bool( on_mode_change ),
bool( on_ammo_change ), tiny );
bool snap_to_target = get_option<bool>( "SNAP_TO_TARGET" );
std::string enemiesmsg;
if( t.empty() ) {
enemiesmsg = _( "No targets in range." );
} else {
enemiesmsg = string_format( ngettext( "%d target in range.", "%d targets in range.",
t.size()), t.size());
}
const auto set_last_target = []( const tripoint &dst ) {
if( const Creature *const critter_ptr = g->critter_at( dst, true ) ) {
g->last_target = g->shared_from( *critter_ptr );
}
};
const auto confirm_non_enemy_target = [&pc]( const tripoint &dst ) {
if( dst == pc.pos() ) {
return true;
}
if( npc * const who_ = g->critter_at<npc>( dst ) ) {
const npc &who = *who_;
if( who.guaranteed_hostile() ) {
return true;
}
return query_yn( _( "Really attack %s?" ), who.name.c_str() );
}
return true;
};
const tripoint old_offset = pc.view_offset;
do {
ret = g->m.find_clear_path( src, dst );
// This chunk of code handles shifting the aim point around
// at maximum range when using circular distance.
// The range > 1 check ensures that you can alweays at least hit adjacent squares.
if( trigdist && range > 1 && round(trig_dist( src, dst )) > range ) {
bool cont = true;
tripoint cp = dst;
for( size_t i = 0; i < ret.size() && cont; i++ ) {
if( round(trig_dist( src, ret[i] )) > range ) {
ret.resize(i);
cont = false;
} else {
cp = ret[i];
}
}
dst = cp;
}
tripoint center;
if( snap_to_target ) {
center = dst;
} else {
center = pc.pos() + pc.view_offset;
}
// Clear the target window.
for( int i = 1; i <= getmaxy( w_target ) - num_instruction_lines - 2; i++ ) {
// Clear width excluding borders.
for( int j = 1; j <= getmaxx( w_target ) - 2; j++ ) {
mvwputch( w_target, i, j, c_white, ' ' );
}
}
g->draw_ter( center, true );
int line_number = 1;
Creature *critter = g->critter_at( dst, true );
if( dst != src ) {
// Only draw those tiles which are on current z-level
auto ret_this_zlevel = ret;
ret_this_zlevel.erase( std::remove_if( ret_this_zlevel.begin(), ret_this_zlevel.end(),
[&center]( const tripoint &pt ) { return pt.z != center.z; } ), ret_this_zlevel.end() );
// Only draw a highlighted trajectory if we can see the endpoint.
// Provides feedback to the player, and avoids leaking information
// about tiles they can't see.
g->draw_line( dst, center, ret_this_zlevel );
// Print to target window
mvwprintw( w_target, line_number++, 1, _( "Range: %d/%d, %s" ),
rl_dist( src, dst ), range, enemiesmsg.c_str() );
} else {
mvwprintw( w_target, line_number++, 1, _("Range: %d, %s"), range, enemiesmsg.c_str() );
}
// Skip blank lines if we're short on space.
if( !compact ) {
line_number++;
}
if( mode == TARGET_MODE_FIRE || mode == TARGET_MODE_TURRET_MANUAL ) {
auto m = relevant->gun_current_mode();
if( relevant != m.target ) {
mvwprintw( w_target, line_number++, 1, _( "Firing mode: %s %s (%d)" ),
m->tname().c_str(), m.mode.c_str(), m.qty );
} else {
mvwprintw( w_target, line_number++, 1, _( "Firing mode: %s (%d)" ),
m.mode.c_str(), m.qty );
}
const itype *cur = ammo ? ammo : m->ammo_data();
if( cur ) {
auto str = string_format( m->ammo_remaining() ?
_( "Ammo: <color_%s>%s</color> (%d/%d)" ) :
_( "Ammo: <color_%s>%s</color>" ),
get_all_colors().get_name( cur->color ).c_str(),
cur->nname( std::max( m->ammo_remaining(), 1L ) ).c_str(),
m->ammo_remaining(), m->ammo_capacity() );
nc_color col = c_light_gray;
print_colored_text( w_target, line_number++, 1, col, col, str );
}
// Skip blank lines if we're short on space.
if( !compact ) {
line_number++;
}
}
if( critter && critter != &pc && pc.sees( *critter ) ) {
// The 12 is 2 for the border and 10 for aim bars.
// Just print the monster name if we're short on space.
int available_lines = compact ? 1 : ( height - num_instruction_lines - line_number - 12 );
line_number = critter->print_info( w_target, line_number, available_lines, 1 );
} else {
mvwputch( g->w_terrain, POSY + dst.y - center.y, POSX + dst.x - center.x, c_red, '*' );
}
if( mode == TARGET_MODE_FIRE && critter != nullptr && pc.sees( *critter ) ) {
double predicted_recoil = pc.recoil;
int predicted_delay = 0;
if( aim_mode->has_threshold && aim_mode->threshold < pc.recoil ) {
do{
const double aim_amount = pc.aim_per_move( *relevant, predicted_recoil );
if( aim_amount > 0 ) {
predicted_delay++;
predicted_recoil = std::max( predicted_recoil - aim_amount, 0.0 );
}
} while( predicted_recoil > aim_mode->threshold &&
predicted_recoil - sight_dispersion > 0 );
} else {
predicted_recoil = pc.recoil;
}
line_number = print_aim( pc, w_target, line_number, &*relevant->gun_current_mode(), *critter, predicted_recoil );
if( aim_mode->has_threshold ) {
mvwprintw(w_target, line_number++, 1, _("%s Delay: %i"), aim_mode->name.c_str(), predicted_delay );
}
} else if( mode == TARGET_MODE_TURRET ) {
line_number = draw_turret_aim( pc, w_target, line_number, dst );
} else if( mode == TARGET_MODE_THROW ) {
line_number = draw_throw_aim( pc, w_target, line_number, relevant, dst );
}
wrefresh(w_target);
wrefresh(g->w_terrain);
refresh();
std::string action;
if( pc.activity.id() == activity_id( "ACT_AIM" ) && pc.activity.str_values[0] != "AIM" ) {
// If we're in 'aim and shoot' mode,
// skip retrieving input and go straight to the action.
action = pc.activity.str_values[0];
} else {
action = ctxt.handle_input();
}
// Clear the activity if any, we'll re-set it later if we need to.
pc.cancel_activity();
tripoint targ( 0, 0, 0 );
// Our coordinates will either be determined by coordinate input(mouse),
// by a direction key, or by the previous value.
if( action == "SELECT" && ctxt.get_coordinates(g->w_terrain, targ.x, targ.y) ) {
if( !get_option<bool>( "USE_TILES" ) && snap_to_target ) {
// Snap to target doesn't currently work with tiles.
targ.x += dst.x - src.x;
targ.y += dst.y - src.y;
}
targ.x -= dst.x;
targ.y -= dst.y;
} else {
ctxt.get_direction(targ.x, targ.y, action);
if( targ.x == -2 ) {
targ.x = 0;
targ.y = 0;
}
}
if( action == "FIRE" && mode == TARGET_MODE_FIRE && aim_mode->has_threshold ) {
action = aim_mode->action;
}
if( g->m.has_zlevels() && action == "LEVEL_UP" ) {
dst.z++;
pc.view_offset.z++;
} else if( g->m.has_zlevels() && action == "LEVEL_DOWN" ) {
dst.z--;
pc.view_offset.z--;
}
/* More drawing to terrain */
if( targ != tripoint_zero ) {
const Creature *critter = g->critter_at( dst, true );
if( critter != nullptr ) {
g->draw_critter( *critter, center );
} else if( g->m.pl_sees( dst, -1 ) ) {
g->m.drawsq( g->w_terrain, pc, dst, false, true, center );
} else {
mvwputch( g->w_terrain, POSY, POSX, c_black, 'X' );
}
// constrain by range
dst.x = std::min( std::max( dst.x + targ.x, src.x - range ), src.x + range );
dst.y = std::min( std::max( dst.y + targ.y, src.y - range ), src.y + range );
dst.z = std::min( std::max( dst.z + targ.z, src.z - range ), src.z + range );
} else if( (action == "PREV_TARGET") && (target != -1) ) {
int newtarget = find_target( t, dst ) - 1;
if( newtarget < 0 ) {
newtarget = t.size() - 1;
}
dst = t[newtarget]->pos();
} else if( (action == "NEXT_TARGET") && (target != -1) ) {
int newtarget = find_target( t, dst ) + 1;
if( newtarget == (int)t.size() ) {
newtarget = 0;
}
dst = t[newtarget]->pos();
} else if( (action == "AIM") && target != -1 ) {
// No confirm_non_enemy_target here because we have not initiated the firing.
// Aiming can be stopped / aborted at any time.
for( int i = 0; i != 10; ++i ) {
target = do_aim( pc, t, target, *relevant, dst );
}
if( pc.moves <= 0 ) {
// We've run out of moves, clear target vector, but leave target selected.
pc.assign_activity( activity_id( "ACT_AIM" ), 0, 0 );
pc.activity.str_values.push_back( "AIM" );
pc.view_offset = old_offset;
set_last_target( dst );
return empty_result;
}
} else if( action == "SWITCH_MODE" ) {
if( on_mode_change ) {
ammo = on_mode_change( relevant );
}
} else if( action == "SWITCH_AMMO" ) {
if( on_ammo_change ) {
ammo = on_ammo_change( relevant );
}
} else if( action == "SWITCH_AIM" ) {
aim_mode++;
if( aim_mode == aim_types.end() ) {
aim_mode = aim_types.begin();
}
} else if( (action == "AIMED_SHOT" || action == "CAREFUL_SHOT" || action == "PRECISE_SHOT") &&
target != -1 ) {
// This action basically means "FIRE" as well, the actual firing may be delayed
// through aiming, but there is usually no means to stop it. Therefor we query here.
if( !confirm_non_enemy_target( dst ) ) {
continue;
}
int aim_threshold;
std::vector<aim_type>::iterator it;
for( it = aim_types.begin(); it != aim_types.end(); it++ ) {
if( action == it->action ) {
break;
}
}
if( it == aim_types.end() ) {
debugmsg( "Could not find a valid aim_type for %s", action.c_str() );
aim_mode = aim_types.begin();
}
aim_threshold = it->threshold;
do {
target = do_aim( pc, t, target, *relevant, dst );
} while( target != -1 && pc.moves > 0 && pc.recoil > aim_threshold &&
pc.recoil - sight_dispersion > 0 );
if( target == -1 ) {
// Bail out if there's no target.
continue;
}
if( pc.recoil <= aim_threshold ||
pc.recoil - sight_dispersion == 0) {
// If we made it under the aim threshold, go ahead and fire.
// Also fire if we're at our best aim level already.
delwin( w_target );
pc.view_offset = old_offset;
set_last_target( dst );
return ret;
} else {
// We've run out of moves, set the activity to aim so we'll
// automatically re-enter the targeting menu next turn.
// Set the string value of the aim action to the right thing
// so we re-enter this loop.
// Also clear target vector, but leave target selected.
pc.assign_activity( activity_id( "ACT_AIM" ), 0, 0 );
pc.activity.str_values.push_back( action );
pc.view_offset = old_offset;
set_last_target( dst );
return empty_result;
}
} else if( action == "FIRE" ) {
if( !confirm_non_enemy_target( dst ) ) {
continue;
}
target = find_target( t, dst );
if( src == dst ) {
ret.clear();
}
break;
} else if( action == "CENTER" ) {
dst = src;
ret.clear();
} else if( action == "TOGGLE_SNAP_TO_TARGET" ) {
snap_to_target = !snap_to_target;
} else if (action == "QUIT") { // return empty vector (cancel)
ret.clear();
target = -1;
break;
}
} while (true);
delwin( w_target );
pc.view_offset = old_offset;
if( ret.empty() ) {
return ret;
}
set_last_target( ret.back() );
const auto lt_ptr = g->last_target.lock();
if( npc * const guy = dynamic_cast<npc*>( lt_ptr.get() ) ) {
if( !guy->guaranteed_hostile() ) {
// TODO: get rid of this. Or combine it with effect_hit_by_player
guy->hit_by_player = true; // used for morale penalty
}
// TODO: should probably go into the on-hit code?
guy->make_angry();
} else if( monster * const mon = dynamic_cast<monster*>( lt_ptr.get() ) ) {
// TODO: get rid of this. Or move into the on-hit code?
mon->add_effect( effect_hit_by_player, 100 );
}
return ret;
}
static projectile make_gun_projectile( const item &gun ) {
projectile proj;
proj.speed = 1000;
proj.impact = damage_instance::physical( 0, gun.gun_damage(), 0, gun.gun_pierce() );
proj.range = gun.gun_range();
proj.proj_effects = gun.ammo_effects();
auto &fx = proj.proj_effects;
if( ( gun.ammo_data() && gun.ammo_data()->phase == LIQUID ) ||
fx.count( "SHOT" ) || fx.count( "BOUNCE" ) ) {
fx.insert( "WIDE" );
}
if( gun.ammo_data() ) {
// Some projectiles have a chance of being recoverable
bool recover = std::any_of(fx.begin(), fx.end(), []( const std::string& e ) {
int n;
return sscanf( e.c_str(), "RECOVER_%i", &n ) == 1 && !one_in( n );
});
if( recover && !fx.count( "IGNITE" ) && !fx.count( "EXPLOSIVE" ) ) {
item drop( gun.ammo_current(), calendar::turn, 1 );
drop.active = fx.count( "ACT_ON_RANGED_HIT" );
proj.set_drop( drop );
}
const auto ammo = gun.ammo_data()->ammo.get();
if( ammo->drop != "null" && x_in_y( ammo->drop_chance, 1.0 ) ) {
item drop( ammo->drop );
if( ammo->drop_active ) {
drop.activate();
}
proj.set_drop( drop );
}
if( fx.count( "CUSTOM_EXPLOSION" ) > 0 ) {
proj.set_custom_explosion( gun.ammo_data()->explosion );
}
}
return proj;
}
int time_to_fire( const Character &p, const itype &firingt )
{
struct time_info_t {
int min_time; // absolute floor on the time taken to fire.
int base; // the base or max time taken to fire.
int reduction; // the reduction in time given per skill level.
};
static std::map<skill_id, time_info_t> const map {
{skill_id {"pistol"}, {10, 80, 10}},
{skill_id {"shotgun"}, {70, 150, 25}},
{skill_id {"smg"}, {20, 80, 10}},
{skill_id {"rifle"}, {30, 150, 15}},
{skill_id {"archery"}, {20, 220, 25}},
{skill_id {"throw"}, {50, 220, 25}},
{skill_id {"launcher"}, {30, 200, 20}},
{skill_id {"melee"}, {50, 200, 20}}
};
const skill_id &skill_used = firingt.gun.get()->skill_used;
auto const it = map.find( skill_used );
// TODO: maybe JSON-ize this in some way? Probably as part of the skill class.
static const time_info_t default_info{ 50, 220, 25 };
time_info_t const &info = (it == map.end()) ? default_info : it->second;
return std::max(info.min_time, info.base - info.reduction * p.get_skill_level( skill_used ));
}
static void cycle_action( item& weap, const tripoint &pos ) {
// eject casings and linkages in random direction avoiding walls using player position as fallback
auto tiles = closest_tripoints_first( 1, pos );
tiles.erase( tiles.begin() );
tiles.erase( std::remove_if( tiles.begin(), tiles.end(), [&]( const tripoint& e ) {
return !g->m.passable( e );
} ), tiles.end() );
tripoint eject = tiles.empty() ? pos : random_entry( tiles );
// for turrets try and drop casings or linkages directly to any CARGO part on the same tile
auto veh = g->m.veh_at( pos );
std::vector<vehicle_part *> cargo;
if( veh && weap.has_flag( "VEHICLE" ) ) {
cargo = veh->get_parts( pos, "CARGO" );
}
if( weap.ammo_data() && weap.ammo_data()->ammo->casing != "null" ) {
if( weap.has_flag( "RELOAD_EJECT" ) || weap.gunmod_find( "brass_catcher" ) ) {
weap.contents.push_back( item( weap.ammo_data()->ammo->casing ).set_flag( "CASING" ) );
} else {
if( cargo.empty() ) {
g->m.add_item_or_charges( eject, item( weap.ammo_data()->ammo->casing ) );
} else {
veh->add_item( *cargo.front(), item( weap.ammo_data()->ammo->casing ) );
}
sfx::play_variant_sound( "fire_gun", "brass_eject", sfx::get_heard_volume( eject ),
sfx::get_heard_angle( eject ) );
}
}
// some magazines also eject disintegrating linkages
const auto mag = weap.magazine_current();
if( mag && mag->type->magazine->linkage != "NULL" ) {
item linkage( mag->type->magazine->linkage, calendar::turn, 1 );
if( weap.gunmod_find( "brass_catcher" ) ) {
linkage.set_flag( "CASING" );
weap.contents.push_back( linkage );
}
else if( cargo.empty() ) {
g->m.add_item_or_charges( eject, linkage );
} else {
veh->add_item( *cargo.front(), linkage );
}
}
}
void make_gun_sound_effect(player &p, bool burst, item *weapon)
{
const auto data = weapon->gun_noise( burst );
if( data.volume > 0 ) {
sounds::sound( p.pos(), data.volume, data.sound );
}
}
item::sound_data item::gun_noise( bool const burst ) const
{
if( !is_gun() ) {
return { 0, "" };
}
int noise = type->gun->loudness;
for( const auto mod : gunmods() ) {
noise += mod->type->gunmod->loudness;
}
if( ammo_data() ) {
noise += ammo_data()->ammo->loudness;
}
noise = std::max( noise, 0 );
if( ammo_type() == ammotype( "40mm" ) ) {
// Grenade launchers
return { 8, _( "Thunk!" ) };
} else if( ammo_type() == ammotype( "12mm" ) || ammo_type() == ammotype( "metal_rail" ) ) {
// Railguns
return { 24, _( "tz-CRACKck!" ) };
} else if( ammo_type() == ammotype( "flammable" ) || ammo_type() == ammotype( "66mm" ) ||
ammo_type() == ammotype( "84x246mm" ) || ammo_type() == ammotype( "m235" ) ) {
// Rocket launchers and flamethrowers
return { 4, _( "Fwoosh!" ) };
}
auto fx = ammo_effects();
if( fx.count( "LASER" ) || fx.count( "PLASMA" ) ) {
if( noise < 20 ) {
return { noise, _( "Fzzt!" ) };
} else if( noise < 40 ) {
return { noise, _( "Pew!" ) };
} else if( noise < 60 ) {
return { noise, _( "Tsewww!" ) };
} else {
return { noise, _( "Kra-kow!!" ) };
}
} else if( fx.count( "LIGHTNING" ) ) {
if( noise < 20 ) {
return { noise, _( "Bzzt!" ) };
} else if( noise < 40 ) {
return { noise, _( "Bzap!" ) };
} else if( noise < 60 ) {
return { noise, _( "Bzaapp!" ) };
} else {
return { noise, _( "Kra-koom!!" ) };
}
} else if( fx.count( "WHIP" ) ) {
return { noise, _( "Crack!" ) };
} else if( noise > 0 ) {
if( noise < 10 ) {
return { noise, burst ? _( "Brrrip!" ) : _( "plink!" ) };
} else if( noise < 150 ) {
return { noise, burst ? _( "Brrrap!" ) : _( "bang!" ) };
} else if( noise < 175 ) {
return { noise, burst ? _( "P-p-p-pow!" ) : _( "blam!" ) };
} else {
return { noise, burst ? _( "Kaboom!!" ) : _( "kerblam!" ) };
}
}
return { 0, "" }; // silent weapons
}
static bool is_driving( const player &p )
{
const auto veh = g->m.veh_at( p.pos() );
return veh && veh->velocity != 0 && veh->player_in_control( p );
}
// utility functions for projectile_attack
dispersion_sources player::get_weapon_dispersion( const item &obj ) const
{
int weapon_dispersion = obj.gun_dispersion();
dispersion_sources dispersion( weapon_dispersion );
/** @EFFECT_GUN improves usage of accurate weapons and sights */
dispersion.add_range( 3 * ( MAX_SKILL - std::min( int( get_skill_level( skill_gun ) ), MAX_SKILL ) ) );
dispersion.add_range( ranged_dex_mod() );
dispersion.add_range( encumb( bp_arm_l ) + encumb( bp_arm_r ) );
if( is_driving( *this ) ) {
// get volume of gun (or for auxiliary gunmods the parent gun)
const item *parent = has_item( obj ) ? find_parent( obj ) : nullptr;
const int vol = ( parent ? parent->volume() : obj.volume() ) / 250_ml;
/** @EFFECT_DRIVING reduces the inaccuracy penalty when using guns whilst driving */
dispersion.add_range( std::max( vol - get_skill_level( skill_driving ), 1 ) * 20 );
}
if( has_bionic( bionic_id( "bio_targeting" ) ) ) {
dispersion.add_multiplier( 0.75 );
}
if( ( is_underwater() && !obj.has_flag( "UNDERWATER_GUN" ) ) ||
// Range is effectively four times longer when shooting unflagged guns underwater.
( !is_underwater() && obj.has_flag( "UNDERWATER_GUN" ) ) ) {
// Range is effectively four times longer when shooting flagged guns out of water.
dispersion.add_multiplier( 4 );
}
return dispersion;
}
double player::gun_value( const item &weap, long ammo ) const
{
// TODO: Mods
// TODO: Allow using a specified type of ammo rather than default
if( weap.type->gun.get() == nullptr ) {
return 0.0;
}
if( ammo <= 0 ) {
return 0.0;
}
const islot_gun& gun = *weap.type->gun.get();
const itype_id ammo_type = weap.ammo_default( true );
const itype *def_ammo_i = ammo_type != "NULL" ?
item::find_type( ammo_type ) :
nullptr;
float damage_factor = weap.gun_damage( false );
damage_factor += weap.gun_pierce( false ) / 2.0;
item tmp = weap;
tmp.ammo_set( weap.ammo_default() );
int total_dispersion = get_weapon_dispersion( tmp ).max() +
effective_dispersion( tmp.sight_dispersion() );
if( def_ammo_i != nullptr && def_ammo_i->ammo != nullptr ) {
const islot_ammo &def_ammo = *def_ammo_i->ammo;
damage_factor += def_ammo.damage;
damage_factor += def_ammo.pierce / 2;
total_dispersion += def_ammo.dispersion;
}
int move_cost = time_to_fire( *this, *weap.type );
if( gun.clip != 0 && gun.clip < 10 ) {
// @todo RELOAD_ONE should get a penalty here
int reload_cost = gun.reload_time + encumb( bp_hand_l ) + encumb( bp_hand_r );
reload_cost /= gun.clip;
move_cost += reload_cost;
}
// "Medium range" below means 9 tiles, "short range" means 4
// Those are guarantees (assuming maximum time spent aiming)
static const std::vector<std::pair<float, float>> dispersion_thresholds = {{
// Headshots all the time
{ 0.0f, 5.0f },
// Crit at medium range
{ 100.0f, 4.5f },
// Crit at short range or good hit at medium
{ 200.0f, 3.5f },
// OK hits at medium
{ 300.0f, 3.0f },
// Point blank headshots
{ 450.0f, 2.5f },
// OK hits at short
{ 700.0f, 1.5f },
// Glances at medium, crits at point blank
{ 1000.0f, 1.0f },
// Nothing guaranteed, pure gamble
{ 2000.0f, 0.1f },
}};
static const std::vector<std::pair<float, float>> move_cost_thresholds = {{
{ 10.0f, 4.0f },
{ 25.0f, 3.0f },
{ 100.0f, 1.0f },
{ 500.0f, 5.0f },
}};
float move_cost_factor = multi_lerp( move_cost_thresholds, move_cost );
// Penalty for dodging in melee makes the gun unusable in melee
// Until NPCs get proper kiting, at least
int melee_penalty = weapon.volume() / 250_ml - get_skill_level( skill_dodge );
if( melee_penalty <= 0 ) {
// Dispersion matters less if you can just use the gun in melee
total_dispersion = std::min<int>( total_dispersion / move_cost_factor, total_dispersion );
}
float dispersion_factor = multi_lerp( dispersion_thresholds, total_dispersion );
float damage_and_accuracy = damage_factor * dispersion_factor;
// @todo Some better approximation of the ability to keep on shooting
static const std::vector<std::pair<float, float>> capacity_thresholds = {{
{ 1.0f, 0.5f },
{ 5.0f, 1.0f },
{ 10.0f, 1.5f },
{ 20.0f, 2.0f },
{ 50.0f, 3.0f },
}};
// How much until reload
float capacity = gun.clip > 0 ? std::min<float>( gun.clip, ammo ) : ammo;
// How much until dry and a new weapon is needed
capacity += std::min<float>( 1.0, ammo / 20 );
float capacity_factor = multi_lerp( capacity_thresholds, capacity );
double gun_value = damage_and_accuracy * capacity_factor;
add_msg( m_debug, "%s as gun: %.1f total, %.1f dispersion, %.1f damage, %.1f capacity",
weap.tname().c_str(), gun_value, dispersion_factor, damage_factor,
capacity_factor );
return std::max( 0.0, gun_value );
}
You can’t perform that action at this time.