From dea9e639d63176c56c9127778889b80f4a4b7cda Mon Sep 17 00:00:00 2001 From: Curtis Merrill Date: Tue, 23 Feb 2021 02:51:26 -0500 Subject: [PATCH] Prepare npc, spell, character, and item for new ai (#47207) --- src/character.cpp | 2 +- src/character.h | 7 +- src/creature.h | 7 ++ src/item.cpp | 20 +++--- src/item.h | 3 +- src/magic.cpp | 92 ++++++++++++++++++++++-- src/magic.h | 14 +++- src/magic_spell_effect.cpp | 11 +-- src/npc.cpp | 25 +++++++ src/npc.h | 15 ++++ src/npcmove.cpp | 36 +++++++--- src/ranged.cpp | 132 +++++++++++++++++++++++----------- tests/player_helpers.cpp | 46 ++++++++++++ tests/player_helpers.h | 4 ++ tests/ranged_balance_test.cpp | 47 +----------- 15 files changed, 338 insertions(+), 123 deletions(-) diff --git a/src/character.cpp b/src/character.cpp index 913aba7cb565d..6481e3a54239c 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -2676,7 +2676,7 @@ int Character::get_mod_stat_from_bionic( const character_stat &Stat ) const return ret; } -int Character::get_standard_stamina_cost( item *thrown_item ) +int Character::get_standard_stamina_cost( const item *thrown_item ) const { // Previously calculated as 2_gram * std::max( 1, str_cur ) // using 16_gram normalizes it to 8 str. Same effort expenditure diff --git a/src/character.h b/src/character.h index d46b7db26de3a..a348e13a42716 100644 --- a/src/character.h +++ b/src/character.h @@ -501,7 +501,7 @@ class Character : public Creature, public visitable void mod_stat( const std::string &stat, float modifier ) override; - int get_standard_stamina_cost( item *thrown_item = nullptr ); + int get_standard_stamina_cost( const item *thrown_item = nullptr ) const; /**Get bonus to max_hp from excess stored fat*/ int get_fat_to_hp() const; @@ -1635,6 +1635,10 @@ class Character : public Creature, public visitable */ int item_reload_cost( const item &it, const item &ammo, int qty ) const; + projectile thrown_item_projectile( const item &thrown ) const; + int thrown_item_adjusted_damage( const item &thrown ) const; + // calculates the total damage possible from a thrown item, without resistances and such. + int thrown_item_total_damage_raw( const item &thrown ) const; /** Maximum thrown range with a given item, taking all active effects into account. */ int throw_range( const item & ) const; /** Dispersion of a thrown item, against a given target, taking into account whether or not the throw was blind. */ @@ -1847,6 +1851,7 @@ class Character : public Creature, public visitable // gets all the spells known by this character that have this spell class // spells returned are a copy, do not try to edit them from here, instead use known_magic::get_spell std::vector spells_known_of_class( const trait_id &spell_class ) const; + bool cast_spell( spell &sp, bool fake_spell, cata::optional target ); void make_bleed( const effect_source &source, const bodypart_id &bp, time_duration duration, int intensity = 1, bool permanent = false, bool force = false, bool defferred = false ); diff --git a/src/creature.h b/src/creature.h index 720fd01dc6493..011f9cca36a26 100644 --- a/src/creature.h +++ b/src/creature.h @@ -44,6 +44,7 @@ class anatomy; class avatar; class field; class field_entry; +class npc; class player; class time_duration; struct point; @@ -263,6 +264,12 @@ class Creature : public location, public viewer virtual const avatar *as_avatar() const { return nullptr; } + virtual const npc *as_npc() { + return nullptr; + } + virtual const npc *as_npc() const { + return nullptr; + } virtual monster *as_monster() { return nullptr; } diff --git a/src/item.cpp b/src/item.cpp index c2fd90b2ebf73..23dd65b5521b6 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -1493,7 +1493,7 @@ static const double hits_by_accuracy[41] = { 9993, 9997, 9998, 9999, 10000 // 16 to 20 }; -double item::effective_dps( const Character &guy, monster &mon ) const +double item::effective_dps( const Character &guy, Creature &mon ) const { const float mon_dodge = mon.get_dodge(); float base_hit = guy.get_dex() / 4.0f + guy.get_hit_weapon( *this ); @@ -1528,19 +1528,19 @@ double item::effective_dps( const Character &guy, monster &mon ) const // sum average damage past armor and return the number of moves required to achieve // that damage const auto calc_effective_damage = [ &, moves_per_attack]( const double num_strikes, - const bool crit, const Character & guy, monster & mon ) { - monster temp_mon = mon; + const bool crit, const Character & guy, Creature & mon ) { + Creature *temp_mon = &mon; double subtotal_damage = 0; damage_instance base_damage; guy.roll_all_damage( crit, base_damage, true, *this ); damage_instance dealt_damage = base_damage; - temp_mon.absorb_hit( bodypart_id( "torso" ), dealt_damage ); + temp_mon->absorb_hit( bodypart_id( "torso" ), dealt_damage ); dealt_damage_instance dealt_dams; for( const damage_unit &dmg_unit : dealt_damage.damage_units ) { int cur_damage = 0; int total_pain = 0; - temp_mon.deal_damage_handle_type( effect_source::empty(), dmg_unit, bodypart_id( "torso" ), - cur_damage, total_pain ); + temp_mon->deal_damage_handle_type( effect_source::empty(), dmg_unit, bodypart_id( "torso" ), + cur_damage, total_pain ); if( cur_damage > 0 ) { dealt_dams.dealt_dams[ static_cast( dmg_unit.type )] += cur_damage; } @@ -1550,20 +1550,20 @@ double item::effective_dps( const Character &guy, monster &mon ) const double subtotal_moves = moves_per_attack * num_strikes; if( has_technique( RAPID ) ) { - monster temp_rs_mon = mon; + Creature *temp_rs_mon = &mon; damage_instance rs_base_damage; guy.roll_all_damage( crit, rs_base_damage, true, *this ); damage_instance dealt_rs_damage = rs_base_damage; for( damage_unit &dmg_unit : dealt_rs_damage.damage_units ) { dmg_unit.damage_multiplier *= 0.66; } - temp_rs_mon.absorb_hit( bodypart_id( "torso" ), dealt_rs_damage ); + temp_rs_mon->absorb_hit( bodypart_id( "torso" ), dealt_rs_damage ); dealt_damage_instance rs_dealt_dams; for( const damage_unit &dmg_unit : dealt_rs_damage.damage_units ) { int cur_damage = 0; int total_pain = 0; - temp_rs_mon.deal_damage_handle_type( effect_source::empty(), dmg_unit, bodypart_id( "torso" ), - cur_damage, total_pain ); + temp_rs_mon->deal_damage_handle_type( effect_source::empty(), dmg_unit, bodypart_id( "torso" ), + cur_damage, total_pain ); if( cur_damage > 0 ) { rs_dealt_dams.dealt_dams[ static_cast( dmg_unit.type ) ] += cur_damage; } diff --git a/src/item.h b/src/item.h index 6290008679cd7..6f5064ce44ad8 100644 --- a/src/item.h +++ b/src/item.h @@ -34,6 +34,7 @@ #include "visitable.h" class Character; +class Creature; class JsonIn; class JsonObject; class JsonOut; @@ -624,7 +625,7 @@ class item : public visitable * Calculate the item's effective damage per second past armor when wielded by a * character against a monster. */ - double effective_dps( const Character &guy, monster &mon ) const; + double effective_dps( const Character &guy, Creature &mon ) const; /** * calculate effective dps against a stock set of monsters. by default, assume g->u * is wielding diff --git a/src/magic.cpp b/src/magic.cpp index 5c00d08c538d5..f9ddd69fde511 100644 --- a/src/magic.cpp +++ b/src/magic.cpp @@ -8,6 +8,7 @@ #include #include +#include "avatar.h" #include "calendar.h" #include "cata_utility.h" #include "catacharset.h" @@ -33,14 +34,17 @@ #include "make_static.h" #include "magic_enchantment.h" #include "map.h" +#include "map_iterator.h" #include "messages.h" #include "mongroup.h" #include "monster.h" #include "mtype.h" #include "mutation.h" +#include "npc.h" #include "output.h" #include "pimpl.h" #include "point.h" +#include "projectile.h" #include "requirements.h" #include "rng.h" #include "sounds.h" @@ -565,6 +569,18 @@ int spell::min_leveled_damage() const return type->min_damage + std::round( get_level() * type->damage_increment ); } +float spell::dps( const Character &caster, const Creature & ) const +{ + if( type->effect_name != "attack" ) { + return 0.0f; + } + const float time_modifier = 100.0f / casting_time( caster ); + const float failure_modifier = 1.0f - spell_fail( caster ); + const float raw_dps = damage() + damage_dot() * duration_turns() / 1_turns; + // TODO: calculate true dps with armor and resistances and any caster bonuses + return raw_dps * time_modifier * failure_modifier; +} + int spell::damage() const { const int leveled_damage = min_leveled_damage(); @@ -679,6 +695,45 @@ int spell::range() const } } +std::vector spell::targetable_locations( const Character &source ) const +{ + + const tripoint char_pos = source.pos(); + const bool select_ground = is_valid_target( spell_target::ground ); + const bool ignore_walls = has_flag( spell_flag::NO_PROJECTILE ); + map &here = get_map(); + + // TODO: put this in a namespace for reuse + const auto has_obstruction = [&]( const tripoint & at ) { + for( const tripoint &line_point : line_to( char_pos, at ) ) { + if( here.impassable( line_point ) ) { + return true; + } + } + return false; + }; + + std::vector selectable_targets; + for( const tripoint &query : here.points_in_radius( char_pos, range() ) ) { + if( !ignore_walls && has_obstruction( query ) ) { + // it's blocked somewhere! + continue; + } + + if( !select_ground ) { + if( !source.sees( query ) ) { + // can't target a critter you can't see + continue; + } + } + + if( is_valid_target( source, query ) ) { + selectable_targets.push_back( query ); + } + } + return selectable_targets; +} + int spell::min_leveled_duration() const { return type->min_duration + std::round( get_level() * type->duration_increment ); @@ -792,14 +847,17 @@ bool spell::is_spell_class( const trait_id &mid ) const return mid == type->spell_class; } -bool spell::can_cast( Character &guy ) const +bool spell::can_cast( const Character &guy ) const { if( guy.has_trait_flag( STATIC( json_character_flag( "NO_SPELLCASTING" ) ) ) ) { return false; } + // only required because crafting_inventory always rebuilds the cache. maybe a const version doesn't write to cache. + Character &guy_inv = const_cast( guy ); + if( !type->spell_components.is_empty() && - !type->spell_components->can_make_with_inventory( guy.crafting_inventory( guy.pos(), 0 ), + !type->spell_components->can_make_with_inventory( guy_inv.crafting_inventory( guy.pos(), 0 ), return_true ) ) { return false; } @@ -1076,13 +1134,22 @@ void spell::create_field( const tripoint &at ) const } } -void spell::make_sound( const tripoint &target ) const +int spell::sound_volume() const { + int loudness = 0; if( !has_flag( spell_flag::SILENT ) ) { - int loudness = std::abs( damage() ) / 3; + loudness = std::abs( damage() ) / 3; if( has_flag( spell_flag::LOUD ) ) { loudness += 1 + damage() / 3; } + } + return loudness; +} + +void spell::make_sound( const tripoint &target ) const +{ + const int loudness = sound_volume(); + if( loudness > 0 ) { make_sound( target, loudness ); } } @@ -1307,6 +1374,23 @@ dealt_damage_instance spell::get_dealt_damage_instance() const return dmg; } +dealt_projectile_attack spell::get_projectile_attack( const tripoint &target, + Creature &hit_critter ) const +{ + projectile bolt; + bolt.speed = 10000; + bolt.impact = get_damage_instance(); + bolt.proj_effects.emplace( "magic" ); + + dealt_projectile_attack atk; + atk.end_point = target; + atk.hit_critter = &hit_critter; + atk.proj = bolt; + atk.missed_by = 0.0; + + return atk; +} + std::string spell::effect_data() const { return type->effect_str; diff --git a/src/magic.h b/src/magic.h index 9480ab9768a0d..fe4195a46d1b2 100644 --- a/src/magic.h +++ b/src/magic.h @@ -30,6 +30,7 @@ class JsonOut; class nc_color; class spell; class time_duration; +struct dealt_projectile_attack; struct requirement_data; namespace spell_effect @@ -441,7 +442,11 @@ class spell int damage_dot() const; damage_over_time_data damage_over_time( const std::vector &bps ) const; dealt_damage_instance get_dealt_damage_instance() const; + dealt_projectile_attack get_projectile_attack( const tripoint &target, + Creature &hit_critter ) const; damage_instance get_damage_instance() const; + // calculate damage per second against a target + float dps( const Character &caster, const Creature &target ) const; // how big is the spell's radius int aoe() const; std::set effect_area( const spell_effect::override_parameters ¶ms, @@ -449,6 +454,12 @@ class spell std::set effect_area( const tripoint &source, const tripoint &target ) const; // distance spell can be cast int range() const; + /** + * all of the tripoints the spell can be cast at. + * if the spell can't be cast through walls, does not return anything behind walls + * if the spell can't target the ground, can't target unseen locations, etc. + */ + std::vector targetable_locations( const Character &source ) const; // how much energy does the spell cost int energy_cost( const Character &guy ) const; // how long does this spell's effect last @@ -464,7 +475,7 @@ class spell const requirement_data &components() const; bool has_components() const; // can the Character cast this spell? - bool can_cast( Character &guy ) const; + bool can_cast( const Character &guy ) const; // can the Character learn this spell? bool can_learn( const Character &guy ) const; // is this spell valid @@ -525,6 +536,7 @@ class spell // tries to create a field at the location specified void create_field( const tripoint &at ) const; + int sound_volume() const; // makes a spell sound at the location void make_sound( const tripoint &target ) const; void make_sound( const tripoint &target, int loudness ) const; diff --git a/src/magic_spell_effect.cpp b/src/magic_spell_effect.cpp index c010c0cac8a8c..43978f1c511db 100644 --- a/src/magic_spell_effect.cpp +++ b/src/magic_spell_effect.cpp @@ -480,16 +480,7 @@ static void damage_targets( const spell &sp, Creature &caster, continue; } - projectile bolt; - bolt.speed = 10000; - bolt.impact = sp.get_damage_instance(); - bolt.proj_effects.emplace( "magic" ); - - dealt_projectile_attack atk; - atk.end_point = target; - atk.hit_critter = cr; - atk.proj = bolt; - atk.missed_by = 0.0; + dealt_projectile_attack atk = sp.get_projectile_attack( target, *cr ); if( !sp.effect_data().empty() ) { add_effect_to_target( target, sp ); } diff --git a/src/npc.cpp b/src/npc.cpp index f35b05d6d3434..3a397284ef590 100644 --- a/src/npc.cpp +++ b/src/npc.cpp @@ -2135,6 +2135,26 @@ bool npc::is_travelling() const Creature::Attitude npc::attitude_to( const Creature &other ) const { + const auto same_as = []( const Creature * lhs, const Creature * rhs ) { + return &lhs == &rhs; + }; + + for( const weak_ptr_fast &buddy : ai_cache.friends ) { + if( same_as( &other, buddy.lock().get() ) ) { + return Creature::Attitude::FRIENDLY; + } + } + for( const weak_ptr_fast &enemy : ai_cache.hostile_guys ) { + if( same_as( &other, enemy.lock().get() ) ) { + return Creature::Attitude::HOSTILE; + } + } + for( const weak_ptr_fast &neutral : ai_cache.neutral_guys ) { + if( same_as( &other, neutral.lock().get() ) ) { + return Creature::Attitude::NEUTRAL; + } + } + if( other.is_npc() || other.is_player() ) { const player &guy = dynamic_cast( other ); // check faction relationships first @@ -2632,6 +2652,11 @@ std::string npc_attitude_id( npc_attitude att ) return iter->second; } +int npc::closest_enemy_to_friendly_distance() const +{ + return ai_cache.closest_enemy_to_friendly_distance(); +} + std::string npc_attitude_name( npc_attitude att ) { switch( att ) { diff --git a/src/npc.h b/src/npc.h index f0a2abdc1e183..d082eb01409f9 100644 --- a/src/npc.h +++ b/src/npc.h @@ -573,11 +573,17 @@ struct npc_short_term_cache { double my_weapon_value = 0; // Use weak_ptr to avoid circular references between Creatures + // attitude of creatures the npc can see + std::vector> hostile_guys; + std::vector> neutral_guys; std::vector> friends; std::vector dangerous_explosives; std::map threat_map; // Cache of locations the NPC has searched recently in npc::find_item() lru_cache searched_tiles; + // returns the value of the distance between a friendly creature and the closest enemy to that friendly creature. + // returns -1 if not applicable + int closest_enemy_to_friendly_distance() const; }; // DO NOT USE! This is old, use strings as talk topic instead, e.g. "TALK_AGREE_FOLLOW" instead of @@ -766,6 +772,12 @@ class npc : public player bool is_npc() const override { return true; } + const npc *as_npc() override { + return this; + } + const npc *as_npc() const override { + return this; + } void load_npc_template( const string_id &ident ); void npc_dismount(); weak_ptr_fast chosen_mount; @@ -1239,6 +1251,9 @@ class npc : public player std::vector miss_ids; cata::optional assigned_camp = cata::nullopt; + // accessors to ai_cache functions + int closest_enemy_to_friendly_distance() const; + private: npc_attitude attitude = NPCATT_NULL; // What we want to do to the player npc_attitude previous_attitude = NPCATT_NULL; diff --git a/src/npcmove.cpp b/src/npcmove.cpp index 1134c0e69c436..1a47a3e300a11 100644 --- a/src/npcmove.cpp +++ b/src/npcmove.cpp @@ -371,6 +371,23 @@ static bool too_close( const tripoint &critter_pos, const tripoint &ally_pos, co return rl_dist( critter_pos, ally_pos ) <= def_radius; } +int npc_short_term_cache::closest_enemy_to_friendly_distance() const +{ + int distance = INT_MAX; + for( const weak_ptr_fast &buddy : friends ) { + if( buddy.expired() ) { + continue; + } + for( const weak_ptr_fast &enemy : hostile_guys ) { + if( enemy.expired() ) { + continue; + } + distance = std::min( distance, rl_dist( buddy.lock()->pos(), enemy.lock()->pos() ) ); + } + } + return distance; +} + void npc::assess_danger() { float assessment = 0.0f; @@ -454,7 +471,6 @@ void npc::assess_danger() } // find our Character friends and enemies - std::vector> hostile_guys; const bool clairvoyant = clairvoyance(); for( const npc &guy : g->all_npcs() ) { if( &guy == this ) { @@ -467,12 +483,12 @@ void npc::assess_danger() if( has_faction_relationship( guy, npc_factions::watch_your_back ) ) { ai_cache.friends.emplace_back( g->shared_from( guy ) ); } else if( attitude_to( guy ) != Attitude::NEUTRAL && sees( guy.pos() ) ) { - hostile_guys.emplace_back( g->shared_from( guy ) ); + ai_cache.hostile_guys.emplace_back( g->shared_from( guy ) ); } } if( sees( player_character.pos() ) ) { if( is_enemy() ) { - hostile_guys.emplace_back( g->shared_from( player_character ) ); + ai_cache.hostile_guys.emplace_back( g->shared_from( player_character ) ); } else if( is_friendly( player_character ) ) { ai_cache.friends.emplace_back( g->shared_from( player_character ) ); } @@ -488,11 +504,14 @@ void npc::assess_danger() continue; } if( att != Attitude::HOSTILE && ( critter.friendly || !is_enemy() ) ) { + ai_cache.neutral_guys.emplace_back( g->shared_from( critter ) ); continue; } if( !sees( critter ) ) { continue; } + + ai_cache.hostile_guys.emplace_back( g->shared_from( critter ) ); float critter_threat = evaluate_enemy( critter ); // warn and consider the odds for distant enemies int dist = rl_dist( pos(), critter.pos() ); @@ -548,7 +567,7 @@ void npc::assess_danger() } } - if( assessment == 0.0 && hostile_guys.empty() ) { + if( assessment == 0.0 && ai_cache.hostile_guys.empty() ) { ai_cache.danger_assessment = assessment; return; } @@ -594,7 +613,7 @@ void npc::assess_danger() return foe_threat; }; - for( const weak_ptr_fast &guy : hostile_guys ) { + for( const weak_ptr_fast &guy : ai_cache.hostile_guys ) { player *foe = dynamic_cast( guy.lock().get() ); if( foe && foe->is_npc() ) { assessment += handle_hostile( *foe, evaluate_enemy( *foe ), translate_marker( "bandit" ), @@ -603,11 +622,10 @@ void npc::assess_danger() } for( const weak_ptr_fast &guy : ai_cache.friends ) { - player *ally = dynamic_cast( guy.lock().get() ); - if( !( ally && ally->is_npc() ) ) { + if( !( guy.lock() && guy.lock()->is_npc() ) ) { continue; } - float guy_threat = evaluate_enemy( *ally ); + float guy_threat = evaluate_enemy( *guy.lock() ); float min_danger = assessment >= NPC_DANGER_VERY_LOW ? NPC_DANGER_VERY_LOW : -10.0f; assessment = std::max( min_danger, assessment - guy_threat * 0.5f ); } @@ -702,6 +720,8 @@ void npc::regen_ai_cache() } float old_assessment = ai_cache.danger_assessment; ai_cache.friends.clear(); + ai_cache.hostile_guys.clear(); + ai_cache.neutral_guys.clear(); ai_cache.target = shared_ptr_fast(); ai_cache.ally = shared_ptr_fast(); ai_cache.can_heal.clear_all(); diff --git a/src/ranged.cpp b/src/ranged.cpp index a6da99c1a2153..aface68e45d3d 100644 --- a/src/ranged.cpp +++ b/src/ranged.cpp @@ -113,6 +113,8 @@ static const std::string flag_MOUNTABLE( "MOUNTABLE" ); static const trait_id trait_PYROMANIA( "PYROMANIA" ); +static const std::set ferric = { material_id( "iron" ), material_id( "steel" ) }; + // Maximum duration of aim-and-fire loop, in turns static constexpr int AIF_DURATION_LIMIT = 10; @@ -962,64 +964,112 @@ int Character::throwing_dispersion( const item &to_throw, Creature *critter, return std::max( 0, dispersion ); } -dealt_projectile_attack player::throw_item( const tripoint &target, const item &to_throw, - const cata::optional &blind_throw_from_pos ) +static cata::optional character_throw_assist( const Character &guy ) { - // 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 ); - - const int throwing_skill = get_skill_level( skill_throw ); - units::volume volume = to_throw.volume(); - units::mass weight = to_throw.weight(); - - bool throw_assist = false; - int throw_assist_str = 0; - if( is_mounted() ) { - auto *mons = mounted_creature.get(); + cata::optional throw_assist = cata::nullopt; + if( guy.is_mounted() ) { + auto *mons = guy.mounted_creature.get(); if( mons->mech_str_addition() != 0 ) { - throw_assist = true; - throw_assist_str = mons->mech_str_addition(); + throw_assist = mons->mech_str_addition(); mons->use_mech_power( -3 ); } } - if( !throw_assist ) { - const int stamina_cost = get_standard_stamina_cost( &thrown ); - mod_stamina( stamina_cost + throwing_skill ); - } + return throw_assist; +} - const skill_id &skill_used = skill_throw; - int skill_level = std::min( MAX_SKILL, get_skill_level( skill_throw ) ); +static int throwing_skill_adjusted( const Character &guy ) +{ + int skill_level = std::min( MAX_SKILL, guy.get_skill_level( skill_throw ) ); // if you are lying on the floor, you can't really throw that well - if( has_effect( effect_downed ) ) { + if( guy.has_effect( effect_downed ) ) { skill_level = std::max( 0, skill_level - 5 ); } - // 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 ferric = { material_id( "iron" ), material_id( "steel" ) }; + return skill_level; +} - bool do_railgun = has_active_bionic( bio_railgun ) && thrown.made_of_any( ferric ) && - !throw_assist; +int Character::thrown_item_adjusted_damage( const item &thrown ) const +{ + const cata::optional throw_assist = character_throw_assist( *this ); + const bool do_railgun = has_active_bionic( bio_railgun ) && thrown.made_of_any( ferric ) && + !throw_assist; // The damage dealt due to item's weight, player's strength, and skill level // 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 double stats_mod = do_railgun ? get_str() : ( get_str() / 2.0 ); - stats_mod = throw_assist ? throw_assist_str / 2.0 : stats_mod; + stats_mod = throw_assist ? *throw_assist / 2.0 : stats_mod; // modify strength impact based on skill level, clamped to [0.15 - 1] // mod = mod * [ ( ( skill / max_skill ) * 0.85 ) + 0.15 ] stats_mod *= ( std::min( MAX_SKILL, get_skill_level( skill_throw ) ) / static_cast( MAX_SKILL ) ) * 0.85 + 0.15; - impact.add_damage( damage_type::BASH, std::min( weight / 100.0_gram, stats_mod ) ); + return stats_mod; +} + +projectile Character::thrown_item_projectile( const item &thrown ) const +{ + // We'll be constructing a projectile + projectile proj; + proj.impact = thrown.base_damage_thrown(); + proj.speed = 10 + throwing_skill_adjusted( *this ); + return proj; +} + +int Character::thrown_item_total_damage_raw( const item &thrown ) const +{ + projectile proj = thrown_item_projectile( thrown ); + const units::volume volume = thrown.volume(); + proj.impact.add_damage( damage_type::BASH, std::min( thrown.weight() / 100.0_gram, + static_cast( thrown_item_adjusted_damage( thrown ) ) ) ); + // Item will shatter upon landing, destroying the item, dealing damage, and making noise + if( !thrown.active && thrown.made_of( material_id( "glass" ) ) && + rng( 0, units::to_milliliter( 2_liter - volume ) ) < get_str() * 100 ) { + proj.impact.add_damage( damage_type::CUT, units::to_milliliter( volume ) / 500.0f ); + } + // Some minor (skill/2) armor piercing for skillful throws + // Not as much as in melee, though + const int skill_level = throwing_skill_adjusted( *this ); + for( damage_unit &du : proj.impact.damage_units ) { + du.res_pen += skill_level / 2.0f; + } + + int total_damage = 0; + for( damage_unit &du : proj.impact.damage_units ) { + total_damage += du.amount * du.damage_multiplier; + } + return total_damage; +} + +dealt_projectile_attack player::throw_item( const tripoint &target, const item &to_throw, + const cata::optional &blind_throw_from_pos ) +{ + // 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 ); + + const int throwing_skill = get_skill_level( skill_throw ); + const units::volume volume = to_throw.volume(); + const units::mass weight = to_throw.weight(); + const cata::optional throw_assist = character_throw_assist( *this ); + + if( !throw_assist ) { + const int stamina_cost = get_standard_stamina_cost( &thrown ); + mod_stamina( stamina_cost + throwing_skill ); + } + + const int skill_level = throwing_skill_adjusted( *this ); + projectile proj = thrown_item_projectile( thrown ); + damage_instance &impact = proj.impact; + std::set &proj_effects = proj.proj_effects; + + const bool do_railgun = has_active_bionic( bio_railgun ) && thrown.made_of_any( ferric ) && + !throw_assist; + + impact.add_damage( damage_type::BASH, std::min( weight / 100.0_gram, + static_cast( thrown_item_adjusted_damage( thrown ) ) ) ); if( thrown.has_flag( flag_ACT_ON_RANGED_HIT ) ) { proj_effects.insert( "ACT_ON_RANGED_HIT" ); @@ -1096,7 +1146,7 @@ dealt_projectile_attack player::throw_item( const tripoint &target, const item & float range = rl_dist( throw_from, target ); proj.range = range; - int skill_lvl = get_skill_level( skill_used ); + int skill_lvl = get_skill_level( skill_throw ); // Avoid awarding tons of xp for lucky throws against hard to hit targets const float range_factor = std::min( range, skill_lvl + 3 ); // We're aiming to get a damaging hit, not just an accurate one - reward proper weapons @@ -1109,14 +1159,14 @@ dealt_projectile_attack player::throw_item( const tripoint &target, const item & 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 ); + practice( skill_throw, final_xp_mult, MAX_SKILL ); // TODO: Check target for existence of head get_event_bus().send( getID() ); } else if( dealt_attack.hit_critter != nullptr && missed_by > 0.0f ) { - practice( skill_used, final_xp_mult / ( 1.0f + missed_by ), MAX_SKILL ); + practice( skill_throw, final_xp_mult / ( 1.0f + missed_by ), MAX_SKILL ); } else { // Pure grindy practice - cap gain at lvl 2 - practice( skill_used, 5, 2 ); + practice( skill_throw, 5, 2 ); } // Reset last target pos last_target_pos = cata::nullopt; diff --git a/tests/player_helpers.cpp b/tests/player_helpers.cpp index 6658c7490792f..58dc5de7b4914 100644 --- a/tests/player_helpers.cpp +++ b/tests/player_helpers.cpp @@ -116,6 +116,52 @@ void clear_character( player &dummy ) dummy.setpos( spot ); } +void arm_shooter( npc &shooter, const std::string &gun_type, + const std::vector &mods, + const std::string &ammo_type ) +{ + shooter.remove_weapon(); + // XL so arrows can fit. + if( !shooter.is_wearing( itype_id( "debug_backpack" ) ) ) { + shooter.worn.push_back( item( "debug_backpack" ) ); + } + + const itype_id &gun_id{ itype_id( gun_type ) }; + // Give shooter a loaded gun of the requested type. + item &gun = shooter.i_add( item( gun_id ) ); + itype_id ammo_id; + // if ammo is not supplied we want the default + if( ammo_type.empty() ) { + if( gun.ammo_default().is_null() ) { + ammo_id = item( gun.magazine_default() ).ammo_default(); + } else { + ammo_id = gun.ammo_default(); + } + } else { + ammo_id = itype_id( ammo_type ); + } + const ammotype &type_of_ammo = item::find_type( ammo_id )->ammo->type; + if( gun.magazine_integral() ) { + item &ammo = shooter.i_add( item( ammo_id, calendar::turn, gun.ammo_capacity( type_of_ammo ) ) ); + REQUIRE( gun.is_reloadable_with( ammo_id ) ); + REQUIRE( shooter.can_reload( gun, ammo_id ) ); + gun.reload( shooter, item_location( shooter, &ammo ), gun.ammo_capacity( type_of_ammo ) ); + } else { + const itype_id magazine_id = gun.magazine_default(); + item &magazine = shooter.i_add( item( magazine_id ) ); + item &ammo = shooter.i_add( item( ammo_id, calendar::turn, + magazine.ammo_capacity( type_of_ammo ) ) ); + REQUIRE( magazine.is_reloadable_with( ammo_id ) ); + REQUIRE( shooter.can_reload( magazine, ammo_id ) ); + magazine.reload( shooter, item_location( shooter, &ammo ), magazine.ammo_capacity( type_of_ammo ) ); + gun.reload( shooter, item_location( shooter, &magazine ), magazine.ammo_capacity( type_of_ammo ) ); + } + for( const auto &mod : mods ) { + gun.put_in( item( itype_id( mod ) ), item_pocket::pocket_type::MOD ); + } + shooter.wield( gun ); +} + void clear_avatar() { clear_character( get_avatar() ); diff --git a/tests/player_helpers.h b/tests/player_helpers.h index df2fecc801109..59e79903a0737 100644 --- a/tests/player_helpers.h +++ b/tests/player_helpers.h @@ -21,4 +21,8 @@ void give_and_activate_bionic( player &, bionic_id const & ); item tool_with_ammo( const std::string &tool, int qty ); +void arm_shooter( npc &shooter, const std::string &gun_type, + const std::vector &mods = {}, + const std::string &ammo_type = "" ); + #endif // CATA_TESTS_PLAYER_HELPERS_H diff --git a/tests/ranged_balance_test.cpp b/tests/ranged_balance_test.cpp index 32c3e64d6294e..40997377ae042 100644 --- a/tests/ranged_balance_test.cpp +++ b/tests/ranged_balance_test.cpp @@ -24,6 +24,7 @@ #include "map_helpers.h" #include "npc.h" #include "pimpl.h" +#include "player_helpers.h" #include "point.h" #include "ret_val.h" #include "test_statistics.h" @@ -76,52 +77,6 @@ std::ostream &operator<<( std::ostream &stream, const dispersion_sources &source return stream; } -static void arm_shooter( npc &shooter, const std::string &gun_type, - const std::vector &mods = {}, - const std::string &ammo_type = "" ) -{ - shooter.remove_weapon(); - // XL so arrows can fit. - if( !shooter.is_wearing( itype_id( "debug_backpack" ) ) ) { - shooter.worn.push_back( item( "debug_backpack" ) ); - } - - const itype_id &gun_id{ itype_id( gun_type ) }; - // Give shooter a loaded gun of the requested type. - item &gun = shooter.i_add( item( gun_id ) ); - itype_id ammo_id; - // if ammo is not supplied we want the default - if( ammo_type.empty() ) { - if( gun.ammo_default().is_null() ) { - ammo_id = item( gun.magazine_default() ).ammo_default(); - } else { - ammo_id = gun.ammo_default(); - } - } else { - ammo_id = itype_id( ammo_type ); - } - const ammotype &type_of_ammo = item::find_type( ammo_id )->ammo->type; - if( gun.magazine_integral() ) { - item &ammo = shooter.i_add( item( ammo_id, calendar::turn, gun.ammo_capacity( type_of_ammo ) ) ); - REQUIRE( gun.is_reloadable_with( ammo_id ) ); - REQUIRE( shooter.can_reload( gun, ammo_id ) ); - gun.reload( shooter, item_location( shooter, &ammo ), gun.ammo_capacity( type_of_ammo ) ); - } else { - const itype_id magazine_id = gun.magazine_default(); - item &magazine = shooter.i_add( item( magazine_id ) ); - item &ammo = shooter.i_add( item( ammo_id, calendar::turn, - magazine.ammo_capacity( type_of_ammo ) ) ); - REQUIRE( magazine.is_reloadable_with( ammo_id ) ); - REQUIRE( shooter.can_reload( magazine, ammo_id ) ); - magazine.reload( shooter, item_location( shooter, &ammo ), magazine.ammo_capacity( type_of_ammo ) ); - gun.reload( shooter, item_location( shooter, &magazine ), magazine.ammo_capacity( type_of_ammo ) ); - } - for( const auto &mod : mods ) { - gun.put_in( item( itype_id( mod ) ), item_pocket::pocket_type::MOD ); - } - shooter.wield( gun ); -} - static void equip_shooter( npc &shooter, const std::vector &apparel ) { CHECK( !shooter.in_vehicle );