Permalink
Cannot retrieve contributors at this time
Join GitHub today
GitHub is home to over 36 million developers working together to host and review code, manage projects, and build software together.
Sign up
Fetching contributors…
| #include "melee.h" | |
| #include <climits> | |
| #include <algorithm> | |
| #include <cstdlib> | |
| #include <sstream> | |
| #include <array> | |
| #include <limits> | |
| #include <list> | |
| #include <memory> | |
| #include <set> | |
| #include <string> | |
| #include <unordered_map> | |
| #include <utility> | |
| #include <vector> | |
| #include <cmath> | |
| #include "avatar.h" | |
| #include "cata_utility.h" | |
| #include "debug.h" | |
| #include "game.h" | |
| #include "game_inventory.h" | |
| #include "itype.h" | |
| #include "line.h" | |
| #include "map.h" | |
| #include "map_iterator.h" | |
| #include "martialarts.h" | |
| #include "messages.h" | |
| #include "monster.h" | |
| #include "mutation.h" | |
| #include "npc.h" | |
| #include "output.h" | |
| #include "player.h" | |
| #include "rng.h" | |
| #include "sounds.h" | |
| #include "string_formatter.h" | |
| #include "translations.h" | |
| #include "bodypart.h" | |
| #include "calendar.h" | |
| #include "character.h" | |
| #include "creature.h" | |
| #include "damage.h" | |
| #include "enums.h" | |
| #include "game_constants.h" | |
| #include "item.h" | |
| #include "item_location.h" | |
| #include "optional.h" | |
| #include "pldata.h" | |
| #include "string_id.h" | |
| #include "units.h" | |
| #include "weighted_list.h" | |
| #include "material.h" | |
| #include "type_id.h" | |
| #include "point.h" | |
| static const bionic_id bio_cqb( "bio_cqb" ); | |
| static const bionic_id bio_memory( "bio_memory" ); | |
| static const matec_id tec_none( "tec_none" ); | |
| static const matec_id WBLOCK_1( "WBLOCK_1" ); | |
| static const matec_id WBLOCK_2( "WBLOCK_2" ); | |
| static const matec_id WBLOCK_3( "WBLOCK_3" ); | |
| static const skill_id skill_stabbing( "stabbing" ); | |
| static const skill_id skill_cutting( "cutting" ); | |
| static const skill_id skill_unarmed( "unarmed" ); | |
| static const skill_id skill_bashing( "bashing" ); | |
| static const skill_id skill_melee( "melee" ); | |
| const efftype_id effect_badpoison( "badpoison" ); | |
| const efftype_id effect_beartrap( "beartrap" ); | |
| const efftype_id effect_bouldering( "bouldering" ); | |
| const efftype_id effect_contacts( "contacts" ); | |
| const efftype_id effect_downed( "downed" ); | |
| const efftype_id effect_drunk( "drunk" ); | |
| const efftype_id effect_grabbed( "grabbed" ); | |
| const efftype_id effect_heavysnare( "heavysnare" ); | |
| const efftype_id effect_hit_by_player( "hit_by_player" ); | |
| const efftype_id effect_lightsnare( "lightsnare" ); | |
| const efftype_id effect_narcosis( "narcosis" ); | |
| const efftype_id effect_poison( "poison" ); | |
| const efftype_id effect_stunned( "stunned" ); | |
| static const trait_id trait_CLAWS( "CLAWS" ); | |
| static const trait_id trait_CLAWS_RAT( "CLAWS_RAT" ); | |
| static const trait_id trait_CLAWS_RETRACT( "CLAWS_RETRACT" ); | |
| static const trait_id trait_CLAWS_ST( "CLAWS_ST" ); | |
| static const trait_id trait_CLAWS_TENTACLE( "CLAWS_TENTACLE" ); | |
| static const trait_id trait_CLUMSY( "CLUMSY" ); | |
| static const trait_id trait_DEBUG_NIGHTVISION( "DEBUG_NIGHTVISION" ); | |
| static const trait_id trait_DEFT( "DEFT" ); | |
| static const trait_id trait_DRUNKEN( "DRUNKEN" ); | |
| static const trait_id trait_HYPEROPIC( "HYPEROPIC" ); | |
| static const trait_id trait_NAILS( "NAILS" ); | |
| static const trait_id trait_POISONOUS2( "POISONOUS2" ); | |
| static const trait_id trait_POISONOUS( "POISONOUS" ); | |
| static const trait_id trait_PROF_SKATER( "PROF_SKATER" ); | |
| static const trait_id trait_SLIME_HANDS( "SLIME_HANDS" ); | |
| static const trait_id trait_TALONS( "TALONS" ); | |
| static const trait_id trait_THORNS( "THORNS" ); | |
| void player_hit_message( player *attacker, const std::string &message, | |
| Creature &t, int dam, bool crit = false ); | |
| int stumble( player &u, const item &weap ); | |
| std::string melee_message( const ma_technique &tec, player &p, const dealt_damage_instance &ddi ); | |
| /* Melee Functions! | |
| * These all belong to class player. | |
| * | |
| * STATE QUERIES | |
| * bool is_armed() - True if we are armed with any item. | |
| * bool unarmed_attack() - True if we are attacking with a "fist" weapon | |
| * (cestus, bionic claws etc.) or no weapon. | |
| * | |
| * HIT DETERMINATION | |
| * int hit_roll() - The player's hit roll, to be compared to a monster's or | |
| * player's dodge_roll(). This handles weapon bonuses, weapon-specific | |
| * skills, torso encumbrance penalties and drunken master bonuses. | |
| */ | |
| const item &Character::used_weapon() const | |
| { | |
| return dynamic_cast<const player &>( *this ).get_combat_style().force_unarmed ? | |
| null_item_reference() : weapon; | |
| } | |
| item &Character::used_weapon() | |
| { | |
| return const_cast<item &>( const_cast<const Character *>( this )->used_weapon() ); | |
| } | |
| bool Character::is_armed() const | |
| { | |
| return !weapon.is_null(); | |
| } | |
| bool player::unarmed_attack() const | |
| { | |
| const item &weap = used_weapon(); | |
| return weap.is_null() || weap.has_flag( "UNARMED_WEAPON" ); | |
| } | |
| bool player::handle_melee_wear( item &shield, float wear_multiplier ) | |
| { | |
| if( wear_multiplier <= 0.0f ) { | |
| return false; | |
| } | |
| // Here is where we handle wear and tear on things we use as melee weapons or shields. | |
| if( shield.is_null() ) { | |
| return false; | |
| } | |
| // UNBREAKABLE_MELEE items can't be damaged through melee combat usage. | |
| if( shield.has_flag( "UNBREAKABLE_MELEE" ) ) { | |
| return false; | |
| } | |
| /** @EFFECT_DEX reduces chance of damaging your melee weapon */ | |
| /** @EFFECT_STR increases chance of damaging your melee weapon (NEGATIVE) */ | |
| /** @EFFECT_MELEE reduces chance of damaging your melee weapon */ | |
| const float stat_factor = dex_cur / 2.0f | |
| + get_skill_level( skill_melee ) | |
| + ( 64.0f / std::max( str_cur, 4 ) ); | |
| float material_factor; | |
| itype_id weak_comp; | |
| itype_id big_comp = "null"; | |
| // Fragile items that fall apart easily when used as a weapon due to poor construction quality | |
| if( shield.has_flag( "FRAGILE_MELEE" ) ) { | |
| const float fragile_factor = 6; | |
| int weak_chip = INT_MAX; | |
| units::volume big_vol = 0_ml; | |
| // Items that should have no bearing on durability | |
| const std::set<itype_id> blacklist = { "rag", | |
| "leather", | |
| "fur" | |
| }; | |
| for( auto &comp : shield.components ) { | |
| if( blacklist.count( comp.typeId() ) <= 0 ) { | |
| if( weak_chip > comp.chip_resistance() ) { | |
| weak_chip = comp.chip_resistance(); | |
| weak_comp = comp.typeId(); | |
| } | |
| } | |
| if( comp.volume() > big_vol ) { | |
| big_vol = comp.volume(); | |
| big_comp = comp.typeId(); | |
| } | |
| } | |
| material_factor = ( weak_chip < INT_MAX ? weak_chip : shield.chip_resistance() ) / fragile_factor; | |
| } else { | |
| material_factor = shield.chip_resistance(); | |
| } | |
| int damage_chance = static_cast<int>( stat_factor * material_factor / wear_multiplier ); | |
| // DURABLE_MELEE items are made to hit stuff and they do it well, so they're considered to be a lot tougher | |
| // than other weapons made of the same materials. | |
| if( shield.has_flag( "DURABLE_MELEE" ) ) { | |
| damage_chance *= 4; | |
| } | |
| if( damage_chance > 0 && !one_in( damage_chance ) ) { | |
| return false; | |
| } | |
| auto str = shield.tname(); // save name before we apply damage | |
| if( !shield.inc_damage() ) { | |
| add_msg_player_or_npc( m_bad, _( "Your %s is damaged by the force of the blow!" ), | |
| _( "<npcname>'s %s is damaged by the force of the blow!" ), | |
| str ); | |
| return false; | |
| } | |
| // Dump its contents on the ground | |
| // Destroy irremovable mods, if any | |
| for( auto mod : shield.gunmods() ) { | |
| if( mod->is_irremovable() ) { | |
| remove_item( *mod ); | |
| } | |
| } | |
| for( auto &elem : shield.contents ) { | |
| g->m.add_item_or_charges( pos(), elem ); | |
| } | |
| // Preserve item temporarily for component breakdown | |
| item temp = shield; | |
| remove_item( shield ); | |
| // Breakdown fragile weapons into components | |
| if( temp.has_flag( "FRAGILE_MELEE" ) && !temp.components.empty() ) { | |
| add_msg_player_or_npc( m_bad, _( "Your %s breaks apart!" ), | |
| _( "<npcname>'s %s breaks apart!" ), | |
| str ); | |
| for( auto &comp : temp.components ) { | |
| int break_chance = comp.typeId() == weak_comp ? 2 : 8; | |
| if( one_in( break_chance ) ) { | |
| add_msg_if_player( m_bad, _( "The %s is destroyed!" ), comp.tname() ); | |
| continue; | |
| } | |
| if( comp.typeId() == big_comp && !is_armed() ) { | |
| wield( comp ); | |
| } else { | |
| g->m.add_item_or_charges( pos(), comp ); | |
| } | |
| } | |
| } else { | |
| add_msg_player_or_npc( m_bad, _( "Your %s is destroyed by the blow!" ), | |
| _( "<npcname>'s %s is destroyed by the blow!" ), | |
| str ); | |
| } | |
| return true; | |
| } | |
| float player::get_hit_weapon( const item &weap ) const | |
| { | |
| /** @EFFECT_UNARMED improves hit chance for unarmed weapons */ | |
| /** @EFFECT_BASHING improves hit chance for bashing weapons */ | |
| /** @EFFECT_CUTTING improves hit chance for cutting weapons */ | |
| /** @EFFECT_STABBING improves hit chance for piercing weapons */ | |
| auto skill = get_skill_level( weap.melee_skill() ); | |
| // CQB bionic acts as a lower bound providing item uses a weapon skill | |
| if( skill < BIO_CQB_LEVEL && has_active_bionic( bio_cqb ) ) { | |
| skill = BIO_CQB_LEVEL; | |
| } | |
| /** @EFFECT_MELEE improves hit chance for all items (including non-weapons) */ | |
| return ( skill / 3.0f ) + ( get_skill_level( skill_melee ) / 2.0f ); | |
| } | |
| float player::get_hit_base() const | |
| { | |
| // Character::get_hit_base includes stat calculations already | |
| return Character::get_hit_base() + get_hit_weapon( used_weapon() ); | |
| } | |
| float player::hit_roll() const | |
| { | |
| // Dexterity, skills, weapon and martial arts | |
| float hit = get_hit(); | |
| // Drunken master makes us hit better | |
| if( has_trait( trait_DRUNKEN ) ) { | |
| hit += to_turns<float>( get_effect_dur( effect_drunk ) ) / ( used_weapon().is_null() ? 300.0f : | |
| 400.0f ); | |
| } | |
| // Farsightedness makes us hit worse | |
| if( has_trait( trait_HYPEROPIC ) && !worn_with_flag( "FIX_FARSIGHT" ) && | |
| !has_effect( effect_contacts ) ) { | |
| hit -= 2.0f; | |
| } | |
| //Unstable ground chance of failure | |
| if( has_effect( effect_bouldering ) ) { | |
| hit *= 0.75f; | |
| } | |
| hit *= std::max( 0.25f, 1.0f - encumb( bp_torso ) / 100.0f ); | |
| return melee::melee_hit_range( hit ); | |
| } | |
| void player::add_miss_reason( const std::string &reason, const unsigned int weight ) | |
| { | |
| melee_miss_reasons.add( reason, weight ); | |
| } | |
| void player::clear_miss_reasons() | |
| { | |
| melee_miss_reasons.clear(); | |
| } | |
| std::string player::get_miss_reason() | |
| { | |
| // everything that lowers accuracy in player::hit_roll() | |
| // adding it in hit_roll() might not be safe if it's called multiple times | |
| // in one turn | |
| add_miss_reason( | |
| _( "Your torso encumbrance throws you off-balance." ), | |
| roll_remainder( encumb( bp_torso ) / 10.0 ) ); | |
| const int farsightedness = 2 * ( has_trait( trait_HYPEROPIC ) && | |
| !worn_with_flag( "FIX_FARSIGHT" ) && | |
| !has_effect( effect_contacts ) ); | |
| add_miss_reason( | |
| _( "You can't hit reliably due to your farsightedness." ), | |
| farsightedness ); | |
| const std::string *const reason = melee_miss_reasons.pick(); | |
| if( reason == nullptr ) { | |
| return std::string(); | |
| } | |
| return *reason; | |
| } | |
| void player::roll_all_damage( bool crit, damage_instance &di, bool average, const item &weap ) const | |
| { | |
| roll_bash_damage( crit, di, average, weap ); | |
| roll_cut_damage( crit, di, average, weap ); | |
| roll_stab_damage( crit, di, average, weap ); | |
| } | |
| static void melee_train( player &p, int lo, int hi, const item &weap ) | |
| { | |
| p.practice( skill_melee, ceil( rng( lo, hi ) / 2.0 ), hi ); | |
| // allocate XP proportional to damage stats | |
| // Pure unarmed needs a special case because it has 0 weapon damage | |
| int cut = weap.damage_melee( DT_CUT ); | |
| int stab = weap.damage_melee( DT_STAB ); | |
| int bash = weap.damage_melee( DT_BASH ) + ( weap.is_null() ? 1 : 0 ); | |
| float total = std::max( cut + stab + bash, 1 ); | |
| p.practice( skill_cutting, ceil( cut / total * rng( lo, hi ) ), hi ); | |
| p.practice( skill_stabbing, ceil( stab / total * rng( lo, hi ) ), hi ); | |
| // Unarmed skill scaled bashing damage and so scales with bashing damage | |
| p.practice( weap.is_unarmed_weapon() ? skill_unarmed : skill_bashing, | |
| ceil( bash / total * rng( lo, hi ) ), hi ); | |
| } | |
| void player::melee_attack( Creature &t, bool allow_special ) | |
| { | |
| static const matec_id no_technique_id( "" ); | |
| melee_attack( t, allow_special, no_technique_id ); | |
| } | |
| // Melee calculation is in parts. This sets up the attack, then in deal_melee_attack, | |
| // we calculate if we would hit. In Creature::deal_melee_hit, we calculate if the target dodges. | |
| void player::melee_attack( Creature &t, bool allow_special, const matec_id &force_technique, | |
| bool allow_unarmed ) | |
| { | |
| int hit_spread = t.deal_melee_attack( this, hit_roll() ); | |
| if( !t.is_player() ) { | |
| // TODO: Per-NPC tracking? Right now monster hit by either npc or player will draw aggro... | |
| t.add_effect( effect_hit_by_player, 10_minutes ); // Flag as attacked by us for AI | |
| } | |
| item &cur_weapon = allow_unarmed ? used_weapon() : weapon; | |
| const bool critical_hit = scored_crit( t.dodge_roll(), cur_weapon ); | |
| int move_cost = attack_speed( cur_weapon ); | |
| if( hit_spread < 0 ) { | |
| int stumble_pen = stumble( *this, cur_weapon ); | |
| sfx::generate_melee_sound( pos(), t.pos(), false, false ); | |
| if( is_player() ) { // Only display messages if this is the player | |
| if( one_in( 2 ) ) { | |
| const std::string reason_for_miss = get_miss_reason(); | |
| if( !reason_for_miss.empty() ) { | |
| add_msg( reason_for_miss ); | |
| } | |
| } | |
| if( has_miss_recovery_tec( cur_weapon ) ) { | |
| ma_technique tec = get_miss_recovery_tec( cur_weapon ); | |
| add_msg( _( tec.player_message ), t.disp_name() ); | |
| } else if( stumble_pen >= 60 ) { | |
| add_msg( m_bad, _( "You miss and stumble with the momentum." ) ); | |
| } else if( stumble_pen >= 10 ) { | |
| add_msg( _( "You swing wildly and miss." ) ); | |
| } else { | |
| add_msg( _( "You miss." ) ); | |
| } | |
| } else if( g->u.sees( *this ) ) { | |
| if( stumble_pen >= 60 ) { | |
| add_msg( _( "%s misses and stumbles with the momentum." ), name ); | |
| } else if( stumble_pen >= 10 ) { | |
| add_msg( _( "%s swings wildly and misses." ), name ); | |
| } else { | |
| add_msg( _( "%s misses." ), name ); | |
| } | |
| } | |
| // Practice melee and relevant weapon skill (if any) except when using CQB bionic | |
| if( !has_active_bionic( bio_cqb ) ) { | |
| melee_train( *this, 2, 5, cur_weapon ); | |
| } | |
| // Cap stumble penalty, heavy weapons are quite weak already | |
| move_cost += std::min( 60, stumble_pen ); | |
| if( has_miss_recovery_tec( cur_weapon ) ) { | |
| move_cost /= 2; | |
| } | |
| ma_onmiss_effects(); // trigger martial arts on-miss effects | |
| } else { | |
| // Remember if we see the monster at start - it may change | |
| const bool seen = g->u.sees( t ); | |
| // Start of attacks. | |
| damage_instance d; | |
| roll_all_damage( critical_hit, d, false, cur_weapon ); | |
| const bool has_force_technique = !force_technique.str().empty(); | |
| // Pick one or more special attacks | |
| matec_id technique_id; | |
| if( allow_special && !has_force_technique ) { | |
| technique_id = pick_technique( t, cur_weapon, critical_hit, false, false ); | |
| } else if( has_force_technique ) { | |
| technique_id = force_technique; | |
| } else { | |
| technique_id = tec_none; | |
| } | |
| const ma_technique &technique = technique_id.obj(); | |
| // Handles effects as well; not done in melee_affect_* | |
| if( technique.id != tec_none ) { | |
| perform_technique( technique, t, d, move_cost ); | |
| } | |
| if( allow_special && !t.is_dead_state() ) { | |
| perform_special_attacks( t ); | |
| } | |
| // Proceed with melee attack. | |
| if( !t.is_dead_state() ) { | |
| // Handles speed penalties to monster & us, etc | |
| std::string specialmsg = melee_special_effects( t, d, cur_weapon ); | |
| dealt_damage_instance dealt_dam; // gets overwritten with the dealt damage values | |
| t.deal_melee_hit( this, hit_spread, critical_hit, d, dealt_dam ); | |
| // Make a rather quiet sound, to alert any nearby monsters | |
| if( !is_quiet() ) { // check martial arts silence | |
| sounds::sound( pos(), 8, sounds::sound_t::combat, "whack!" ); //sound generated later | |
| } | |
| std::string material = "flesh"; | |
| if( t.is_monster() ) { | |
| const monster *m = dynamic_cast<const monster *>( &t ); | |
| if( m->made_of( material_id( "steel" ) ) ) { | |
| material = "steel"; | |
| } | |
| } | |
| sfx::generate_melee_sound( pos(), t.pos(), true, t.is_monster(), material ); | |
| int dam = dealt_dam.total_damage(); | |
| // Practice melee and relevant weapon skill (if any) except when using CQB bionic | |
| if( !has_active_bionic( bio_cqb ) ) { | |
| melee_train( *this, 5, 10, cur_weapon ); | |
| } | |
| if( dam >= 5 && has_artifact_with( AEP_SAP_LIFE ) ) { | |
| healall( rng( dam / 10, dam / 5 ) ); | |
| } | |
| // Treat monster as seen if we see it before or after the attack | |
| if( seen || g->u.sees( t ) ) { | |
| std::string message = melee_message( technique, *this, dealt_dam ); | |
| player_hit_message( this, message, t, dam, critical_hit ); | |
| } else { | |
| add_msg_player_or_npc( m_good, _( "You hit something." ), _( "<npcname> hits something." ) ); | |
| } | |
| if( !specialmsg.empty() ) { | |
| add_msg_if_player( m_neutral, specialmsg ); | |
| } | |
| if( critical_hit ) { | |
| ma_oncrit_effects(); // trigger martial arts on-crit effects | |
| } | |
| } | |
| t.check_dead_state(); | |
| if( t.is_dead_state() ) { | |
| ma_onkill_effects(); // trigger martial arts on-kill effects | |
| } | |
| } | |
| const int melee = get_skill_level( skill_melee ); | |
| /** @EFFECT_STR reduces stamina cost for melee attack with heavier weapons */ | |
| const int weight_cost = cur_weapon.weight() / ( 2_gram * std::max( 1, str_cur ) ); | |
| const int encumbrance_cost = roll_remainder( ( encumb( bp_arm_l ) + encumb( bp_arm_r ) ) * 2.0f ); | |
| const int deft_bonus = hit_spread < 0 && has_trait( trait_DEFT ) ? 50 : 0; | |
| /** @EFFECT_MELEE reduces stamina cost of melee attacks */ | |
| const int mod_sta = ( weight_cost + encumbrance_cost - melee - deft_bonus + 50 ) * -1; | |
| mod_stat( "stamina", std::min( -50, mod_sta ) ); | |
| add_msg( m_debug, "Stamina burn: %d", std::min( -50, mod_sta ) ); | |
| mod_moves( -move_cost ); | |
| ma_onattack_effects(); // trigger martial arts on-attack effects | |
| // some things (shattering weapons) can harm the attacking creature. | |
| check_dead_state(); | |
| return; | |
| } | |
| void player::reach_attack( const tripoint &p ) | |
| { | |
| matec_id force_technique = tec_none; | |
| /** @EFFECT_MELEE >5 allows WHIP_DISARM technique */ | |
| if( weapon.has_flag( "WHIP" ) && ( get_skill_level( skill_melee ) > 5 ) && one_in( 3 ) ) { | |
| force_technique = matec_id( "WHIP_DISARM" ); | |
| } | |
| Creature *critter = g->critter_at( p ); | |
| // Original target size, used when there are monsters in front of our target | |
| int target_size = critter != nullptr ? critter->get_size() : 2; | |
| // Reset last target pos | |
| last_target_pos = cata::nullopt; | |
| int move_cost = attack_speed( weapon ); | |
| int skill = std::min( 10, get_skill_level( skill_stabbing ) ); | |
| int t = 0; | |
| std::vector<tripoint> path = line_to( pos(), p, t, 0 ); | |
| path.pop_back(); // Last point is our critter | |
| for( const tripoint &p : path ) { | |
| // Possibly hit some unintended target instead | |
| Creature *inter = g->critter_at( p ); | |
| /** @EFFECT_STABBING decreases chance of hitting intervening target on reach attack */ | |
| if( inter != nullptr && | |
| !x_in_y( ( target_size * target_size + 1 ) * skill, | |
| ( inter->get_size() * inter->get_size() + 1 ) * 10 ) ) { | |
| // Even if we miss here, low roll means weapon is pushed away or something like that | |
| critter = inter; | |
| break; | |
| /** @EFFECT_STABBING increases ability to reach attack through fences */ | |
| } else if( g->m.impassable( p ) && | |
| // Fences etc. Spears can stab through those | |
| !( weapon.has_flag( "SPEAR" ) && | |
| g->m.has_flag( "THIN_OBSTACLE", p ) && | |
| x_in_y( skill, 10 ) ) ) { | |
| /** @EFFECT_STR increases bash effects when reach attacking past something */ | |
| g->m.bash( p, str_cur + weapon.damage_melee( DT_BASH ) ); | |
| handle_melee_wear( weapon ); | |
| mod_moves( -move_cost ); | |
| return; | |
| } | |
| } | |
| if( critter == nullptr ) { | |
| add_msg_if_player( _( "You swing at the air." ) ); | |
| if( has_miss_recovery_tec( weapon ) ) { | |
| move_cost /= 3; // "Probing" is faster than a regular miss | |
| } | |
| mod_moves( -move_cost ); | |
| return; | |
| } | |
| reach_attacking = true; | |
| melee_attack( *critter, false, force_technique, false ); | |
| reach_attacking = false; | |
| } | |
| int stumble( player &u, const item &weap ) | |
| { | |
| if( u.has_trait( trait_DEFT ) ) { | |
| return 0; | |
| } | |
| // Examples: | |
| // 10 str with a hatchet: 4 + 8 = 12 | |
| // 5 str with a battle axe: 26 + 49 = 75 | |
| // Fist: 0 | |
| /** @EFFECT_STR reduces chance of stumbling with heavier weapons */ | |
| return ( weap.volume() / 125_ml ) + | |
| ( weap.weight() / ( u.str_cur * 10_gram + 13.0_gram ) ); | |
| } | |
| bool player::scored_crit( float target_dodge, const item &weap ) const | |
| { | |
| return rng_float( 0, 1.0 ) < crit_chance( hit_roll(), target_dodge, weap ); | |
| } | |
| /** | |
| * Limits a probability to be between 0.0 and 1.0 | |
| */ | |
| inline double limit_probability( double unbounded_probability ) | |
| { | |
| return std::max( std::min( unbounded_probability, 1.0 ), 0.0 ); | |
| } | |
| double player::crit_chance( float roll_hit, float target_dodge, const item &weap ) const | |
| { | |
| // Weapon to-hit roll | |
| double weapon_crit_chance = 0.5; | |
| if( weap.is_unarmed_weapon() ) { | |
| // Unarmed attack: 1/2 of unarmed skill is to-hit | |
| /** @EFFECT_UNARMED increases critical chance with UNARMED_WEAPON */ | |
| weapon_crit_chance = 0.5 + 0.05 * get_skill_level( skill_unarmed ); | |
| } | |
| if( weap.type->m_to_hit > 0 ) { | |
| weapon_crit_chance = std::max( weapon_crit_chance, 0.5 + 0.1 * weap.type->m_to_hit ); | |
| } else if( weap.type->m_to_hit < 0 ) { | |
| weapon_crit_chance += 0.1 * weap.type->m_to_hit; | |
| } | |
| weapon_crit_chance = limit_probability( weapon_crit_chance ); | |
| // Dexterity and perception | |
| /** @EFFECT_DEX increases chance for critical hits */ | |
| /** @EFFECT_PER increases chance for critical hits */ | |
| const double stat_crit_chance = limit_probability( 0.25 + 0.01 * dex_cur + ( 0.02 * per_cur ) ); | |
| /** @EFFECT_BASHING increases critical chance with bashing weapons */ | |
| /** @EFFECT_CUTTING increases critical chance with cutting weapons */ | |
| /** @EFFECT_STABBING increases critical chance with piercing weapons */ | |
| /** @EFFECT_UNARMED increases critical chance with unarmed weapons */ | |
| int sk = get_skill_level( weap.melee_skill() ); | |
| if( has_active_bionic( bio_cqb ) ) { | |
| sk = std::max( sk, BIO_CQB_LEVEL ); | |
| } | |
| /** @EFFECT_MELEE slightly increases critical chance with any item */ | |
| sk += get_skill_level( skill_melee ) / 2.5; | |
| const double skill_crit_chance = limit_probability( 0.25 + sk * 0.025 ); | |
| // Examples (survivor stats/chances of each critical): | |
| // Fresh (skill-less) 8/8/8/8, unarmed: | |
| // 50%, 49%, 25%; ~1/16 guaranteed critical + ~1/8 if roll>dodge*1.5 | |
| // Expert (skills 10) 10/10/10/10, unarmed: | |
| // 100%, 55%, 60%; ~1/3 guaranteed critical + ~4/10 if roll>dodge*1.5 | |
| // Godlike with combat CBM 20/20/20/20, pipe (+1 accuracy): | |
| // 60%, 100%, 42%; ~1/4 guaranteed critical + ~3/8 if roll>dodge*1.5 | |
| // Note: the formulas below are only valid if none of the 3 critical chance values go above 1.0 | |
| // It is therefore important to limit them to between 0.0 and 1.0 | |
| // Chance to get all 3 criticals (a guaranteed critical regardless of hit/dodge) | |
| const double chance_triple = weapon_crit_chance * stat_crit_chance * skill_crit_chance; | |
| // Only check double critical (one that requires hit/dodge comparison) if we have good hit vs dodge | |
| if( roll_hit > target_dodge * 3 / 2 ) { | |
| const double chance_double = 0.5 * ( | |
| weapon_crit_chance * stat_crit_chance + | |
| stat_crit_chance * skill_crit_chance + | |
| weapon_crit_chance * skill_crit_chance - | |
| ( 3 * chance_triple ) ); | |
| // Because chance_double already removed the triples with -( 3 * chance_triple ), chance_triple | |
| // and chance_double are mutually exclusive probabilities and can just be added together. | |
| return chance_triple + chance_double; | |
| } | |
| return chance_triple; | |
| } | |
| float player::get_dodge_base() const | |
| { | |
| // TODO: Remove this override? | |
| return Character::get_dodge_base(); | |
| } | |
| float player::get_dodge() const | |
| { | |
| //If we're asleep or busy we can't dodge | |
| if( in_sleep_state() || has_effect( effect_narcosis ) ) { | |
| return 0.0f; | |
| } | |
| float ret = Creature::get_dodge(); | |
| // Chop in half if we are unable to move | |
| if( has_effect( effect_beartrap ) || has_effect( effect_lightsnare ) || | |
| has_effect( effect_heavysnare ) ) { | |
| ret /= 2; | |
| } | |
| int zed_number = 0; | |
| for( auto &dest : g->m.points_in_radius( pos(), 1, 0 ) ) { | |
| const monster *const mon = g->critter_at<monster>( dest ); | |
| if( mon && ( mon->has_flag( MF_GRABS ) || | |
| mon->type->has_special_attack( "GRAB" ) ) ) { | |
| zed_number++; | |
| } | |
| } | |
| if( has_effect( effect_grabbed ) && zed_number > 0 ) { | |
| ret /= zed_number + 1; | |
| } | |
| if( worn_with_flag( "ROLLER_INLINE" ) || | |
| worn_with_flag( "ROLLER_QUAD" ) || | |
| worn_with_flag( "ROLLER_ONE" ) ) { | |
| ret /= has_trait( trait_PROF_SKATER ) ? 2 : 5; | |
| } | |
| if( has_effect( effect_bouldering ) ) { | |
| ret /= 4; | |
| } | |
| // Each dodge after the first subtracts equivalent of 2 points of dodge skill | |
| if( dodges_left <= 0 ) { | |
| ret += dodges_left * 2 - 2; | |
| } | |
| // Speed below 100 linearly decreases dodge effectiveness | |
| int speed_stat = get_speed(); | |
| if( speed_stat < 100 ) { | |
| ret *= speed_stat / 100.0f; | |
| } | |
| return std::max( 0.0f, ret ); | |
| } | |
| float player::dodge_roll() | |
| { | |
| return get_dodge() * 5; | |
| } | |
| float player::bonus_damage( bool random ) const | |
| { | |
| /** @EFFECT_STR increases bashing damage */ | |
| if( random ) { | |
| return rng_float( get_str() / 2.0f, get_str() ); | |
| } | |
| return get_str() * 0.75f; | |
| } | |
| void player::roll_bash_damage( bool crit, damage_instance &di, bool average, | |
| const item &weap ) const | |
| { | |
| float bash_dam = 0.0f; | |
| const bool unarmed = weap.is_unarmed_weapon(); | |
| int skill = get_skill_level( unarmed ? skill_unarmed : skill_bashing ); | |
| if( has_active_bionic( bio_cqb ) ) { | |
| skill = BIO_CQB_LEVEL; | |
| } | |
| const int stat = get_str(); | |
| /** @EFFECT_STR increases bashing damage */ | |
| float stat_bonus = bonus_damage( !average ); | |
| stat_bonus += mabuff_damage_bonus( DT_BASH ); | |
| // Drunken Master damage bonuses | |
| if( has_trait( trait_DRUNKEN ) && has_effect( effect_drunk ) ) { | |
| // Remember, a single drink gives 600 levels of "drunk" | |
| int mindrunk = 0; | |
| int maxdrunk = 0; | |
| const time_duration drunk_dur = get_effect_dur( effect_drunk ); | |
| if( unarmed ) { | |
| mindrunk = drunk_dur / 600_turns; | |
| maxdrunk = drunk_dur / 250_turns; | |
| } else { | |
| mindrunk = drunk_dur / 900_turns; | |
| maxdrunk = drunk_dur / 400_turns; | |
| } | |
| bash_dam += average ? ( mindrunk + maxdrunk ) * 0.5f : rng( mindrunk, maxdrunk ); | |
| } | |
| /** @EFFECT_STR increases bashing damage */ | |
| float weap_dam = weap.damage_melee( DT_BASH ) + stat_bonus; | |
| /** @EFFECT_UNARMED caps bash damage with unarmed weapons */ | |
| /** @EFFECT_BASHING caps bash damage with bashing weapons */ | |
| float bash_cap = 2 * stat + 2 * skill; | |
| float bash_mul = 1.0f; | |
| // 80%, 88%, 96%, 104%, 112%, 116%, 120%, 124%, 128%, 132% | |
| if( skill < 5 ) { | |
| bash_mul = 0.8 + 0.08 * skill; | |
| } else { | |
| bash_mul = 0.96 + 0.04 * skill; | |
| } | |
| if( bash_cap < weap_dam && !weap.is_null() ) { | |
| // If damage goes over cap due to low stats/skills, | |
| // scale the post-armor damage down halfway between damage and cap | |
| bash_mul *= ( 1.0f + ( bash_cap / weap_dam ) ) / 2.0f; | |
| } | |
| /** @EFFECT_STR boosts low cap on bashing damage */ | |
| const float low_cap = std::min( 1.0f, stat / 20.0f ); | |
| const float bash_min = low_cap * weap_dam; | |
| weap_dam = average ? ( bash_min + weap_dam ) * 0.5f : rng_float( bash_min, weap_dam ); | |
| bash_dam += weap_dam; | |
| bash_mul *= mabuff_damage_mult( DT_BASH ); | |
| float armor_mult = 1.0f; | |
| // Finally, extra critical effects | |
| if( crit ) { | |
| bash_mul *= 1.5f; | |
| // 50% armor penetration | |
| armor_mult = 0.5f; | |
| } | |
| di.add_damage( DT_BASH, bash_dam, 0, armor_mult, bash_mul ); | |
| } | |
| void player::roll_cut_damage( bool crit, damage_instance &di, bool average, const item &weap ) const | |
| { | |
| float cut_dam = mabuff_damage_bonus( DT_CUT ) + weap.damage_melee( DT_CUT ); | |
| float cut_mul = 1.0f; | |
| int cutting_skill = get_skill_level( skill_cutting ); | |
| int unarmed_skill = get_skill_level( skill_unarmed ); | |
| if( has_active_bionic( bio_cqb ) ) { | |
| cutting_skill = BIO_CQB_LEVEL; | |
| } | |
| if( weap.is_unarmed_weapon() ) { | |
| // TODO: 1-handed weapons that aren't unarmed attacks | |
| const bool left_empty = !natural_attack_restricted_on( bp_hand_l ); | |
| const bool right_empty = !natural_attack_restricted_on( bp_hand_r ) && | |
| weap.is_null(); | |
| if( left_empty || right_empty ) { | |
| float per_hand = 0.0f; | |
| if( has_trait( trait_CLAWS ) || ( has_active_mutation( trait_CLAWS_RETRACT ) ) ) { | |
| per_hand += 3; | |
| } | |
| if( has_bionic( bionic_id( "bio_razors" ) ) ) { | |
| per_hand += 2; | |
| } | |
| if( has_trait( trait_TALONS ) ) { | |
| /** @EFFECT_UNARMED increases cutting damage with TALONS */ | |
| per_hand += 3 + ( unarmed_skill > 8 ? 4 : unarmed_skill / 2 ); | |
| } | |
| // Stainless Steel Claws do stabbing damage, too. | |
| if( has_trait( trait_CLAWS_RAT ) || has_trait( trait_CLAWS_ST ) ) { | |
| /** @EFFECT_UNARMED increases cutting damage with CLAWS_RAT and CLAWS_ST */ | |
| per_hand += 1 + ( unarmed_skill > 8 ? 4 : unarmed_skill / 2 ); | |
| } | |
| // TODO: add acidproof check back to slime hands (probably move it elsewhere) | |
| if( has_trait( trait_SLIME_HANDS ) ) { | |
| /** @EFFECT_UNARMED increases cutting damage with SLIME_HANDS */ | |
| per_hand += average ? 2.5f : rng( 2, 3 ); | |
| } | |
| cut_dam += per_hand; // First hand | |
| if( left_empty && right_empty ) { | |
| // Second hand | |
| cut_dam += per_hand; | |
| } | |
| } | |
| } | |
| if( cut_dam <= 0.0f ) { | |
| return; // No negative damage! | |
| } | |
| int arpen = 0; | |
| float armor_mult = 1.0f; | |
| // 80%, 88%, 96%, 104%, 112%, 116%, 120%, 124%, 128%, 132% | |
| /** @EFFECT_CUTTING increases cutting damage multiplier */ | |
| if( cutting_skill < 5 ) { | |
| cut_mul *= 0.8 + 0.08 * cutting_skill; | |
| } else { | |
| cut_mul *= 0.96 + 0.04 * cutting_skill; | |
| } | |
| cut_mul *= mabuff_damage_mult( DT_CUT ); | |
| if( crit ) { | |
| cut_mul *= 1.25f; | |
| arpen += 5; | |
| armor_mult = 0.75f; //25% armor penetration | |
| } | |
| di.add_damage( DT_CUT, cut_dam, arpen, armor_mult, cut_mul ); | |
| } | |
| void player::roll_stab_damage( bool crit, damage_instance &di, bool average, | |
| const item &weap ) const | |
| { | |
| ( void )average; // No random rolls in stab damage | |
| float cut_dam = mabuff_damage_bonus( DT_STAB ) + weap.damage_melee( DT_STAB ); | |
| int unarmed_skill = get_skill_level( skill_unarmed ); | |
| int stabbing_skill = get_skill_level( skill_stabbing ); | |
| if( has_active_bionic( bio_cqb ) ) { | |
| stabbing_skill = BIO_CQB_LEVEL; | |
| } | |
| if( weap.is_unarmed_weapon() ) { | |
| const bool left_empty = !natural_attack_restricted_on( bp_hand_l ); | |
| const bool right_empty = !natural_attack_restricted_on( bp_hand_r ) && | |
| weap.is_null(); | |
| if( left_empty || right_empty ) { | |
| float per_hand = 0.0f; | |
| if( has_trait( trait_CLAWS ) || has_active_mutation( trait_CLAWS_RETRACT ) ) { | |
| per_hand += 3; | |
| } | |
| if( has_trait( trait_NAILS ) ) { | |
| per_hand += .5; | |
| } | |
| if( has_bionic( bionic_id( "bio_razors" ) ) ) { | |
| per_hand += 2; | |
| } | |
| if( has_trait( trait_THORNS ) ) { | |
| per_hand += 2; | |
| } | |
| if( has_trait( trait_CLAWS_ST ) ) { | |
| /** @EFFECT_UNARMED increases stabbing damage with CLAWS_ST */ | |
| per_hand += 3 + ( unarmed_skill / 2 ); | |
| } | |
| cut_dam += per_hand; // First hand | |
| if( left_empty && right_empty ) { | |
| // Second hand | |
| cut_dam += per_hand; | |
| } | |
| } | |
| } | |
| if( cut_dam <= 0 ) { | |
| return; // No negative stabbing! | |
| } | |
| float stab_mul = 1.0f; | |
| // 66%, 76%, 86%, 96%, 106%, 116%, 122%, 128%, 134%, 140% | |
| /** @EFFECT_STABBING increases stabbing damage multiplier */ | |
| if( stabbing_skill <= 5 ) { | |
| stab_mul = 0.66 + 0.1 * stabbing_skill; | |
| } else { | |
| stab_mul = 0.86 + 0.06 * stabbing_skill; | |
| } | |
| stab_mul *= mabuff_damage_mult( DT_STAB ); | |
| float armor_mult = 1.0f; | |
| if( crit ) { | |
| // Critical damage bonus for stabbing scales with skill | |
| stab_mul *= 1.0 + ( stabbing_skill / 10.0 ); | |
| // Stab criticals have extra %arpen | |
| armor_mult = 0.66f; | |
| } | |
| di.add_damage( DT_STAB, cut_dam, 0, armor_mult, stab_mul ); | |
| } | |
| matec_id player::pick_technique( Creature &t, const item &weap, | |
| bool crit, bool dodge_counter, bool block_counter ) | |
| { | |
| std::vector<matec_id> all = get_all_techniques( weap ); | |
| std::vector<matec_id> possible; | |
| bool downed = t.has_effect( effect_downed ); | |
| // first add non-aoe tecs | |
| for( auto &tec_id : all ) { | |
| const ma_technique &tec = tec_id.obj(); | |
| // ignore "dummy" techniques like WBLOCK_1 | |
| if( tec.dummy ) { | |
| continue; | |
| } | |
| // skip defensive techniques | |
| if( tec.defensive ) { | |
| continue; | |
| } | |
| // skip dodge counter techniques | |
| if( dodge_counter != tec.dodge_counter ) { | |
| continue; | |
| } | |
| // skip block counter techniques | |
| if( block_counter != tec.block_counter ) { | |
| continue; | |
| } | |
| // if critical then select only from critical tecs | |
| // but allow the technique if its crit ok | |
| if( !tec.crit_ok && ( crit != tec.crit_tec ) ) { | |
| continue; | |
| } | |
| // don't apply downing techniques to someone who's already downed | |
| if( downed && tec.down_dur > 0 ) { | |
| continue; | |
| } | |
| // don't apply disarming techniques to someone without a weapon | |
| // TODO: these are the stat requirements for tec_disarm | |
| // dice( dex_cur + get_skill_level("unarmed"), 8) > | |
| // dice(p->dex_cur + p->get_skill_level("melee"), 10)) | |
| if( tec.disarms && !t.has_weapon() ) { | |
| continue; | |
| } | |
| // if aoe, check if there are valid targets | |
| if( !tec.aoe.empty() && !valid_aoe_technique( t, tec ) ) { | |
| continue; | |
| } | |
| // If we have negative weighting then roll to see if it's valid this time | |
| if( tec.weighting < 0 && !one_in( abs( tec.weighting ) ) ) { | |
| continue; | |
| } | |
| if( tec.is_valid_player( *this ) ) { | |
| possible.push_back( tec.id ); | |
| //add weighted options into the list extra times, to increase their chance of being selected | |
| if( tec.weighting > 1 ) { | |
| for( int i = 1; i < tec.weighting; i++ ) { | |
| possible.push_back( tec.id ); | |
| } | |
| } | |
| } | |
| } | |
| return random_entry( possible, tec_none ); | |
| } | |
| bool player::valid_aoe_technique( Creature &t, const ma_technique &technique ) | |
| { | |
| std::vector<Creature *> dummy_targets; | |
| return valid_aoe_technique( t, technique, dummy_targets ); | |
| } | |
| bool player::valid_aoe_technique( Creature &t, const ma_technique &technique, | |
| std::vector<Creature *> &targets ) | |
| { | |
| if( technique.aoe.empty() ) { | |
| return false; | |
| } | |
| // pre-computed matrix of adjacent squares | |
| std::array<int, 9> offset_a = {{0, -1, -1, 1, 0, -1, 1, 1, 0 }}; | |
| std::array<int, 9> offset_b = {{-1, -1, 0, -1, 0, 1, 0, 1, 1 }}; | |
| // filter the values to be between -1 and 1 to avoid indexing the array out of bounds | |
| int dy = std::max( -1, std::min( 1, t.posy() - posy() ) ); | |
| int dx = std::max( -1, std::min( 1, t.posx() - posx() ) ); | |
| int lookup = dy + 1 + 3 * ( dx + 1 ); | |
| //wide hits all targets adjacent to the attacker and the target | |
| if( technique.aoe == "wide" ) { | |
| //check if either (or both) of the squares next to our target contain a possible victim | |
| //offsets are a pre-computed matrix allowing us to quickly lookup adjacent squares | |
| tripoint left = pos() + tripoint( offset_a[lookup], offset_b[lookup], 0 ); | |
| tripoint right = pos() + tripoint( offset_b[lookup], -offset_a[lookup], 0 ); | |
| monster *const mon_l = g->critter_at<monster>( left ); | |
| if( mon_l && mon_l->friendly == 0 ) { | |
| targets.push_back( mon_l ); | |
| } | |
| monster *const mon_r = g->critter_at<monster>( right ); | |
| if( mon_r && mon_r->friendly == 0 ) { | |
| targets.push_back( mon_r ); | |
| } | |
| npc *const npc_l = g->critter_at<npc>( left ); | |
| npc *const npc_r = g->critter_at<npc>( right ); | |
| if( npc_l && npc_l->is_enemy() ) { | |
| targets.push_back( npc_l ); | |
| } | |
| if( npc_r && npc_r->is_enemy() ) { | |
| targets.push_back( npc_r ); | |
| } | |
| if( !targets.empty() ) { | |
| return true; | |
| } | |
| } | |
| if( technique.aoe == "impale" ) { | |
| // Impale hits the target and a single target behind them | |
| // Check if the square cardinally behind our target, or to the left / right, | |
| // contains a possible target. | |
| tripoint left = t.pos() + tripoint( offset_a[lookup], offset_b[lookup], 0 ); | |
| tripoint target_pos = t.pos() + ( t.pos() - pos() ); | |
| tripoint right = t.pos() + tripoint( offset_b[lookup], -offset_b[lookup], 0 ); | |
| monster *const mon_l = g->critter_at<monster>( left ); | |
| monster *const mon_t = g->critter_at<monster>( target_pos ); | |
| monster *const mon_r = g->critter_at<monster>( right ); | |
| if( mon_l && mon_l->friendly == 0 ) { | |
| targets.push_back( mon_l ); | |
| } | |
| if( mon_t && mon_t->friendly == 0 ) { | |
| targets.push_back( mon_t ); | |
| } | |
| if( mon_r && mon_r->friendly == 0 ) { | |
| targets.push_back( mon_r ); | |
| } | |
| npc *const npc_l = g->critter_at<npc>( left ); | |
| npc *const npc_t = g->critter_at<npc>( target_pos ); | |
| npc *const npc_r = g->critter_at<npc>( right ); | |
| if( npc_l && npc_l->is_enemy() ) { | |
| targets.push_back( npc_l ); | |
| } | |
| if( npc_t && npc_t->is_enemy() ) { | |
| targets.push_back( npc_t ); | |
| } | |
| if( npc_r && npc_r->is_enemy() ) { | |
| targets.push_back( npc_r ); | |
| } | |
| if( !targets.empty() ) { | |
| return true; | |
| } | |
| } | |
| if( targets.empty() && technique.aoe == "spin" ) { | |
| for( const tripoint &tmp : g->m.points_in_radius( pos(), 1 ) ) { | |
| if( tmp == t.pos() ) { | |
| continue; | |
| } | |
| monster *const mon = g->critter_at<monster>( tmp ); | |
| if( mon && mon->friendly == 0 ) { | |
| targets.push_back( mon ); | |
| } | |
| npc *const np = g->critter_at<npc>( tmp ); | |
| if( np && np->is_enemy() ) { | |
| targets.push_back( np ); | |
| } | |
| } | |
| //don't trigger circle for fewer than 2 targets | |
| if( targets.size() < 2 ) { | |
| targets.clear(); | |
| } else { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool player::has_technique( const matec_id &id, const item &weap ) const | |
| { | |
| return weap.has_technique( id ) || | |
| style_selected.obj().has_technique( *this, id ); | |
| } | |
| static damage_unit &get_damage_unit( std::vector<damage_unit> &di, const damage_type dt ) | |
| { | |
| static damage_unit nullunit( DT_NULL, 0, 0, 0, 0 ); | |
| for( auto &du : di ) { | |
| if( du.type == dt && du.amount > 0 ) { | |
| return du; | |
| } | |
| } | |
| return nullunit; | |
| } | |
| static void print_damage_info( const damage_instance &di ) | |
| { | |
| if( !debug_mode ) { | |
| return; | |
| } | |
| int total = 0; | |
| std::ostringstream ss; | |
| for( auto &du : di.damage_units ) { | |
| int amount = di.type_damage( du.type ); | |
| total += amount; | |
| ss << name_by_dt( du.type ) << ':' << amount << ','; | |
| } | |
| add_msg( m_debug, "%stotal: %d", ss.str(), total ); | |
| } | |
| void player::perform_technique( const ma_technique &technique, Creature &t, damage_instance &di, | |
| int &move_cost ) | |
| { | |
| add_msg( m_debug, "dmg before tec:" ); | |
| print_damage_info( di ); | |
| for( damage_unit &du : di.damage_units ) { | |
| // TODO: Allow techniques to add more damage types to attacks | |
| if( du.amount <= 0 ) { | |
| continue; | |
| } | |
| du.amount += technique.damage_bonus( *this, du.type ); | |
| du.damage_multiplier *= technique.damage_multiplier( *this, du.type ); | |
| du.res_pen += technique.armor_penetration( *this, du.type ); | |
| } | |
| add_msg( m_debug, "dmg after tec:" ); | |
| print_damage_info( di ); | |
| move_cost *= technique.move_cost_multiplier( *this ); | |
| move_cost += technique.move_cost_penalty( *this ); | |
| if( technique.down_dur > 0 ) { | |
| if( t.get_throw_resist() == 0 ) { | |
| t.add_effect( effect_downed, rng( 1_turns, time_duration::from_turns( technique.down_dur ) ) ); | |
| auto &bash = get_damage_unit( di.damage_units, DT_BASH ); | |
| if( bash.amount > 0 ) { | |
| bash.amount += 3; | |
| } | |
| } | |
| } | |
| if( technique.stun_dur > 0 ) { | |
| t.add_effect( effect_stunned, rng( 1_turns, time_duration::from_turns( technique.stun_dur ) ) ); | |
| } | |
| if( technique.knockback_dist > 0 ) { | |
| const tripoint prev_pos = t.pos(); // track target startpoint for knockback_follow | |
| const int kb_offset_x = rng( -technique.knockback_spread, technique.knockback_spread ); | |
| const int kb_offset_y = rng( -technique.knockback_spread, technique.knockback_spread ); | |
| tripoint kb_point( posx() + kb_offset_x, posy() + kb_offset_y, posz() ); | |
| for( int dist = rng( 1, technique.knockback_dist ); dist > 0; dist-- ) { | |
| t.knock_back_from( kb_point ); | |
| } | |
| // This technique makes the player follow into the tile the target was knocked from | |
| if( technique.knockback_follow > 0 ) { | |
| // Check if terrain there is safe then if a critter's still there - if clear, move player there | |
| if( !g->prompt_dangerous_tile( prev_pos ) ) { | |
| return; | |
| } else { | |
| if( t.pos() != prev_pos ) { | |
| g->place_player( prev_pos ); | |
| } | |
| } | |
| } | |
| } | |
| player *p = dynamic_cast<player *>( &t ); | |
| if( technique.disarms && p != nullptr && p->is_armed() ) { | |
| g->m.add_item_or_charges( p->pos(), p->remove_weapon() ); | |
| if( p->is_player() ) { | |
| add_msg_if_npc( _( "<npcname> disarms you!" ) ); | |
| } else { | |
| add_msg_player_or_npc( _( "You disarm %s!" ), | |
| _( "<npcname> disarms %s!" ), | |
| p->name ); | |
| } | |
| } | |
| //AOE attacks, feel free to skip over this lump | |
| if( technique.aoe.length() > 0 ) { | |
| // Remember out moves and stamina | |
| // We don't want to consume them for every attack! | |
| const int temp_moves = moves; | |
| const int temp_stamina = stamina; | |
| std::vector<Creature *> targets; | |
| valid_aoe_technique( t, technique, targets ); | |
| //hit only one valid target (pierce through doesn't spread out) | |
| if( technique.aoe == "impale" ) { | |
| // TODO: what if targets is empty | |
| Creature *const v = random_entry( targets ); | |
| targets.clear(); | |
| targets.push_back( v ); | |
| } | |
| //hit the targets in the lists (all candidates if wide or burst, or just the unlucky sod if deep) | |
| int count_hit = 0; | |
| for( Creature *const c : targets ) { | |
| melee_attack( *c, false ); | |
| } | |
| t.add_msg_if_player( m_good, ngettext( "%d enemy hit!", "%d enemies hit!", count_hit ), count_hit ); | |
| // Extra attacks are free of charge (otherwise AoE attacks would SUCK) | |
| moves = temp_moves; | |
| stamina = temp_stamina; | |
| } | |
| //player has a very small chance, based on their intelligence, to learn a style whilst using the CQB bionic | |
| if( has_active_bionic( bio_cqb ) && !has_martialart( style_selected ) ) { | |
| /** @EFFECT_INT slightly increases chance to learn techniques when using CQB bionic */ | |
| // Enhanced Memory Banks bionic doubles chance to learn martial art | |
| const int bionic_boost = has_active_bionic( bionic_id( bio_memory ) ) ? 2 : 1; | |
| if( one_in( ( 1400 - ( get_int() * 50 ) ) / bionic_boost ) ) { | |
| ma_styles.push_back( style_selected ); | |
| add_msg_if_player( m_good, _( "You have learned %s from extensive practice with the CQB Bionic." ), | |
| _( style_selected.obj().name ) ); | |
| } | |
| } | |
| } | |
| static int blocking_ability( const item &shield ) | |
| { | |
| int block_bonus = 0; | |
| if( shield.has_technique( WBLOCK_3 ) ) { | |
| block_bonus = 10; | |
| } else if( shield.has_technique( WBLOCK_2 ) ) { | |
| block_bonus = 6; | |
| } else if( shield.has_technique( WBLOCK_1 ) ) { | |
| block_bonus = 4; | |
| } else if( shield.has_flag( "BLOCK_WHILE_WORN" ) ) { | |
| block_bonus = 2; | |
| } | |
| return block_bonus; | |
| } | |
| item &player::best_shield() | |
| { | |
| // Note: wielded weapon, not one used for attacks | |
| int best_value = blocking_ability( weapon ); | |
| // "BLOCK_WHILE_WORN" without a blocking tech need to be worn for the bonus | |
| best_value = best_value == 2 ? 0 : best_value; | |
| item *best = best_value > 0 ? &weapon : &null_item_reference(); | |
| for( item &shield : worn ) { | |
| if( shield.has_flag( "BLOCK_WHILE_WORN" ) && blocking_ability( shield ) >= best_value ) { | |
| best = &shield; | |
| } | |
| } | |
| return *best; | |
| } | |
| bool player::block_hit( Creature *source, body_part &bp_hit, damage_instance &dam ) | |
| { | |
| // Shouldn't block if player is asleep; this only seems to be used by player. | |
| // TODO: It should probably be moved to the section that regenerates blocks | |
| // and to effects that disallow blocking | |
| if( blocks_left < 1 || in_sleep_state() ) { | |
| return false; | |
| } | |
| blocks_left--; | |
| ma_ongethit_effects(); // fire martial arts on-getting-hit-triggered effects | |
| // these fire even if the attack is blocked (you still got hit) | |
| // This bonus absorbs damage from incoming attacks before they land, | |
| // but it still counts as a block even if it absorbs all the damage. | |
| float total_phys_block = mabuff_block_bonus(); | |
| // Extract this to make it easier to implement shields/multiwield later | |
| item &shield = best_shield(); | |
| bool conductive_shield = shield.conductive(); | |
| bool unarmed = shield.has_flag( "UNARMED_WEAPON" ); | |
| // This gets us a number between: | |
| // str ~0 + skill 0 = 0 | |
| // str ~20 + skill 10 + 10(unarmed skill or weapon bonus) = 40 | |
| int block_score = 1; | |
| // Remember if we're using a weapon or a limb to block. | |
| // So that we don't suddenly switch that for any reason. | |
| const bool weapon_blocking = !shield.is_null(); | |
| if( !is_force_unarmed() && weapon_blocking ) { | |
| /** @EFFECT_STR increases attack blocking effectiveness with a weapon */ | |
| /** @EFFECT_MELEE increases attack blocking effectiveness with a weapon */ | |
| block_bonus = blocking_ability( shield ); | |
| block_score = str_cur + block_bonus + get_skill_level( skill_melee ); | |
| } else if( can_limb_block() ) { | |
| /** @EFFECT_STR increases attack blocking effectiveness with a limb */ | |
| /** @EFFECT_UNARMED increases attack blocking effectiveness with a limb */ | |
| block_score = str_cur + get_skill_level( skill_melee ) + get_skill_level( skill_unarmed ); | |
| } else { | |
| // Can't block with items, can't block with limbs | |
| // What were we supposed to be blocking with then? | |
| return false; | |
| } | |
| // Map block_score to the logistic curve for a number between 1 and 0. | |
| // Basic beginner character (str 8, skill 0, basic weapon) | |
| // Will have a score around 10 and block about %15 of incoming damage. | |
| // More proficient melee character (str 10, skill 4, wbock_2 weapon) | |
| // will have a score of 20 and block about 45% of damage. | |
| // A highly expert character (str 14, skill 8 wblock_2) | |
| // will have a score in the high 20s and will block about 80% of damage. | |
| // As the block score approaches 40, damage making it through will dwindle | |
| // to nothing, at which point we're relying on attackers hitting enough to drain blocks. | |
| const float physical_block_multiplier = logarithmic_range( 0, 40, block_score ); | |
| float total_damage = 0.0; | |
| float damage_blocked = 0.0; | |
| for( auto &elem : dam.damage_units ) { | |
| total_damage += elem.amount; | |
| // block physical damage "normally" | |
| if( elem.type == DT_BASH || elem.type == DT_CUT || elem.type == DT_STAB ) { | |
| // use up our flat block bonus first | |
| float block_amount = std::min( total_phys_block, elem.amount ); | |
| total_phys_block -= block_amount; | |
| elem.amount -= block_amount; | |
| damage_blocked += block_amount; | |
| if( elem.amount <= std::numeric_limits<float>::epsilon() ) { | |
| continue; | |
| } | |
| float previous_amount = elem.amount; | |
| elem.amount *= physical_block_multiplier; | |
| damage_blocked += previous_amount - elem.amount; | |
| // non-electrical "elemental" damage types do their full damage if unarmed, | |
| // but severely mitigated damage if not | |
| } else if( elem.type == DT_HEAT || elem.type == DT_ACID || elem.type == DT_COLD ) { | |
| // Unarmed weapons won't block those | |
| if( !is_force_unarmed() && weapon_blocking && !unarmed ) { | |
| float previous_amount = elem.amount; | |
| elem.amount /= 5; | |
| damage_blocked += previous_amount - elem.amount; | |
| } | |
| // electrical damage deals full damage if unarmed OR wielding a | |
| // conductive weapon | |
| } else if( elem.type == DT_ELECTRIC ) { | |
| // Unarmed weapons and conductive weapons won't block this | |
| if( !is_force_unarmed() && weapon_blocking && !unarmed && !conductive_shield ) { | |
| float previous_amount = elem.amount; | |
| elem.amount /= 5; | |
| damage_blocked += previous_amount - elem.amount; | |
| } | |
| } | |
| } | |
| ma_onblock_effects(); // fire martial arts block-triggered effects | |
| // weapon blocks are preferred to limb blocks | |
| std::string thing_blocked_with; | |
| if( !is_force_unarmed() && weapon_blocking ) { | |
| thing_blocked_with = shield.tname(); | |
| // TODO: Change this depending on damage blocked | |
| float wear_modifier = 1.0f; | |
| if( source != nullptr && source->is_hallucination() ) { | |
| wear_modifier = 0.0f; | |
| } | |
| handle_melee_wear( shield, wear_modifier ); | |
| } else { | |
| //Choose which body part to block with, assume left side first | |
| if( can_leg_block() && can_arm_block() ) { | |
| bp_hit = one_in( 2 ) ? bp_leg_l : bp_arm_l; | |
| } else if( can_leg_block() ) { | |
| bp_hit = bp_leg_l; | |
| } else { | |
| bp_hit = bp_arm_l; | |
| } | |
| // Check if we should actually use the right side to block | |
| if( bp_hit == bp_leg_l ) { | |
| if( hp_cur[hp_leg_r] > hp_cur[hp_leg_l] ) { | |
| bp_hit = bp_leg_r; | |
| } | |
| } else { | |
| if( hp_cur[hp_arm_r] > hp_cur[hp_arm_l] ) { | |
| bp_hit = bp_arm_r; | |
| } | |
| } | |
| thing_blocked_with = body_part_name( bp_hit ); | |
| } | |
| std::string damage_blocked_description; | |
| // good/bad/ugly add_msg color code? | |
| // none, hardly any, a little, some, most, all | |
| float blocked_ratio = 0.0f; | |
| if( total_damage > std::numeric_limits<float>::epsilon() ) { | |
| blocked_ratio = ( total_damage - damage_blocked ) / total_damage; | |
| } | |
| if( blocked_ratio < std::numeric_limits<float>::epsilon() ) { | |
| //~ Adjective in "You block <adjective> of the damage with your <weapon>. | |
| damage_blocked_description = _( "all" ); | |
| } else if( blocked_ratio < 0.2 ) { | |
| //~ Adjective in "You block <adjective> of the damage with your <weapon>. | |
| damage_blocked_description = _( "nearly all" ); | |
| } else if( blocked_ratio < 0.4 ) { | |
| //~ Adjective in "You block <adjective> of the damage with your <weapon>. | |
| damage_blocked_description = _( "most" ); | |
| } else if( blocked_ratio < 0.6 ) { | |
| //~ Adjective in "You block <adjective> of the damage with your <weapon>. | |
| damage_blocked_description = _( "a lot" ); | |
| } else if( blocked_ratio < 0.8 ) { | |
| //~ Adjective in "You block <adjective> of the damage with your <weapon>. | |
| damage_blocked_description = _( "some" ); | |
| } else if( blocked_ratio > std::numeric_limits<float>::epsilon() ) { | |
| //~ Adjective in "You block <adjective> of the damage with your <weapon>. | |
| damage_blocked_description = _( "a little" ); | |
| } else { | |
| //~ Adjective in "You block <adjective> of the damage with your <weapon>. | |
| damage_blocked_description = _( "none" ); | |
| } | |
| add_msg_player_or_npc( _( "You block %1$s of the damage with your %2$s!" ), | |
| _( "<npcname> blocks %1$s of the damage with their %2$s!" ), | |
| damage_blocked_description, thing_blocked_with ); | |
| // Check if we have any block counters | |
| matec_id tec = pick_technique( *source, shield, false, false, true ); | |
| if( tec != tec_none ) { | |
| melee_attack( *source, false, tec ); | |
| } | |
| return true; | |
| } | |
| void player::perform_special_attacks( Creature &t ) | |
| { | |
| bool can_poison = false; | |
| std::vector<special_attack> special_attacks = mutation_attacks( t ); | |
| std::string target = t.disp_name(); | |
| bool practiced = false; | |
| for( const auto &att : special_attacks ) { | |
| if( t.is_dead_state() ) { | |
| break; | |
| } | |
| dealt_damage_instance dealt_dam; | |
| // TODO: Make this hit roll use unarmed skill, not weapon skill + weapon to_hit | |
| int hit_spread = t.deal_melee_attack( this, hit_roll() * 0.8 ); | |
| if( hit_spread >= 0 ) { | |
| t.deal_melee_hit( this, hit_spread, false, att.damage, dealt_dam ); | |
| if( !practiced ) { | |
| // Practice unarmed, at most once per combo | |
| practiced = true; | |
| practice( skill_unarmed, rng( 0, 10 ) ); | |
| } | |
| } | |
| int dam = dealt_dam.total_damage(); | |
| if( dam > 0 ) { | |
| player_hit_message( this, att.text, t, dam ); | |
| } | |
| can_poison = can_poison || | |
| dealt_dam.type_damage( DT_CUT ) > 0 || | |
| dealt_dam.type_damage( DT_STAB ) > 0; | |
| } | |
| if( can_poison && ( has_trait( trait_POISONOUS ) || has_trait( trait_POISONOUS2 ) ) ) { | |
| if( has_trait( trait_POISONOUS ) ) { | |
| add_msg_if_player( m_good, _( "You poison %s!" ), target ); | |
| t.add_effect( effect_poison, 6_turns ); | |
| } else if( has_trait( trait_POISONOUS2 ) ) { | |
| add_msg_if_player( m_good, _( "You inject your venom into %s!" ), target ); | |
| t.add_effect( effect_badpoison, 6_turns ); | |
| } | |
| } | |
| } | |
| std::string player::melee_special_effects( Creature &t, damage_instance &d, item &weap ) | |
| { | |
| std::stringstream dump; | |
| std::string target = t.disp_name(); | |
| if( has_active_bionic( bionic_id( "bio_shock" ) ) && power_level >= 2 && | |
| ( !is_armed() || weapon.conductive() ) ) { | |
| charge_power( -2 ); | |
| d.add_damage( DT_ELECTRIC, rng( 2, 10 ) ); | |
| if( is_player() ) { | |
| dump << string_format( _( "You shock %s." ), target ) << std::endl; | |
| } else { | |
| add_msg_if_npc( _( "<npcname> shocks %s." ), target ); | |
| } | |
| } | |
| if( has_active_bionic( bionic_id( "bio_heat_absorb" ) ) && !is_armed() && t.is_warm() ) { | |
| charge_power( 3 ); | |
| d.add_damage( DT_COLD, 3 ); | |
| if( is_player() ) { | |
| dump << string_format( _( "You drain %s's body heat." ), target ) << std::endl; | |
| } else { | |
| add_msg_if_npc( _( "<npcname> drains %s's body heat!" ), target ); | |
| } | |
| } | |
| if( weapon.has_flag( "FLAMING" ) ) { | |
| d.add_damage( DT_HEAT, rng( 1, 8 ) ); | |
| if( is_player() ) { | |
| dump << string_format( _( "You burn %s." ), target ) << std::endl; | |
| } else { | |
| add_msg_player_or_npc( _( "<npcname> burns %s." ), target ); | |
| } | |
| } | |
| //Hurting the wielder from poorly-chosen weapons | |
| if( weap.has_flag( "HURT_WHEN_WIELDED" ) && x_in_y( 2, 3 ) ) { | |
| add_msg_if_player( m_bad, _( "The %s cuts your hand!" ), weap.tname() ); | |
| deal_damage( nullptr, bp_hand_r, damage_instance::physical( 0, weap.damage_melee( DT_CUT ), 0 ) ); | |
| if( weap.is_two_handed( *this ) ) { // Hurt left hand too, if it was big | |
| deal_damage( nullptr, bp_hand_l, damage_instance::physical( 0, weap.damage_melee( DT_CUT ), 0 ) ); | |
| } | |
| } | |
| const int vol = weap.volume() / 250_ml; | |
| // Glass weapons shatter sometimes | |
| if( weap.made_of( material_id( "glass" ) ) && | |
| /** @EFFECT_STR increases chance of breaking glass weapons (NEGATIVE) */ | |
| rng( 0, vol + 8 ) < vol + str_cur ) { | |
| if( is_player() ) { | |
| dump << string_format( _( "Your %s shatters!" ), weap.tname() ) << std::endl; | |
| } else { | |
| add_msg_player_or_npc( m_bad, _( "Your %s shatters!" ), | |
| _( "<npcname>'s %s shatters!" ), | |
| weap.tname() ); | |
| } | |
| sounds::sound( pos(), 16, sounds::sound_t::combat, "Crack!", true, "smash_success", | |
| "smash_glass_contents" ); | |
| // Dump its contents on the ground | |
| for( auto &elem : weap.contents ) { | |
| g->m.add_item_or_charges( pos(), elem ); | |
| } | |
| // Take damage | |
| deal_damage( nullptr, bp_arm_r, damage_instance::physical( 0, rng( 0, vol * 2 ), 0 ) ); | |
| if( weap.is_two_handed( *this ) ) { // Hurt left arm too, if it was big | |
| //redeclare shatter_dam because deal_damage mutates it | |
| deal_damage( nullptr, bp_arm_l, damage_instance::physical( 0, rng( 0, vol * 2 ), 0 ) ); | |
| } | |
| d.add_damage( DT_CUT, rng( 0, 5 + static_cast<int>( vol * 1.5 ) ) ); // Hurt the monster extra | |
| remove_weapon(); | |
| } | |
| if( !t.is_hallucination() ) { | |
| handle_melee_wear( weap ); | |
| } | |
| // on-hit effects for martial arts | |
| ma_onhit_effects(); | |
| return dump.str(); | |
| } | |
| static damage_instance hardcoded_mutation_attack( const player &u, const trait_id &id ) | |
| { | |
| if( id == "BEAK_PECK" ) { | |
| // method open to improvement, please feel free to suggest | |
| // a better way to simulate target's anti-peck efforts | |
| /** @EFFECT_DEX increases number of hits with BEAK_PECK */ | |
| /** @EFFECT_UNARMED increases number of hits with BEAK_PECK */ | |
| int num_hits = std::max( 1, std::min<int>( 6, | |
| u.get_dex() + u.get_skill_level( skill_unarmed ) - rng( 4, 10 ) ) ); | |
| return damage_instance::physical( 0, 0, num_hits * 10 ); | |
| } | |
| if( id == "ARM_TENTACLES" || id == "ARM_TENTACLES_4" || id == "ARM_TENTACLES_8" ) { | |
| int num_attacks = 1; | |
| if( id == "ARM_TENTACLES_4" ) { | |
| num_attacks = 3; | |
| } else if( id == "ARM_TENTACLES_8" ) { | |
| num_attacks = 7; | |
| } | |
| // Note: we're counting arms, so we want wielded item here, not weapon used for attack | |
| if( u.weapon.is_two_handed( u ) || !u.has_two_arms() || u.worn_with_flag( "RESTRICT_HANDS" ) ) { | |
| num_attacks--; | |
| } | |
| if( num_attacks <= 0 ) { | |
| return damage_instance(); | |
| } | |
| const bool rake = u.has_trait( trait_CLAWS_TENTACLE ); | |
| /** @EFFECT_STR increases damage with ARM_TENTACLES* */ | |
| damage_instance ret; | |
| if( rake ) { | |
| ret.add_damage( DT_CUT, u.get_str() / 2.0f + 1.0f, 0, 1.0f, num_attacks ); | |
| } else { | |
| ret.add_damage( DT_BASH, u.get_str() / 3.0f + 1.0f, 0, 1.0f, num_attacks ); | |
| } | |
| return ret; | |
| } | |
| if( id == "VINES2" || id == "VINES3" ) { | |
| const int num_attacks = id == "VINES2" ? 2 : 3; | |
| /** @EFFECT_STR increases damage with VINES* */ | |
| damage_instance ret; | |
| ret.add_damage( DT_BASH, u.get_str() / 2.0f, 0, 1.0f, num_attacks ); | |
| return ret; | |
| } | |
| debugmsg( "Invalid hardcoded mutation id: %s", id.c_str() ); | |
| return damage_instance(); | |
| } | |
| std::vector<special_attack> player::mutation_attacks( Creature &t ) const | |
| { | |
| std::vector<special_attack> ret; | |
| std::string target = t.disp_name(); | |
| const auto usable_body_parts = exclusive_flag_coverage( "ALLOWS_NATURAL_ATTACKS" ); | |
| const int unarmed = get_skill_level( skill_unarmed ); | |
| for( const auto &pr : my_mutations ) { | |
| const auto &branch = pr.first.obj(); | |
| for( const auto &mut_atk : branch.attacks_granted ) { | |
| // Covered body part | |
| if( mut_atk.bp != num_bp && !usable_body_parts.test( mut_atk.bp ) ) { | |
| continue; | |
| } | |
| /** @EFFECT_UNARMED increases chance of attacking with mutated body parts */ | |
| /** @EFFECT_DEX increases chance of attacking with mutated body parts */ | |
| // Calculate actor ability value to be compared against mutation attack difficulty and add debug message | |
| const int proc_value = get_dex() + unarmed; | |
| add_msg( m_debug, "%s proc chance: %d in %d", pr.first.c_str(), proc_value, mut_atk.chance ); | |
| // If the mutation attack fails to proc, bail out | |
| if( !x_in_y( proc_value, mut_atk.chance ) ) { | |
| continue; | |
| } | |
| // If player has any blocker, bail out | |
| if( std::any_of( mut_atk.blocker_mutations.begin(), mut_atk.blocker_mutations.end(), | |
| [this]( const trait_id & blocker ) { | |
| return has_trait( blocker ); | |
| } ) ) { | |
| add_msg( m_debug, "%s not procing: blocked", pr.first.c_str() ); | |
| continue; | |
| } | |
| // Player must have all needed traits | |
| if( !std::all_of( mut_atk.required_mutations.begin(), mut_atk.required_mutations.end(), | |
| [this]( const trait_id & need ) { | |
| return has_trait( need ); | |
| } ) ) { | |
| add_msg( m_debug, "%s not procing: unmet req", pr.first.c_str() ); | |
| continue; | |
| } | |
| special_attack tmp; | |
| // Ugly special case: player's strings have only 1 variable, NPC have 2 | |
| // Can't use <npcname> here | |
| // TODO: Fix | |
| if( is_player() ) { | |
| tmp.text = string_format( _( mut_atk.attack_text_u ), target ); | |
| } else { | |
| tmp.text = string_format( _( mut_atk.attack_text_npc ), name, target ); | |
| } | |
| // Attack starts here | |
| if( mut_atk.hardcoded_effect ) { | |
| tmp.damage = hardcoded_mutation_attack( *this, pr.first ); | |
| } else { | |
| damage_instance dam = mut_atk.base_damage; | |
| damage_instance scaled = mut_atk.strength_damage; | |
| scaled.mult_damage( std::min<float>( 15.0f, get_str() ), true ); | |
| dam.add( scaled ); | |
| tmp.damage = dam; | |
| } | |
| if( tmp.damage.total_damage() > 0.0f ) { | |
| ret.emplace_back( tmp ); | |
| } else { | |
| add_msg( m_debug, "%s not procing: zero damage", pr.first.c_str() ); | |
| } | |
| } | |
| } | |
| return ret; | |
| } | |
| std::string melee_message( const ma_technique &tec, player &p, const dealt_damage_instance &ddi ) | |
| { | |
| // Those could be extracted to a json | |
| // Three last values are for low damage | |
| static const std::array<std::string, 6> player_stab = {{ | |
| _( "You impale %s" ), _( "You gouge %s" ), _( "You run %s through" ), | |
| _( "You puncture %s" ), _( "You pierce %s" ), _( "You poke %s" ) | |
| } | |
| }; | |
| static const std::array<std::string, 6> npc_stab = {{ | |
| _( "<npcname> impales %s" ), _( "<npcname> gouges %s" ), _( "<npcname> runs %s through" ), | |
| _( "<npcname> punctures %s" ), _( "<npcname> pierces %s" ), _( "<npcname> pokes %s" ) | |
| } | |
| }; | |
| // First 5 are for high damage, next 2 for medium, then for low and then for v. low | |
| static const std::array<std::string, 9> player_cut = {{ | |
| _( "You gut %s" ), _( "You chop %s" ), _( "You slash %s" ), | |
| _( "You mutilate %s" ), _( "You maim %s" ), _( "You stab %s" ), | |
| _( "You slice %s" ), _( "You cut %s" ), _( "You nick %s" ) | |
| } | |
| }; | |
| static const std::array<std::string, 9> npc_cut = {{ | |
| _( "<npcname> guts %s" ), _( "<npcname> chops %s" ), _( "<npcname> slashes %s" ), | |
| _( "<npcname> mutilates %s" ), _( "<npcname> maims %s" ), _( "<npcname> stabs %s" ), | |
| _( "<npcname> slices %s" ), _( "<npcname> cuts %s" ), _( "<npcname> nicks %s" ) | |
| } | |
| }; | |
| // Three last values are for low damage | |
| static const std::array<std::string, 6> player_bash = {{ | |
| _( "You clobber %s" ), _( "You smash %s" ), _( "You thrash %s" ), | |
| _( "You batter %s" ), _( "You whack %s" ), _( "You hit %s" ) | |
| } | |
| }; | |
| static const std::array<std::string, 6> npc_bash = {{ | |
| _( "<npcname> clobbers %s" ), _( "<npcname> smashes %s" ), _( "<npcname> thrashes %s" ), | |
| _( "<npcname> batters %s" ), _( "<npcname> whacks %s" ), _( "<npcname> hits %s" ) | |
| } | |
| }; | |
| const int bash_dam = ddi.type_damage( DT_BASH ); | |
| const int cut_dam = ddi.type_damage( DT_CUT ); | |
| const int stab_dam = ddi.type_damage( DT_STAB ); | |
| if( tec.id != tec_none ) { | |
| std::string message; | |
| if( p.is_npc() ) { | |
| message = _( tec.npc_message ); | |
| } else { | |
| message = _( tec.player_message ); | |
| } | |
| if( !message.empty() ) { | |
| return message; | |
| } | |
| } | |
| damage_type dominant_type = DT_BASH; | |
| if( cut_dam + stab_dam > bash_dam ) { | |
| dominant_type = cut_dam >= stab_dam ? DT_CUT : DT_STAB; | |
| } | |
| const bool npc = p.is_npc(); | |
| // Cutting has more messages and so needs different handling | |
| const bool cutting = dominant_type == DT_CUT; | |
| size_t index; | |
| const int total_dam = bash_dam + stab_dam + cut_dam; | |
| if( total_dam > 30 ) { | |
| index = cutting ? rng( 0, 4 ) : rng( 0, 2 ); | |
| } else if( total_dam > 20 ) { | |
| index = cutting ? rng( 5, 6 ) : 3; | |
| } else if( total_dam > 10 ) { | |
| index = cutting ? 7 : 4; | |
| } else { | |
| index = cutting ? 8 : 5; | |
| } | |
| if( dominant_type == DT_STAB ) { | |
| return ( npc ? npc_stab[index] : player_stab[index] ); | |
| } else if( dominant_type == DT_CUT ) { | |
| return ( npc ? npc_cut[index] : player_cut[index] ); | |
| } else if( dominant_type == DT_BASH ) { | |
| return ( npc ? npc_bash[index] : player_bash[index] ); | |
| } | |
| return _( "The bugs attack %s" ); | |
| } | |
| // display the hit message for an attack | |
| void player_hit_message( player *attacker, const std::string &message, | |
| Creature &t, int dam, bool crit ) | |
| { | |
| std::string msg; | |
| game_message_type msgtype = m_good; | |
| std::string sSCTmod; | |
| game_message_type gmtSCTcolor = m_good; | |
| if( dam <= 0 ) { | |
| if( attacker->is_npc() ) { | |
| //~ NPC hits something but does no damage | |
| msg = string_format( _( "%s but does no damage." ), message ); | |
| } else { | |
| //~ someone hits something but do no damage | |
| msg = string_format( _( "%s but do no damage." ), message ); | |
| } | |
| msgtype = m_neutral; | |
| } else if( | |
| crit ) { //Player won't see exact numbers of damage dealt by NPC unless player has DEBUG_NIGHTVISION trait | |
| if( attacker->is_npc() && !g->u.has_trait( trait_DEBUG_NIGHTVISION ) ) { | |
| //~ NPC hits something (critical) | |
| msg = string_format( _( "%s. Critical!" ), message ); | |
| } else { | |
| //~ someone hits something for %d damage (critical) | |
| msg = string_format( _( "%s for %d damage. Critical!" ), message, dam ); | |
| } | |
| sSCTmod = _( "Critical!" ); | |
| gmtSCTcolor = m_critical; | |
| } else { | |
| if( attacker->is_npc() && !g->u.has_trait( trait_DEBUG_NIGHTVISION ) ) { | |
| //~ NPC hits something | |
| msg = string_format( _( "%s." ), message ); | |
| } else { | |
| //~ someone hits something for %d damage | |
| msg = string_format( _( "%s for %d damage." ), message, dam ); | |
| } | |
| } | |
| if( dam > 0 && attacker->is_player() ) { | |
| //player hits monster melee | |
| SCT.add( t.posx(), | |
| t.posy(), | |
| direction_from( 0, 0, t.posx() - attacker->posx(), t.posy() - attacker->posy() ), | |
| get_hp_bar( dam, t.get_hp_max(), true ).first, m_good, | |
| sSCTmod, gmtSCTcolor ); | |
| if( t.get_hp() > 0 ) { | |
| SCT.add( t.posx(), | |
| t.posy(), | |
| direction_from( 0, 0, t.posx() - attacker->posx(), t.posy() - attacker->posy() ), | |
| get_hp_bar( t.get_hp(), t.get_hp_max(), true ).first, m_good, | |
| //~ "hit points", used in scrolling combat text | |
| _( "hp" ), m_neutral, | |
| "hp" ); | |
| } else { | |
| SCT.removeCreatureHP(); | |
| } | |
| } | |
| // same message is used for player and npc, | |
| // just using this for the <npcname> substitution. | |
| attacker->add_msg_player_or_npc( msgtype, msg, msg, t.disp_name() ); | |
| } | |
| int player::attack_speed( const item &weap ) const | |
| { | |
| const int base_move_cost = weap.attack_time() / 2; | |
| const int melee_skill = has_active_bionic( bionic_id( bio_cqb ) ) ? BIO_CQB_LEVEL : get_skill_level( | |
| skill_melee ); | |
| /** @EFFECT_MELEE increases melee attack speed */ | |
| const int skill_cost = static_cast<int>( ( base_move_cost * ( 15 - melee_skill ) / 15 ) ); | |
| /** @EFFECT_DEX increases attack speed */ | |
| const int dexbonus = dex_cur / 2; | |
| const int encumbrance_penalty = encumb( bp_torso ) + | |
| ( encumb( bp_hand_l ) + encumb( bp_hand_r ) ) / 2; | |
| const int ma_move_cost = mabuff_attack_cost_penalty(); | |
| const float stamina_ratio = static_cast<float>( stamina ) / static_cast<float>( get_stamina_max() ); | |
| // Increase cost multiplier linearly from 1.0 to 2.0 as stamina goes from 25% to 0%. | |
| const float stamina_penalty = 1.0 + std::max( ( 0.25f - stamina_ratio ) * 4.0f, 0.0f ); | |
| const float ma_mult = mabuff_attack_cost_mult(); | |
| 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; | |
| // Martial arts last. Flat has to be after mult, because comments say so. | |
| move_cost *= ma_mult; | |
| move_cost += ma_move_cost; | |
| move_cost *= mutation_value( "attackcost_modifier" ); | |
| if( move_cost < 25 ) { | |
| return 25; | |
| } | |
| return move_cost; | |
| } | |
| double player::weapon_value( const item &weap, int ammo ) const | |
| { | |
| if( &weapon == &weap ) { | |
| auto cached_value = cached_info.find( "weapon_value" ); | |
| if( cached_value != cached_info.end() ) { | |
| return cached_value->second; | |
| } | |
| } | |
| const double val_gun = gun_value( weap, ammo ); | |
| const double val_melee = melee_value( weap ); | |
| const double more = std::max( val_gun, val_melee ); | |
| const double less = std::min( val_gun, val_melee ); | |
| // A small bonus for guns you can also use to hit stuff with (bayonets etc.) | |
| const double my_val = more + ( less / 2.0 ); | |
| add_msg( m_debug, "%s (%ld ammo) sum value: %.1f", weap.type->get_id(), ammo, my_val ); | |
| if( &weapon == &weap ) { | |
| cached_info.emplace( "weapon_value", my_val ); | |
| } | |
| return my_val; | |
| } | |
| double player::melee_value( const item &weap ) const | |
| { | |
| double my_value = 0; | |
| damage_instance non_crit; | |
| roll_all_damage( false, non_crit, true, weap ); | |
| float avg_dmg = non_crit.total_damage(); | |
| const int accuracy = weap.type->m_to_hit + get_hit_weapon( weap ); | |
| if( accuracy < 0 ) { | |
| // Heavy penalty | |
| my_value += accuracy * 5; | |
| } else if( accuracy <= 5 ) { | |
| // Big bonus | |
| my_value += accuracy * 3; | |
| } else { | |
| // Small bonus above that | |
| my_value += 15 + ( accuracy - 5 ); | |
| } | |
| int move_cost = attack_speed( weap ); | |
| static const matec_id rapid_strike( "RAPID" ); | |
| if( weap.has_technique( rapid_strike ) ) { | |
| move_cost /= 2; | |
| avg_dmg *= 0.66; | |
| } | |
| const int arbitrary_dodge_target = 5; | |
| double crit_ch = crit_chance( accuracy, arbitrary_dodge_target, weap ); | |
| my_value += crit_ch * 10; // Criticals are worth more than just extra damage | |
| if( crit_ch > 0.1 ) { | |
| damage_instance crit; | |
| roll_all_damage( true, crit, true, weap ); | |
| // Note: intentionally doesn't include rapid attack bonus in criticals | |
| avg_dmg = ( 1.0 - crit_ch ) * avg_dmg + crit.total_damage() * crit_ch; | |
| } | |
| my_value += avg_dmg * 100 / move_cost; | |
| float reach = weap.reach_range( *this ); | |
| if( reach > 1.0f ) { | |
| my_value *= 1.0f + 0.5f * ( sqrtf( reach ) - 1.0f ); | |
| } | |
| add_msg( m_debug, "%s as melee: %.1f", weap.type->get_id(), my_value ); | |
| return std::max( 0.0, my_value ); | |
| } | |
| double player::unarmed_value() const | |
| { | |
| // TODO: Martial arts | |
| return melee_value( item() ); | |
| } | |
| void player::disarm( npc &target ) | |
| { | |
| if( !target.is_armed() ) { | |
| return; | |
| } | |
| if( target.is_hallucination() ) { | |
| target.on_attacked( *this ); | |
| return; | |
| } | |
| /** @EFFECT_STR increases chance to disarm, primary stat */ | |
| /** @EFFECT_DEX increases chance to disarm, secondary stat */ | |
| int my_roll = dice( 3, 2 * get_str() + get_dex() ); | |
| /** @EFFECT_MELEE increases chance to disarm */ | |
| my_roll += dice( 3, get_skill_level( skill_melee ) ); | |
| int their_roll = dice( 3, 2 * target.get_str() + target.get_dex() ); | |
| their_roll += dice( 3, target.get_per() ); | |
| their_roll += dice( 3, target.get_skill_level( skill_melee ) ); | |
| item &it = target.weapon; | |
| // roll your melee and target's dodge skills to check if grab/smash attack succeeds | |
| int hitspread = target.deal_melee_attack( this, hit_roll() ); | |
| if( hitspread < 0 ) { | |
| add_msg( _( "You lunge for the %s, but miss!" ), it.tname() ); | |
| mod_moves( -100 - stumble( *this, weapon ) - attack_speed( weapon ) ); | |
| target.on_attacked( *this ); | |
| return; | |
| } | |
| // hitspread >= 0, which means we are going to disarm by grabbing target by their weapon | |
| if( !is_armed() ) { | |
| /** @EFFECT_UNARMED increases chance to disarm, bonus when nothing wielded */ | |
| my_roll += dice( 3, get_skill_level( skill_unarmed ) ); | |
| if( my_roll >= their_roll ) { | |
| add_msg( _( "You grab at %s and pull with all your force!" ), it.tname() ); | |
| add_msg( _( "You forcefully take %s from %s!" ), it.tname(), target.name ); | |
| // wield() will deduce our moves, consider to deduce more/less moves for balance | |
| item rem_it = target.i_rem( &it ); | |
| wield( rem_it ); | |
| } else if( my_roll >= their_roll / 2 ) { | |
| add_msg( _( "You grab at %s and pull with all your force, but it drops nearby!" ), | |
| it.tname() ); | |
| const tripoint tp = target.pos() + tripoint( rng( -1, 1 ), rng( -1, 1 ), 0 ); | |
| g->m.add_item_or_charges( tp, target.i_rem( &it ) ); | |
| mod_moves( -100 ); | |
| } else { | |
| add_msg( _( "You grab at %s and pull with all your force, but in vain!" ), it.tname() ); | |
| mod_moves( -100 ); | |
| } | |
| target.on_attacked( *this ); | |
| return; | |
| } | |
| // Make their weapon fall on floor if we've rolled enough. | |
| mod_moves( -100 - attack_speed( weapon ) ); | |
| if( my_roll >= their_roll ) { | |
| add_msg( _( "You smash %s with all your might forcing their %s to drop down nearby!" ), | |
| target.name, it.tname() ); | |
| const tripoint tp = target.pos() + tripoint( rng( -1, 1 ), rng( -1, 1 ), 0 ); | |
| g->m.add_item_or_charges( tp, target.i_rem( &it ) ); | |
| } else { | |
| add_msg( _( "You smash %s with all your might but %s remains in their hands!" ), | |
| target.name, it.tname() ); | |
| } | |
| target.on_attacked( *this ); | |
| } | |
| void avatar::steal( npc &target ) | |
| { | |
| if( target.is_enemy() ) { | |
| add_msg( _( "%s is hostile!" ), target.name ); | |
| return; | |
| } | |
| item_location loc = game_menus::inv::steal( *this, target ); | |
| if( !loc ) { | |
| return; | |
| } | |
| /** @EFFECT_DEX defines the chance to steal */ | |
| int my_roll = dice( 3, get_dex() ); | |
| /** @EFFECT_UNARMED adds bonus to stealing when wielding nothing */ | |
| if( !is_armed() ) { | |
| my_roll += dice( 4, 3 ); | |
| } | |
| if( has_trait( trait_DEFT ) ) { | |
| my_roll += dice( 2, 6 ); | |
| } | |
| if( has_trait( trait_CLUMSY ) ) { | |
| my_roll -= dice( 4, 6 ); | |
| } | |
| int their_roll = dice( 5, target.get_per() ); | |
| const item *it = loc.get_item(); | |
| if( my_roll >= their_roll ) { | |
| add_msg( _( "You sneakily steal %1$s from %2$s!" ), | |
| it->tname(), target.name ); | |
| i_add( target.i_rem( it ) ); | |
| } else if( my_roll >= their_roll / 2 ) { | |
| add_msg( _( "You failed to steal %1$s from %2$s, but did not attract attention." ), | |
| it->tname(), target.name ); | |
| } else { | |
| add_msg( _( "You failed to steal %1$s from %2$s." ), | |
| it->tname(), target.name ); | |
| target.on_attacked( *this ); | |
| } | |
| // consider to deduce less/more moves for balance | |
| mod_moves( -200 ); | |
| } | |
| namespace melee | |
| { | |
| /** | |
| * Once the accuracy (sum of modifiers) of an attack has been determined, | |
| * this is used to actually roll the "hit value" of the attack to be compared to dodge. | |
| */ | |
| float melee_hit_range( float accuracy ) | |
| { | |
| return normal_roll( accuracy * 5, 25.0f ); | |
| } | |
| } // namespace melee |