Permalink
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| #include "player.h" | |
| #include <cctype> | |
| #include <algorithm> | |
| #include <cmath> | |
| #include <cstdlib> | |
| #include <iterator> | |
| #include <map> | |
| #include <string> | |
| #include <sstream> | |
| #include <limits> | |
| #include <bitset> | |
| #include <exception> | |
| #include <tuple> | |
| #include "action.h" | |
| #include "activity_handlers.h" | |
| #include "addiction.h" | |
| #include "ammo.h" | |
| #include "avatar.h" | |
| #include "avatar_action.h" | |
| #include "bionics.h" | |
| #include "cata_utility.h" | |
| #include "catacharset.h" | |
| #include "coordinate_conversions.h" | |
| #include "craft_command.h" | |
| #include "cursesdef.h" | |
| #include "debug.h" | |
| #include "effect.h" | |
| #include "fault.h" | |
| #include "filesystem.h" | |
| #include "fungal_effects.h" | |
| #include "game.h" | |
| #include "gun_mode.h" | |
| #include "handle_liquid.h" | |
| #include "input.h" | |
| #include "inventory.h" | |
| #include "item.h" | |
| #include "item_location.h" | |
| #include "itype.h" | |
| #include "iuse_actor.h" | |
| #include "magic.h" | |
| #include "map.h" | |
| #include "map_iterator.h" | |
| #include "mapdata.h" | |
| #include "martialarts.h" | |
| #include "material.h" | |
| #include "messages.h" | |
| #include "morale.h" | |
| #include "morale_types.h" | |
| #include "mtype.h" | |
| #include "mutation.h" | |
| #include "name.h" | |
| #include "npc.h" | |
| #include "options.h" | |
| #include "output.h" | |
| #include "overlay_ordering.h" | |
| #include "overmapbuffer.h" | |
| #include "pickup.h" | |
| #include "profession.h" | |
| #include "ranged.h" | |
| #include "recipe_dictionary.h" | |
| #include "requirements.h" | |
| #include "skill.h" | |
| #include "sounds.h" | |
| #include "string_formatter.h" | |
| #include "submap.h" | |
| #include "text_snippets.h" | |
| #include "translations.h" | |
| #include "trap.h" | |
| #include "ui.h" | |
| #include "uistate.h" | |
| #include "veh_type.h" | |
| #include "vehicle.h" | |
| #include "vitamin.h" | |
| #include "vpart_position.h" | |
| #include "vpart_range.h" | |
| #include "weather.h" | |
| #include "weather_gen.h" | |
| #include "field.h" | |
| #include "fire.h" | |
| #include "int_id.h" | |
| #include "iuse.h" | |
| #include "lightmap.h" | |
| #include "line.h" | |
| #include "math_defines.h" | |
| #include "monster.h" | |
| #include "omdata.h" | |
| #include "overmap_types.h" | |
| #include "recipe.h" | |
| #include "rng.h" | |
| #include "units.h" | |
| #include "visitable.h" | |
| #include "string_id.h" | |
| #include "colony.h" | |
| #include "enums.h" | |
| #include "flat_set.h" | |
| #include "stomach.h" | |
| const double MAX_RECOIL = 3000; | |
| const mtype_id mon_player_blob( "mon_player_blob" ); | |
| const mtype_id mon_shadow_snake( "mon_shadow_snake" ); | |
| const skill_id skill_dodge( "dodge" ); | |
| const skill_id skill_gun( "gun" ); | |
| const skill_id skill_mechanics( "mechanics" ); | |
| const skill_id skill_swimming( "swimming" ); | |
| const skill_id skill_throw( "throw" ); | |
| const skill_id skill_unarmed( "unarmed" ); | |
| const efftype_id effect_adrenaline( "adrenaline" ); | |
| const efftype_id effect_alarm_clock( "alarm_clock" ); | |
| const efftype_id effect_asthma( "asthma" ); | |
| const efftype_id effect_attention( "attention" ); | |
| const efftype_id effect_bandaged( "bandaged" ); | |
| const efftype_id effect_bite( "bite" ); | |
| const efftype_id effect_blind( "blind" ); | |
| const efftype_id effect_blisters( "blisters" ); | |
| const efftype_id effect_bloodworms( "bloodworms" ); | |
| const efftype_id effect_boomered( "boomered" ); | |
| const efftype_id effect_brainworms( "brainworms" ); | |
| const efftype_id effect_cig( "cig" ); | |
| const efftype_id effect_cold( "cold" ); | |
| const efftype_id effect_common_cold( "common_cold" ); | |
| const efftype_id effect_contacts( "contacts" ); | |
| const efftype_id effect_corroding( "corroding" ); | |
| const efftype_id effect_cough_suppress( "cough_suppress" ); | |
| const efftype_id effect_darkness( "darkness" ); | |
| const efftype_id effect_datura( "datura" ); | |
| const efftype_id effect_deaf( "deaf" ); | |
| const efftype_id effect_depressants( "depressants" ); | |
| const efftype_id effect_dermatik( "dermatik" ); | |
| const efftype_id effect_disabled( "disabled" ); | |
| const efftype_id effect_disinfected( "disinfected" ); | |
| const efftype_id effect_downed( "downed" ); | |
| const efftype_id effect_drunk( "drunk" ); | |
| const efftype_id effect_earphones( "earphones" ); | |
| const efftype_id effect_evil( "evil" ); | |
| const efftype_id effect_flu( "flu" ); | |
| const efftype_id effect_foodpoison( "foodpoison" ); | |
| const efftype_id effect_formication( "formication" ); | |
| const efftype_id effect_frostbite( "frostbite" ); | |
| const efftype_id effect_frostbite_recovery( "frostbite_recovery" ); | |
| const efftype_id effect_fungus( "fungus" ); | |
| const efftype_id effect_glowing( "glowing" ); | |
| const efftype_id effect_glowy_led( "glowy_led" ); | |
| const efftype_id effect_grabbed( "grabbed" ); | |
| const efftype_id effect_hallu( "hallu" ); | |
| const efftype_id effect_happy( "happy" ); | |
| const efftype_id effect_hot( "hot" ); | |
| const efftype_id effect_infected( "infected" ); | |
| const efftype_id effect_iodine( "iodine" ); | |
| const efftype_id effect_irradiated( "irradiated" ); | |
| const efftype_id effect_jetinjector( "jetinjector" ); | |
| const efftype_id effect_lack_sleep( "lack_sleep" ); | |
| const efftype_id effect_sleep_deprived( "sleep_deprived" ); | |
| const efftype_id effect_lying_down( "lying_down" ); | |
| const efftype_id effect_mending( "mending" ); | |
| const efftype_id effect_meth( "meth" ); | |
| const efftype_id effect_narcosis( "narcosis" ); | |
| const efftype_id effect_nausea( "nausea" ); | |
| const efftype_id effect_no_sight( "no_sight" ); | |
| const efftype_id effect_onfire( "onfire" ); | |
| const efftype_id effect_paincysts( "paincysts" ); | |
| const efftype_id effect_pkill( "pkill" ); | |
| const efftype_id effect_pkill1( "pkill1" ); | |
| const efftype_id effect_pkill2( "pkill2" ); | |
| const efftype_id effect_pkill3( "pkill3" ); | |
| const efftype_id effect_recover( "recover" ); | |
| const efftype_id effect_riding( "riding" ); | |
| const efftype_id effect_sad( "sad" ); | |
| const efftype_id effect_shakes( "shakes" ); | |
| const efftype_id effect_sleep( "sleep" ); | |
| const efftype_id effect_slept_through_alarm( "slept_through_alarm" ); | |
| const efftype_id effect_spores( "spores" ); | |
| const efftype_id effect_stim( "stim" ); | |
| const efftype_id effect_stim_overdose( "stim_overdose" ); | |
| const efftype_id effect_stunned( "stunned" ); | |
| const efftype_id effect_tapeworm( "tapeworm" ); | |
| const efftype_id effect_took_prozac( "took_prozac" ); | |
| const efftype_id effect_took_xanax( "took_xanax" ); | |
| const efftype_id effect_visuals( "visuals" ); | |
| const efftype_id effect_weed_high( "weed_high" ); | |
| const efftype_id effect_winded( "winded" ); | |
| const efftype_id effect_bleed( "bleed" ); | |
| const efftype_id effect_magnesium_supplements( "magnesium" ); | |
| const efftype_id effect_ridden( "ridden" ); | |
| const matype_id style_none( "style_none" ); | |
| const matype_id style_kicks( "style_kicks" ); | |
| const species_id ROBOT( "ROBOT" ); | |
| static const bionic_id bio_ads( "bio_ads" ); | |
| static const bionic_id bio_advreactor( "bio_advreactor" ); | |
| static const bionic_id bio_armor_arms( "bio_armor_arms" ); | |
| static const bionic_id bio_armor_eyes( "bio_armor_eyes" ); | |
| static const bionic_id bio_armor_head( "bio_armor_head" ); | |
| static const bionic_id bio_armor_legs( "bio_armor_legs" ); | |
| static const bionic_id bio_armor_torso( "bio_armor_torso" ); | |
| static const bionic_id bio_blaster( "bio_blaster" ); | |
| static const bionic_id bio_carbon( "bio_carbon" ); | |
| static const bionic_id bio_climate( "bio_climate" ); | |
| static const bionic_id bio_cloak( "bio_cloak" ); | |
| static const bionic_id bio_cqb( "bio_cqb" ); | |
| static const bionic_id bio_dis_acid( "bio_dis_acid" ); | |
| static const bionic_id bio_dis_shock( "bio_dis_shock" ); | |
| static const bionic_id bio_drain( "bio_drain" ); | |
| static const bionic_id bio_earplugs( "bio_earplugs" ); | |
| static const bionic_id bio_ears( "bio_ears" ); | |
| static const bionic_id bio_eye_optic( "bio_eye_optic" ); | |
| static const bionic_id bio_faraday( "bio_faraday" ); | |
| static const bionic_id bio_flashlight( "bio_flashlight" ); | |
| static const bionic_id bio_tattoo_led( "bio_tattoo_led" ); | |
| static const bionic_id bio_glowy( "bio_glowy" ); | |
| static const bionic_id bio_geiger( "bio_geiger" ); | |
| static const bionic_id bio_gills( "bio_gills" ); | |
| static const bionic_id bio_ground_sonar( "bio_ground_sonar" ); | |
| static const bionic_id bio_heatsink( "bio_heatsink" ); | |
| static const bionic_id bio_itchy( "bio_itchy" ); | |
| static const bionic_id bio_jointservo( "bio_jointservo" ); | |
| static const bionic_id bio_laser( "bio_laser" ); | |
| static const bionic_id bio_leaky( "bio_leaky" ); | |
| static const bionic_id bio_lighter( "bio_lighter" ); | |
| static const bionic_id bio_membrane( "bio_membrane" ); | |
| static const bionic_id bio_memory( "bio_memory" ); | |
| static const bionic_id bio_metabolics( "bio_metabolics" ); | |
| static const bionic_id bio_noise( "bio_noise" ); | |
| static const bionic_id bio_plut_filter( "bio_plut_filter" ); | |
| static const bionic_id bio_power_weakness( "bio_power_weakness" ); | |
| static const bionic_id bio_purifier( "bio_purifier" ); | |
| static const bionic_id bio_reactor( "bio_reactor" ); | |
| static const bionic_id bio_recycler( "bio_recycler" ); | |
| static const bionic_id bio_shakes( "bio_shakes" ); | |
| static const bionic_id bio_sleepy( "bio_sleepy" ); | |
| static const bionic_id bn_bio_solar( "bn_bio_solar" ); | |
| static const bionic_id bio_soporific( "bio_soporific" ); | |
| static const bionic_id bio_spasm( "bio_spasm" ); | |
| static const bionic_id bio_speed( "bio_speed" ); | |
| static const bionic_id bio_syringe( "bio_syringe" ); | |
| static const bionic_id bio_tools( "bio_tools" ); | |
| static const bionic_id bio_trip( "bio_trip" ); | |
| static const bionic_id bio_uncanny_dodge( "bio_uncanny_dodge" ); | |
| static const bionic_id bio_ups( "bio_ups" ); | |
| static const bionic_id bio_watch( "bio_watch" ); | |
| static const bionic_id bio_synaptic_regen( "bio_synaptic_regen" ); | |
| // Aftershock stuff! | |
| static const bionic_id afs_bio_linguistic_coprocessor( "afs_bio_linguistic_coprocessor" ); | |
| static const trait_id trait_ACIDBLOOD( "ACIDBLOOD" ); | |
| static const trait_id trait_ACIDPROOF( "ACIDPROOF" ); | |
| static const trait_id trait_ADDICTIVE( "ADDICTIVE" ); | |
| static const trait_id trait_ADRENALINE( "ADRENALINE" ); | |
| static const trait_id trait_ALBINO( "ALBINO" ); | |
| static const trait_id trait_AMPHIBIAN( "AMPHIBIAN" ); | |
| static const trait_id trait_ANTENNAE( "ANTENNAE" ); | |
| static const trait_id trait_ANTLERS( "ANTLERS" ); | |
| static const trait_id trait_ASTHMA( "ASTHMA" ); | |
| static const trait_id trait_BADBACK( "BADBACK" ); | |
| static const trait_id trait_BARK( "BARK" ); | |
| static const trait_id trait_CANNIBAL( "CANNIBAL" ); | |
| static const trait_id trait_CENOBITE( "CENOBITE" ); | |
| static const trait_id trait_CEPH_EYES( "CEPH_EYES" ); | |
| static const trait_id trait_CF_HAIR( "CF_HAIR" ); | |
| static const trait_id trait_CHAOTIC( "CHAOTIC" ); | |
| static const trait_id trait_CHAOTIC_BAD( "CHAOTIC_BAD" ); | |
| static const trait_id trait_CHEMIMBALANCE( "CHEMIMBALANCE" ); | |
| static const trait_id trait_CHITIN_FUR( "CHITIN_FUR" ); | |
| static const trait_id trait_CHITIN_FUR2( "CHITIN_FUR2" ); | |
| static const trait_id trait_CHITIN_FUR3( "CHITIN_FUR3" ); | |
| static const trait_id trait_CHLOROMORPH( "CHLOROMORPH" ); | |
| static const trait_id trait_CLUMSY( "CLUMSY" ); | |
| static const trait_id trait_COLDBLOOD4( "COLDBLOOD4" ); | |
| static const trait_id trait_DEAF( "DEAF" ); | |
| static const trait_id trait_DEFT( "DEFT" ); | |
| static const trait_id trait_DEBUG_BIONIC_POWER( "DEBUG_BIONIC_POWER" ); | |
| static const trait_id trait_DEBUG_CLOAK( "DEBUG_CLOAK" ); | |
| static const trait_id trait_DEBUG_HS( "DEBUG_HS" ); | |
| static const trait_id trait_DEBUG_LS( "DEBUG_LS" ); | |
| static const trait_id trait_DEBUG_NODMG( "DEBUG_NODMG" ); | |
| static const trait_id trait_DEBUG_NOTEMP( "DEBUG_NOTEMP" ); | |
| static const trait_id trait_DISIMMUNE( "DISIMMUNE" ); | |
| static const trait_id trait_DISRESISTANT( "DISRESISTANT" ); | |
| static const trait_id trait_DOWN( "DOWN" ); | |
| static const trait_id trait_EASYSLEEPER( "EASYSLEEPER" ); | |
| static const trait_id trait_EASYSLEEPER2( "EASYSLEEPER2" ); | |
| static const trait_id trait_ELECTRORECEPTORS( "ELECTRORECEPTORS" ); | |
| static const trait_id trait_EATHEALTH( "EATHEALTH" ); | |
| static const trait_id trait_FASTHEALER( "FASTHEALER" ); | |
| static const trait_id trait_FASTHEALER2( "FASTHEALER2" ); | |
| static const trait_id trait_FASTLEARNER( "FASTLEARNER" ); | |
| static const trait_id trait_FASTREADER( "FASTREADER" ); | |
| static const trait_id trait_FAT( "FAT" ); | |
| static const trait_id trait_FELINE_FUR( "FELINE_FUR" ); | |
| static const trait_id trait_FLOWERS( "FLOWERS" ); | |
| static const trait_id trait_FRESHWATEROSMOSIS( "FRESHWATEROSMOSIS" ); | |
| static const trait_id trait_FUR( "FUR" ); | |
| static const trait_id trait_GILLS( "GILLS" ); | |
| static const trait_id trait_GILLS_CEPH( "GILLS_CEPH" ); | |
| static const trait_id trait_HATES_BOOKS( "HATES_BOOKS" ); | |
| static const trait_id trait_HEAVYSLEEPER( "HEAVYSLEEPER" ); | |
| static const trait_id trait_HEAVYSLEEPER2( "HEAVYSLEEPER2" ); | |
| static const trait_id trait_HOARDER( "HOARDER" ); | |
| static const trait_id trait_HOLLOW_BONES( "HOLLOW_BONES" ); | |
| static const trait_id trait_HOOVES( "HOOVES" ); | |
| static const trait_id trait_HORNS_POINTED( "HORNS_POINTED" ); | |
| static const trait_id trait_HUGE( "HUGE" ); | |
| static const trait_id trait_HUGE_OK( "HUGE_OK" ); | |
| static const trait_id trait_INFIMMUNE( "INFIMMUNE" ); | |
| static const trait_id trait_INSOMNIA( "INSOMNIA" ); | |
| static const trait_id trait_INT_SLIME( "INT_SLIME" ); | |
| static const trait_id trait_JITTERY( "JITTERY" ); | |
| static const trait_id trait_LARGE( "LARGE" ); | |
| static const trait_id trait_LARGE_OK( "LARGE_OK" ); | |
| static const trait_id trait_LEAVES( "LEAVES" ); | |
| static const trait_id trait_LEAVES2( "LEAVES2" ); | |
| static const trait_id trait_LEAVES3( "LEAVES3" ); | |
| static const trait_id trait_LEG_TENTACLES( "LEG_TENTACLES" ); | |
| static const trait_id trait_LEG_TENT_BRACE( "LEG_TENT_BRACE" ); | |
| static const trait_id trait_LIGHTFUR( "LIGHTFUR" ); | |
| static const trait_id trait_LIGHTSTEP( "LIGHTSTEP" ); | |
| static const trait_id trait_LIGHT_BONES( "LIGHT_BONES" ); | |
| static const trait_id trait_LOVES_BOOKS( "LOVES_BOOKS" ); | |
| static const trait_id trait_LUPINE_FUR( "LUPINE_FUR" ); | |
| static const trait_id trait_MEMBRANE( "MEMBRANE" ); | |
| static const trait_id trait_MOODSWINGS( "MOODSWINGS" ); | |
| static const trait_id trait_MOREPAIN( "MORE_PAIN" ); | |
| static const trait_id trait_MOREPAIN2( "MORE_PAIN2" ); | |
| static const trait_id trait_MOREPAIN3( "MORE_PAIN3" ); | |
| static const trait_id trait_MYOPIC( "MYOPIC" ); | |
| static const trait_id trait_M_BLOSSOMS( "M_BLOSSOMS" ); | |
| static const trait_id trait_M_DEPENDENT( "M_DEPENDENT" ); | |
| static const trait_id trait_M_IMMUNE( "M_IMMUNE" ); | |
| static const trait_id trait_M_SKIN2( "M_SKIN2" ); | |
| static const trait_id trait_M_SKIN3( "M_SKIN3" ); | |
| static const trait_id trait_M_SPORES( "M_SPORES" ); | |
| static const trait_id trait_NARCOLEPTIC( "NARCOLEPTIC" ); | |
| static const trait_id trait_NAUSEA( "NAUSEA" ); | |
| static const trait_id trait_NOMAD( "NOMAD" ); | |
| static const trait_id trait_NOMAD2( "NOMAD2" ); | |
| static const trait_id trait_NOMAD3( "NOMAD3" ); | |
| static const trait_id trait_NONADDICTIVE( "NONADDICTIVE" ); | |
| static const trait_id trait_NOPAIN( "NOPAIN" ); | |
| static const trait_id trait_NO_THIRST( "NO_THIRST" ); | |
| static const trait_id trait_PACIFIST( "PACIFIST" ); | |
| static const trait_id trait_PADDED_FEET( "PADDED_FEET" ); | |
| static const trait_id trait_PAINREC1( "PAINREC1" ); | |
| static const trait_id trait_PAINREC2( "PAINREC2" ); | |
| static const trait_id trait_PAINREC3( "PAINREC3" ); | |
| static const trait_id trait_PAINRESIST( "PAINRESIST" ); | |
| static const trait_id trait_PAINRESIST_TROGLO( "PAINRESIST_TROGLO" ); | |
| static const trait_id trait_PARAIMMUNE( "PARAIMMUNE" ); | |
| static const trait_id trait_PARKOUR( "PARKOUR" ); | |
| static const trait_id trait_PAWS( "PAWS" ); | |
| static const trait_id trait_PAWS_LARGE( "PAWS_LARGE" ); | |
| static const trait_id trait_PER_SLIME( "PER_SLIME" ); | |
| static const trait_id trait_PER_SLIME_OK( "PER_SLIME_OK" ); | |
| static const trait_id trait_PRED2( "PRED2" ); | |
| static const trait_id trait_PRED3( "PRED3" ); | |
| static const trait_id trait_PRED4( "PRED4" ); | |
| static const trait_id trait_PROF_DICEMASTER( "PROF_DICEMASTER" ); | |
| static const trait_id trait_PROF_FOODP( "PROF_FOODP" ); | |
| static const trait_id trait_PROF_SKATER( "PROF_SKATER" ); | |
| static const trait_id trait_PSYCHOPATH( "PSYCHOPATH" ); | |
| static const trait_id trait_PYROMANIA( "PYROMANIA" ); | |
| static const trait_id trait_KILLER( "KILLER" ); | |
| static const trait_id trait_QUILLS( "QUILLS" ); | |
| static const trait_id trait_RADIOACTIVE1( "RADIOACTIVE1" ); | |
| static const trait_id trait_RADIOACTIVE2( "RADIOACTIVE2" ); | |
| static const trait_id trait_RADIOACTIVE3( "RADIOACTIVE3" ); | |
| static const trait_id trait_RADIOGENIC( "RADIOGENIC" ); | |
| static const trait_id trait_REGEN( "REGEN" ); | |
| static const trait_id trait_REGEN_LIZ( "REGEN_LIZ" ); | |
| static const trait_id trait_ROOTS2( "ROOTS2" ); | |
| static const trait_id trait_ROOTS3( "ROOTS3" ); | |
| static const trait_id trait_SAPIOVORE( "SAPIOVORE" ); | |
| static const trait_id trait_SAVANT( "SAVANT" ); | |
| static const trait_id trait_SCHIZOPHRENIC( "SCHIZOPHRENIC" ); | |
| static const trait_id trait_SEESLEEP( "SEESLEEP" ); | |
| static const trait_id trait_SELFAWARE( "SELFAWARE" ); | |
| static const trait_id trait_SHARKTEETH( "SHARKTEETH" ); | |
| static const trait_id trait_SHELL2( "SHELL2" ); | |
| static const trait_id trait_SHOUT1( "SHOUT1" ); | |
| static const trait_id trait_SHOUT2( "SHOUT2" ); | |
| static const trait_id trait_SHOUT3( "SHOUT3" ); | |
| static const trait_id trait_SLEEK_SCALES( "SLEEK_SCALES" ); | |
| static const trait_id trait_SLIMESPAWNER( "SLIMESPAWNER" ); | |
| static const trait_id trait_SLIMY( "SLIMY" ); | |
| static const trait_id trait_SLOWHEALER( "SLOWHEALER" ); | |
| static const trait_id trait_SLOWLEARNER( "SLOWLEARNER" ); | |
| static const trait_id trait_SLOWREADER( "SLOWREADER" ); | |
| static const trait_id trait_SMELLY( "SMELLY" ); | |
| static const trait_id trait_SMELLY2( "SMELLY2" ); | |
| static const trait_id trait_SORES( "SORES" ); | |
| static const trait_id trait_SPINES( "SPINES" ); | |
| static const trait_id trait_SPIRITUAL( "SPIRITUAL" ); | |
| static const trait_id trait_SQUEAMISH( "SQUEAMISH" ); | |
| static const trait_id trait_STRONGSTOMACH( "STRONGSTOMACH" ); | |
| static const trait_id trait_SUNBURN( "SUNBURN" ); | |
| static const trait_id trait_SUNLIGHT_DEPENDENT( "SUNLIGHT_DEPENDENT" ); | |
| static const trait_id trait_TAIL_FIN( "TAIL_FIN" ); | |
| static const trait_id trait_THORNS( "THORNS" ); | |
| static const trait_id trait_THRESH_SPIDER( "THRESH_SPIDER" ); | |
| static const trait_id trait_TOUGH_FEET( "TOUGH_FEET" ); | |
| static const trait_id trait_TRANSPIRATION( "TRANSPIRATION" ); | |
| static const trait_id trait_TROGLO( "TROGLO" ); | |
| static const trait_id trait_TROGLO2( "TROGLO2" ); | |
| static const trait_id trait_TROGLO3( "TROGLO3" ); | |
| static const trait_id trait_UNSTABLE( "UNSTABLE" ); | |
| static const trait_id trait_URSINE_EYE( "URSINE_EYE" ); | |
| static const trait_id trait_URSINE_FUR( "URSINE_FUR" ); | |
| static const trait_id trait_VISCOUS( "VISCOUS" ); | |
| static const trait_id trait_VOMITOUS( "VOMITOUS" ); | |
| static const trait_id trait_WATERSLEEP( "WATERSLEEP" ); | |
| static const trait_id trait_WEAKSCENT( "WEAKSCENT" ); | |
| static const trait_id trait_WEAKSTOMACH( "WEAKSTOMACH" ); | |
| static const trait_id trait_WEBBED( "WEBBED" ); | |
| static const trait_id trait_WEB_SPINNER( "WEB_SPINNER" ); | |
| static const trait_id trait_WEB_WALKER( "WEB_WALKER" ); | |
| static const trait_id trait_WEB_WEAVER( "WEB_WEAVER" ); | |
| static const trait_id trait_WOOLALLERGY( "WOOLALLERGY" ); | |
| stat_mod player::get_pain_penalty() const | |
| { | |
| stat_mod ret; | |
| int pain = get_perceived_pain(); | |
| if( pain <= 0 ) { | |
| return ret; | |
| } | |
| int stat_penalty = std::floor( std::pow( pain, 0.8f ) / 10.0f ); | |
| bool ceno = has_trait( trait_CENOBITE ); | |
| if( !ceno ) { | |
| ret.strength = stat_penalty; | |
| ret.dexterity = stat_penalty; | |
| } | |
| if( !has_trait( trait_INT_SLIME ) ) { | |
| ret.intelligence = 1 + stat_penalty; | |
| } else { | |
| ret.intelligence = 1 + pain / 5; | |
| } | |
| ret.perception = stat_penalty * 2 / 3; | |
| ret.speed = std::pow( pain, 0.7f ); | |
| if( ceno ) { | |
| ret.speed /= 2; | |
| } | |
| ret.speed = std::min( ret.speed, 50 ); | |
| return ret; | |
| } | |
| player::player() : Character() | |
| , next_climate_control_check( calendar::before_time_starts ) | |
| , cached_time( calendar::before_time_starts ) | |
| { | |
| id = -1; // -1 is invalid | |
| str_cur = 8; | |
| str_max = 8; | |
| dex_cur = 8; | |
| dex_max = 8; | |
| int_cur = 8; | |
| int_max = 8; | |
| per_cur = 8; | |
| per_max = 8; | |
| dodges_left = 1; | |
| blocks_left = 1; | |
| power_level = 0; | |
| max_power_level = 0; | |
| stamina = 10000; //Temporary value for stamina. It will be reset later from external json option. | |
| stim = 0; | |
| pkill = 0; | |
| radiation = 0; | |
| tank_plut = 0; | |
| reactor_plut = 0; | |
| slow_rad = 0; | |
| cash = 0; | |
| scent = 500; | |
| male = true; | |
| prof = profession::has_initialized() ? profession::generic() : | |
| nullptr; //workaround for a potential structural limitation, see player::create | |
| start_location = start_location_id( "shelter" ); | |
| moves = 100; | |
| movecounter = 0; | |
| oxygen = 0; | |
| last_climate_control_ret = false; | |
| in_vehicle = false; | |
| controlling_vehicle = false; | |
| grab_point = tripoint_zero; | |
| hauling = false; | |
| move_mode = PMM_WALK; | |
| style_selected = style_none; | |
| keep_hands_free = false; | |
| focus_pool = 100; | |
| last_item = itype_id( "null" ); | |
| sight_max = 9999; | |
| last_batch = 0; | |
| lastconsumed = itype_id( "null" ); | |
| next_expected_position = cata::nullopt; | |
| death_drops = true; | |
| empty_traits(); | |
| temp_cur.fill( BODYTEMP_NORM ); | |
| frostbite_timer.fill( 0 ); | |
| temp_conv.fill( BODYTEMP_NORM ); | |
| body_wetness.fill( 0 ); | |
| nv_cached = false; | |
| volume = 0; | |
| for( const auto &v : vitamin::all() ) { | |
| vitamin_levels[ v.first ] = 0; | |
| } | |
| memorial_log.clear(); | |
| drench_capacity[bp_eyes] = 1; | |
| drench_capacity[bp_mouth] = 1; | |
| drench_capacity[bp_head] = 7; | |
| drench_capacity[bp_leg_l] = 11; | |
| drench_capacity[bp_leg_r] = 11; | |
| drench_capacity[bp_foot_l] = 3; | |
| drench_capacity[bp_foot_r] = 3; | |
| drench_capacity[bp_arm_l] = 10; | |
| drench_capacity[bp_arm_r] = 10; | |
| drench_capacity[bp_hand_l] = 3; | |
| drench_capacity[bp_hand_r] = 3; | |
| drench_capacity[bp_torso] = 40; | |
| recalc_sight_limits(); | |
| reset_encumbrance(); | |
| ma_styles = {{ | |
| style_none, style_kicks | |
| } | |
| }; | |
| } | |
| player::~player() = default; | |
| player::player( player && ) = default; | |
| player &player::operator=( player && ) = default; | |
| void player::normalize() | |
| { | |
| Character::normalize(); | |
| style_selected = style_none; | |
| recalc_hp(); | |
| temp_conv.fill( BODYTEMP_NORM ); | |
| stamina = get_stamina_max(); | |
| } | |
| void player::process_turn() | |
| { | |
| // Has to happen before reset_stats | |
| clear_miss_reasons(); | |
| Character::process_turn(); | |
| // If we're actively handling something we can't just drop it on the ground | |
| // in the middle of handling it | |
| if( activity.targets.empty() ) { | |
| drop_invalid_inventory(); | |
| } | |
| // Didn't just pick something up | |
| last_item = itype_id( "null" ); | |
| if( has_active_bionic( bio_metabolics ) && power_level < max_power_level && | |
| 0.8f < get_kcal_percent() && calendar::once_every( 3_turns ) ) { | |
| // Efficiency is approximately 25%, power output is ~60W | |
| mod_stored_kcal( -1 ); | |
| charge_power( 1 ); | |
| } | |
| if( has_trait( trait_DEBUG_BIONIC_POWER ) ) { | |
| charge_power( max_power_level ); | |
| } | |
| visit_items( [this]( item * e ) { | |
| e->process_artifact( this, pos() ); | |
| return VisitResponse::NEXT; | |
| } ); | |
| suffer(); | |
| // Set our scent towards the norm | |
| int norm_scent = 500; | |
| if( has_trait( trait_WEAKSCENT ) ) { | |
| norm_scent = 300; | |
| } | |
| if( has_trait( trait_SMELLY ) ) { | |
| norm_scent = 800; | |
| } | |
| if( has_trait( trait_SMELLY2 ) ) { | |
| norm_scent = 1200; | |
| } | |
| // Not so much that you don't have a scent | |
| // but that you smell like a plant, rather than | |
| // a human. When was the last time you saw a critter | |
| // attack a bluebell or an apple tree? | |
| if( ( has_trait( trait_FLOWERS ) ) && ( !( has_trait( trait_CHLOROMORPH ) ) ) ) { | |
| norm_scent -= 200; | |
| } | |
| // You *are* a plant. Unless someone hunts triffids by scent, | |
| // you don't smell like prey. | |
| if( has_trait( trait_CHLOROMORPH ) ) { | |
| norm_scent = 0; | |
| } | |
| // Scent increases fast at first, and slows down as it approaches normal levels. | |
| // Estimate it will take about norm_scent * 2 turns to go from 0 - norm_scent / 2 | |
| // Without smelly trait this is about 1.5 hrs. Slows down significantly after that. | |
| if( scent < rng( 0, norm_scent ) ) { | |
| scent++; | |
| } | |
| // Unusually high scent decreases steadily until it reaches normal levels. | |
| if( scent > norm_scent ) { | |
| scent--; | |
| } | |
| // We can dodge again! Assuming we can actually move... | |
| if( !in_sleep_state() ) { | |
| blocks_left = get_num_blocks(); | |
| dodges_left = get_num_dodges(); | |
| } else { | |
| blocks_left = 0; | |
| dodges_left = 0; | |
| } | |
| // auto-learning. This is here because skill-increases happens all over the place: | |
| // SkillLevel::readBook (has no connection to the skill or the player), | |
| // player::read, player::practice, ... | |
| // Check for spontaneous discovery of martial art styles | |
| for( auto &style : autolearn_martialart_types() ) { | |
| const matype_id ma( style ); | |
| if( !has_martialart( ma ) && can_autolearn( ma ) ) { | |
| add_martialart( ma ); | |
| add_msg_if_player( m_info, _( "You have learned a new style: %s!" ), ma.obj().name ); | |
| } | |
| } | |
| // Update time spent conscious in this overmap tile for the Nomad traits. | |
| if( ( has_trait( trait_NOMAD ) || has_trait( trait_NOMAD2 ) || has_trait( trait_NOMAD3 ) ) && | |
| !has_effect( effect_sleep ) && !has_effect( effect_narcosis ) ) { | |
| const tripoint ompos = global_omt_location(); | |
| const point pos = point( ompos.x, ompos.y ); | |
| if( overmap_time.find( pos ) == overmap_time.end() ) { | |
| overmap_time[pos] = 1_turns; | |
| } else { | |
| overmap_time[pos] += 1_turns; | |
| } | |
| } | |
| // Decay time spent in other overmap tiles. | |
| if( calendar::once_every( 1_hours ) ) { | |
| const tripoint ompos = global_omt_location(); | |
| const time_point now = calendar::turn; | |
| time_duration decay_time = 0_days; | |
| if( has_trait( trait_NOMAD ) ) { | |
| decay_time = 7_days; | |
| } else if( has_trait( trait_NOMAD2 ) ) { | |
| decay_time = 14_days; | |
| } else if( has_trait( trait_NOMAD3 ) ) { | |
| decay_time = 28_days; | |
| } | |
| auto it = overmap_time.begin(); | |
| while( it != overmap_time.end() ) { | |
| if( it->first.x == ompos.x && it->first.y == ompos.y ) { | |
| it++; | |
| continue; | |
| } | |
| // Find the amount of time passed since the player touched any of the overmap tile's submaps. | |
| const tripoint tpt = tripoint( it->first.x, it->first.y, 0 ); | |
| const time_point last_touched = overmap_buffer.scent_at( tpt ).creation_time; | |
| const time_duration since_visit = now - last_touched; | |
| // If the player has spent little time in this overmap tile, let it decay after just an hour instead of the usual extended decay time. | |
| const time_duration modified_decay_time = it->second > 5_minutes ? decay_time : 1_hours; | |
| if( since_visit > modified_decay_time ) { | |
| // Reduce the tracked time spent in this overmap tile. | |
| const time_duration decay_amount = std::min( since_visit - modified_decay_time, 1_hours ); | |
| const time_duration updated_value = it->second - decay_amount; | |
| if( updated_value <= 0_turns ) { | |
| // We can stop tracking this tile if there's no longer any time recorded there. | |
| it = overmap_time.erase( it ); | |
| continue; | |
| } else { | |
| it->second = updated_value; | |
| } | |
| } | |
| it++; | |
| } | |
| } | |
| } | |
| void player::action_taken() | |
| { | |
| nv_cached = false; | |
| } | |
| void player::update_morale() | |
| { | |
| morale->decay( 1_minutes ); | |
| apply_persistent_morale(); | |
| } | |
| void player::apply_persistent_morale() | |
| { | |
| // Hoarders get a morale penalty if they're not carrying a full inventory. | |
| if( has_trait( trait_HOARDER ) ) { | |
| int pen = ( volume_capacity() - volume_carried() ) / 125_ml; | |
| if( pen > 70 ) { | |
| pen = 70; | |
| } | |
| if( pen <= 0 ) { | |
| pen = 0; | |
| } | |
| if( has_effect( effect_took_xanax ) ) { | |
| pen = pen / 7; | |
| } else if( has_effect( effect_took_prozac ) ) { | |
| pen = pen / 2; | |
| } | |
| if( pen > 0 ) { | |
| add_morale( MORALE_PERM_HOARDER, -pen, -pen, 1_minutes, 1_minutes, true ); | |
| } | |
| } | |
| // Nomads get a morale penalty if they stay near the same overmap tiles too long. | |
| if( has_trait( trait_NOMAD ) || has_trait( trait_NOMAD2 ) || has_trait( trait_NOMAD3 ) ) { | |
| const tripoint ompos = global_omt_location(); | |
| float total_time = 0; | |
| // Check how long we've stayed in any overmap tile within 5 of us. | |
| const int max_dist = 5; | |
| for( int dx = -max_dist; dx <= max_dist; dx++ ) { | |
| for( int dy = -max_dist; dy <= max_dist; dy++ ) { | |
| const float dist = rl_dist( 0, 0, dx, dy ); | |
| if( dist > max_dist ) { | |
| continue; | |
| } | |
| const point pos = point( ompos.x + dx, ompos.y + dy ); | |
| if( overmap_time.find( pos ) == overmap_time.end() ) { | |
| continue; | |
| } | |
| // Count time in own tile fully, tiles one away as 4/5, tiles two away as 3/5, etc. | |
| total_time += to_moves<float>( overmap_time[pos] ) * ( max_dist - dist ) / max_dist; | |
| } | |
| } | |
| // Characters with higher tiers of Nomad suffer worse morale penalties, faster. | |
| int max_unhappiness; | |
| float min_time, max_time; | |
| if( has_trait( trait_NOMAD ) ) { | |
| max_unhappiness = 20; | |
| min_time = to_moves<float>( 12_hours ); | |
| max_time = to_moves<float>( 1_days ); | |
| } else if( has_trait( trait_NOMAD2 ) ) { | |
| max_unhappiness = 40; | |
| min_time = to_moves<float>( 4_hours ); | |
| max_time = to_moves<float>( 8_hours ); | |
| } else { // traid_NOMAD3 | |
| max_unhappiness = 60; | |
| min_time = to_moves<float>( 1_hours ); | |
| max_time = to_moves<float>( 2_hours ); | |
| } | |
| // The penalty starts at 1 at min_time and scales up to max_unhappiness at max_time. | |
| const float t = ( total_time - min_time ) / ( max_time - min_time ); | |
| const int pen = ceil( lerp_clamped( 0, max_unhappiness, t ) ); | |
| if( pen > 0 ) { | |
| add_morale( MORALE_PERM_NOMAD, -pen, -pen, 1_minutes, 1_minutes, true ); | |
| } | |
| } | |
| if( has_trait( trait_PROF_FOODP ) ) { | |
| // Loosing your face is distressing | |
| if( !( is_wearing( itype_id( "foodperson_mask" ) ) || | |
| is_wearing( itype_id( "foodperson_mask_on" ) ) ) ) { | |
| add_morale( MORALE_PERM_NOFACE, -20, -20, 1_minutes, 1_minutes, true ); | |
| } else if( is_wearing( itype_id( "foodperson_mask" ) ) || | |
| is_wearing( itype_id( "foodperson_mask_on" ) ) ) { | |
| rem_morale( MORALE_PERM_NOFACE ); | |
| } | |
| if( is_wearing( itype_id( "foodperson_mask_on" ) ) ) { | |
| add_morale( MORALE_PERM_FPMODE_ON, 10, 10, 1_minutes, 1_minutes, true ); | |
| } else { | |
| rem_morale( MORALE_PERM_FPMODE_ON ); | |
| } | |
| } | |
| } | |
| /* Here lies the intended effects of body temperature | |
| Assumption 1 : a naked person is comfortable at 19C/66.2F (31C/87.8F at rest). | |
| Assumption 2 : a "lightly clothed" person is comfortable at 13C/55.4F (25C/77F at rest). | |
| Assumption 3 : the player is always running, thus generating more heat. | |
| Assumption 4 : frostbite cannot happen above 0C temperature.* | |
| * In the current model, a naked person can get frostbite at 1C. This isn't true, but it's a compromise with using nice whole numbers. | |
| Here is a list of warmth values and the corresponding temperatures in which the player is comfortable, and in which the player is very cold. | |
| Warmth Temperature (Comfortable) Temperature (Very cold) Notes | |
| 0 19C / 66.2F -11C / 12.2F * Naked | |
| 10 13C / 55.4F -17C / 1.4F * Lightly clothed | |
| 20 7C / 44.6F -23C / -9.4F | |
| 30 1C / 33.8F -29C / -20.2F | |
| 40 -5C / 23.0F -35C / -31.0F | |
| 50 -11C / 12.2F -41C / -41.8F | |
| 60 -17C / 1.4F -47C / -52.6F | |
| 70 -23C / -9.4F -53C / -63.4F | |
| 80 -29C / -20.2F -59C / -74.2F | |
| 90 -35C / -31.0F -65C / -85.0F | |
| 100 -41C / -41.8F -71C / -95.8F | |
| WIND POWER | |
| Except for the last entry, pressures are sort of made up... | |
| Breeze : 5mph (1015 hPa) | |
| Strong Breeze : 20 mph (1000 hPa) | |
| Moderate Gale : 30 mph (990 hPa) | |
| Storm : 50 mph (970 hPa) | |
| Hurricane : 100 mph (920 hPa) | |
| HURRICANE : 185 mph (880 hPa) [Ref: Hurricane Wilma] | |
| */ | |
| void player::update_bodytemp() | |
| { | |
| if( has_trait( trait_DEBUG_NOTEMP ) ) { | |
| temp_cur.fill( BODYTEMP_NORM ); | |
| temp_conv.fill( BODYTEMP_NORM ); | |
| return; | |
| } | |
| /* Cache calls to g->get_temperature( player position ), used in several places in function */ | |
| const auto player_local_temp = g->weather.get_temperature( pos() ); | |
| // NOTE : visit weather.h for some details on the numbers used | |
| // Converts temperature to Celsius/10 | |
| int Ctemperature = static_cast<int>( 100 * temp_to_celsius( player_local_temp ) ); | |
| const w_point weather = *g->weather.weather_precise; | |
| int vehwindspeed = 0; | |
| if( const optional_vpart_position vp = g->m.veh_at( pos() ) ) { | |
| vehwindspeed = abs( vp->vehicle().velocity / 100 ); // vehicle velocity in mph | |
| } | |
| const oter_id &cur_om_ter = overmap_buffer.ter( global_omt_location() ); | |
| bool sheltered = g->is_sheltered( pos() ); | |
| double total_windpower = get_local_windpower( g->weather.windspeed + vehwindspeed, cur_om_ter, | |
| pos(), | |
| g->weather.winddirection, sheltered ); | |
| // Let's cache this not to check it num_bp times | |
| const bool has_bark = has_trait( trait_BARK ); | |
| const bool has_sleep = has_effect( effect_sleep ); | |
| const bool has_sleep_state = has_sleep || in_sleep_state(); | |
| const bool has_heatsink = has_bionic( bio_heatsink ) || is_wearing( "rm13_armor_on" ) || | |
| has_trait( trait_M_SKIN2 ) || has_trait( trait_M_SKIN3 ); | |
| const bool has_common_cold = has_effect( effect_common_cold ); | |
| const bool has_climate_control = in_climate_control(); | |
| const bool use_floor_warmth = can_use_floor_warmth(); | |
| const furn_id furn_at_pos = g->m.furn( pos() ); | |
| // Temperature norms | |
| // Ambient normal temperature is lower while asleep | |
| const int ambient_norm = has_sleep ? 3100 : 1900; | |
| /** | |
| * Calculations that affect all body parts equally go here, not in the loop | |
| */ | |
| // Hunger / Starvation | |
| // -1000 when about to starve to death | |
| // -1333 when starving with light eater | |
| // -2000 if you managed to get 0 metabolism rate somehow | |
| const float met_rate = metabolic_rate(); | |
| const int hunger_warmth = static_cast<int>( 2000 * std::min( met_rate, 1.0f ) - 2000 ); | |
| // Give SOME bonus to those living furnaces with extreme metabolism | |
| const int metabolism_warmth = static_cast<int>( std::max( 0.0f, met_rate - 1.0f ) * 1000 ); | |
| // Fatigue | |
| // ~-900 when exhausted | |
| const int fatigue_warmth = has_sleep ? 0 : static_cast<int>( std::min( 0.0f, | |
| -1.5f * get_fatigue() ) ); | |
| // Sunlight | |
| const int sunlight_warmth = g->is_in_sunlight( pos() ) ? ( g->weather.weather == WEATHER_SUNNY ? | |
| 1000 : | |
| 500 ) : 0; | |
| const int best_fire = get_heat_radiation( pos(), true ); | |
| const int lying_warmth = use_floor_warmth ? floor_warmth( pos() ) : 0; | |
| const int water_temperature = | |
| 100 * temp_to_celsius( g->weather.get_cur_weather_gen().get_water_temperature() ); | |
| // Correction of body temperature due to traits and mutations | |
| // Lower heat is applied always | |
| const int mutation_heat_low = bodytemp_modifier_traits( false ); | |
| const int mutation_heat_high = bodytemp_modifier_traits( true ); | |
| // Difference between high and low is the "safe" heat - one we only apply if it's beneficial | |
| const int mutation_heat_bonus = mutation_heat_high - mutation_heat_low; | |
| const int h_radiation = get_heat_radiation( pos(), false ); | |
| // Current temperature and converging temperature calculations | |
| for( const body_part bp : all_body_parts ) { | |
| // Skip eyes | |
| if( bp == bp_eyes ) { | |
| continue; | |
| } | |
| // This adjusts the temperature scale to match the bodytemp scale, | |
| // it needs to be reset every iteration | |
| int adjusted_temp = ( Ctemperature - ambient_norm ); | |
| int bp_windpower = total_windpower; | |
| // Represents the fact that the body generates heat when it is cold. | |
| // TODO: : should this increase hunger? | |
| double scaled_temperature = logarithmic_range( BODYTEMP_VERY_COLD, BODYTEMP_VERY_HOT, | |
| temp_cur[bp] ); | |
| // Produces a smooth curve between 30.0 and 60.0. | |
| double homeostasis_adjustement = 30.0 * ( 1.0 + scaled_temperature ); | |
| int clothing_warmth_adjustement = static_cast<int>( homeostasis_adjustement * warmth( bp ) ); | |
| int clothing_warmth_adjusted_bonus = static_cast<int>( homeostasis_adjustement * bonus_item_warmth( | |
| bp ) ); | |
| // WINDCHILL | |
| bp_windpower = static_cast<int>( static_cast<float>( bp_windpower ) * ( 1 - get_wind_resistance( | |
| bp ) / 100.0 ) ); | |
| // Calculate windchill | |
| int windchill = get_local_windchill( player_local_temp, | |
| get_local_humidity( weather.humidity, g->weather.weather, | |
| sheltered ), | |
| bp_windpower ); | |
| // If you're standing in water, air temperature is replaced by water temperature. No wind. | |
| const ter_id ter_at_pos = g->m.ter( pos() ); | |
| // Convert to 0.01C | |
| if( ( ter_at_pos == t_water_dp || ter_at_pos == t_water_pool || ter_at_pos == t_swater_dp || | |
| ter_at_pos == t_water_moving_dp ) || | |
| ( ( ter_at_pos == t_water_sh || ter_at_pos == t_swater_sh || ter_at_pos == t_sewage || | |
| ter_at_pos == t_water_moving_sh ) && | |
| ( bp == bp_foot_l || bp == bp_foot_r || bp == bp_leg_l || bp == bp_leg_r ) ) ) { | |
| adjusted_temp += water_temperature - Ctemperature; // Swap out air temp for water temp. | |
| windchill = 0; | |
| } | |
| // Convergent temperature is affected by ambient temperature, | |
| // clothing warmth, and body wetness. | |
| temp_conv[bp] = BODYTEMP_NORM + adjusted_temp + windchill * 100 + clothing_warmth_adjustement; | |
| // HUNGER / STARVATION | |
| temp_conv[bp] += hunger_warmth; | |
| // FATIGUE | |
| temp_conv[bp] += fatigue_warmth; | |
| // Mutations | |
| temp_conv[bp] += mutation_heat_low; | |
| // DIRECT HEAT SOURCES (generates body heat, helps fight frostbite) | |
| // Bark : lowers blister count to -5; harder to get blisters | |
| int blister_count = ( has_bark ? -5 : 0 ); // If the counter is high, your skin starts to burn | |
| if( frostbite_timer[bp] > 0 ) { | |
| frostbite_timer[bp] -= std::max( 5, h_radiation ); | |
| } | |
| // 111F (44C) is a temperature in which proteins break down: https://en.wikipedia.org/wiki/Burn | |
| blister_count += h_radiation - 111 > 0 ? std::max( static_cast<int>( sqrt( h_radiation - 111 ) ), | |
| 0 ) : 0; | |
| const bool pyromania = has_trait( trait_PYROMANIA ); | |
| // BLISTERS : Skin gets blisters from intense heat exposure. | |
| // Fire protection protects from blisters. | |
| // Heatsinks give near-immunity. | |
| if( blister_count - get_armor_fire( bp ) - ( has_heatsink ? 20 : 0 ) > 0 ) { | |
| add_effect( effect_blisters, 1_turns, bp ); | |
| if( pyromania ) { | |
| add_morale( MORALE_PYROMANIA_NEARFIRE, 10, 10, 1_hours, | |
| 30_minutes ); // Proximity that's close enough to harm us gives us a bit of a thrill | |
| rem_morale( MORALE_PYROMANIA_NOFIRE ); | |
| } | |
| } else if( pyromania && best_fire >= 1 ) { // Only give us fire bonus if there's actually fire | |
| add_morale( MORALE_PYROMANIA_NEARFIRE, 5, 5, 30_minutes, | |
| 15_minutes ); // Gain a much smaller mood boost even if it doesn't hurt us | |
| rem_morale( MORALE_PYROMANIA_NOFIRE ); | |
| } | |
| temp_conv[bp] += sunlight_warmth; | |
| // DISEASES | |
| if( bp == bp_head && has_effect( effect_flu ) ) { | |
| temp_conv[bp] += 1500; | |
| } | |
| if( has_common_cold ) { | |
| temp_conv[bp] -= 750; | |
| } | |
| // Loss of blood results in loss of body heat, 1% bodyheat lost per 2% hp lost | |
| temp_conv[bp] -= blood_loss( bp ) * temp_conv[bp] / 200; | |
| // EQUALIZATION | |
| switch( bp ) { | |
| case bp_torso: | |
| temp_equalizer( bp_torso, bp_arm_l ); | |
| temp_equalizer( bp_torso, bp_arm_r ); | |
| temp_equalizer( bp_torso, bp_leg_l ); | |
| temp_equalizer( bp_torso, bp_leg_r ); | |
| temp_equalizer( bp_torso, bp_head ); | |
| break; | |
| case bp_head: | |
| temp_equalizer( bp_head, bp_torso ); | |
| temp_equalizer( bp_head, bp_mouth ); | |
| break; | |
| case bp_arm_l: | |
| temp_equalizer( bp_arm_l, bp_torso ); | |
| temp_equalizer( bp_arm_l, bp_hand_l ); | |
| break; | |
| case bp_arm_r: | |
| temp_equalizer( bp_arm_r, bp_torso ); | |
| temp_equalizer( bp_arm_r, bp_hand_r ); | |
| break; | |
| case bp_leg_l: | |
| temp_equalizer( bp_leg_l, bp_torso ); | |
| temp_equalizer( bp_leg_l, bp_foot_l ); | |
| break; | |
| case bp_leg_r: | |
| temp_equalizer( bp_leg_r, bp_torso ); | |
| temp_equalizer( bp_leg_r, bp_foot_r ); | |
| break; | |
| case bp_mouth: | |
| temp_equalizer( bp_mouth, bp_head ); | |
| break; | |
| case bp_hand_l: | |
| temp_equalizer( bp_hand_l, bp_arm_l ); | |
| break; | |
| case bp_hand_r: | |
| temp_equalizer( bp_hand_r, bp_arm_r ); | |
| break; | |
| case bp_foot_l: | |
| temp_equalizer( bp_foot_l, bp_leg_l ); | |
| break; | |
| case bp_foot_r: | |
| temp_equalizer( bp_foot_r, bp_leg_r ); | |
| break; | |
| default: | |
| debugmsg( "Wacky body part temperature equalization!" ); | |
| break; | |
| } | |
| // Climate Control eases the effects of high and low ambient temps | |
| if( has_climate_control ) { | |
| temp_conv[bp] = temp_corrected_by_climate_control( temp_conv[bp] ); | |
| } | |
| // FINAL CALCULATION : Increments current body temperature towards convergent. | |
| int bonus_fire_warmth = 0; | |
| if( !has_sleep_state && best_fire > 0 ) { | |
| // Warming up over a fire | |
| // Extremities are easier to extend over a fire | |
| switch( bp ) { | |
| case bp_head: | |
| case bp_torso: | |
| case bp_mouth: | |
| case bp_leg_l: | |
| case bp_leg_r: | |
| bonus_fire_warmth = best_fire * best_fire * 150; // Not much | |
| break; | |
| case bp_arm_l: | |
| case bp_arm_r: | |
| bonus_fire_warmth = best_fire * 600; // A fair bit | |
| break; | |
| case bp_foot_l: | |
| case bp_foot_r: | |
| if( furn_at_pos != f_null ) { | |
| // Can sit on something to lift feet up to the fire | |
| bonus_fire_warmth = best_fire * furn_at_pos.obj().bonus_fire_warmth_feet; | |
| } else { | |
| // Has to stand | |
| bonus_fire_warmth = best_fire * 300; | |
| } | |
| break; | |
| case bp_hand_l: | |
| case bp_hand_r: | |
| bonus_fire_warmth = best_fire * 1500; // A lot | |
| default: | |
| break; | |
| } | |
| } | |
| const int comfortable_warmth = bonus_fire_warmth + lying_warmth; | |
| const int bonus_warmth = comfortable_warmth + metabolism_warmth + mutation_heat_bonus; | |
| if( bonus_warmth > 0 ) { | |
| // Approximate temp_conv needed to reach comfortable temperature in this very turn | |
| // Basically inverted formula for temp_cur below | |
| int desired = 501 * BODYTEMP_NORM - 499 * temp_cur[bp]; | |
| if( std::abs( BODYTEMP_NORM - desired ) < 1000 ) { | |
| desired = BODYTEMP_NORM; // Ensure that it converges | |
| } else if( desired > BODYTEMP_HOT ) { | |
| desired = BODYTEMP_HOT; // Cap excess at sane temperature | |
| } | |
| if( desired < temp_conv[bp] ) { | |
| // Too hot, can't help here | |
| } else if( desired < temp_conv[bp] + bonus_warmth ) { | |
| // Use some heat, but not all of it | |
| temp_conv[bp] = desired; | |
| } else { | |
| // Use all the heat | |
| temp_conv[bp] += bonus_warmth; | |
| } | |
| // Morale bonus for comfiness - only if actually comfy (not too warm/cold) | |
| // Spread the morale bonus in time. | |
| if( comfortable_warmth > 0 && | |
| calendar::turn % MINUTES( 1 ) == ( MINUTES( bp ) / MINUTES( num_bp ) ) && | |
| get_effect_int( effect_cold, num_bp ) == 0 && | |
| get_effect_int( effect_hot, num_bp ) == 0 && | |
| temp_cur[bp] > BODYTEMP_COLD && temp_cur[bp] <= BODYTEMP_NORM ) { | |
| add_morale( MORALE_COMFY, 1, 10, 2_minutes, 1_minutes, true ); | |
| } | |
| } | |
| int temp_before = temp_cur[bp]; | |
| int temp_difference = temp_before - temp_conv[bp]; // Negative if the player is warming up. | |
| // exp(-0.001) : half life of 60 minutes, exp(-0.002) : half life of 30 minutes, | |
| // exp(-0.003) : half life of 20 minutes, exp(-0.004) : half life of 15 minutes | |
| int rounding_error = 0; | |
| // If temp_diff is small, the player cannot warm up due to rounding errors. This fixes that. | |
| if( temp_difference < 0 && temp_difference > -600 ) { | |
| rounding_error = 1; | |
| } | |
| if( temp_cur[bp] != temp_conv[bp] ) { | |
| temp_cur[bp] = static_cast<int>( temp_difference * exp( -0.002 ) + temp_conv[bp] + rounding_error ); | |
| } | |
| // This statement checks if we should be wearing our bonus warmth. | |
| // If, after all the warmth calculations, we should be, then we have to recalculate the temperature. | |
| if( clothing_warmth_adjusted_bonus != 0 && | |
| ( ( temp_conv[bp] + clothing_warmth_adjusted_bonus ) < BODYTEMP_HOT || | |
| temp_cur[bp] < BODYTEMP_COLD ) ) { | |
| temp_conv[bp] += clothing_warmth_adjusted_bonus; | |
| rounding_error = 0; | |
| if( temp_difference < 0 && temp_difference > -600 ) { | |
| rounding_error = 1; | |
| } | |
| if( temp_before != temp_conv[bp] ) { | |
| temp_difference = temp_before - temp_conv[bp]; | |
| temp_cur[bp] = static_cast<int>( temp_difference * exp( -0.002 ) + temp_conv[bp] + rounding_error ); | |
| } | |
| } | |
| int temp_after = temp_cur[bp]; | |
| // PENALTIES | |
| if( temp_cur[bp] < BODYTEMP_FREEZING ) { | |
| add_effect( effect_cold, 1_turns, bp, true, 3 ); | |
| } else if( temp_cur[bp] < BODYTEMP_VERY_COLD ) { | |
| add_effect( effect_cold, 1_turns, bp, true, 2 ); | |
| } else if( temp_cur[bp] < BODYTEMP_COLD ) { | |
| add_effect( effect_cold, 1_turns, bp, true, 1 ); | |
| } else if( temp_cur[bp] > BODYTEMP_SCORCHING ) { | |
| add_effect( effect_hot, 1_turns, bp, true, 3 ); | |
| } else if( temp_cur[bp] > BODYTEMP_VERY_HOT ) { | |
| add_effect( effect_hot, 1_turns, bp, true, 2 ); | |
| } else if( temp_cur[bp] > BODYTEMP_HOT ) { | |
| add_effect( effect_hot, 1_turns, bp, true, 1 ); | |
| } else { | |
| if( temp_cur[bp] >= BODYTEMP_COLD ) { | |
| remove_effect( effect_cold, bp ); | |
| } | |
| if( temp_cur[bp] <= BODYTEMP_HOT ) { | |
| remove_effect( effect_hot, bp ); | |
| } | |
| } | |
| // FROSTBITE - only occurs to hands, feet, face | |
| /** | |
| Source : http://www.atc.army.mil/weather/windchill.pdf | |
| Temperature and wind chill are main factors, mitigated by clothing warmth. Each 10 warmth protects against 2C of cold. | |
| 1200 turns in low risk, + 3 tics | |
| 450 turns in moderate risk, + 8 tics | |
| 50 turns in high risk, +72 tics | |
| Let's say frostnip @ 1800 tics, frostbite @ 3600 tics | |
| >> Chunked into 8 parts (http://imgur.com/xlTPmJF) | |
| -- 2 hour risk -- | |
| Between 30F and 10F | |
| Between 10F and -5F, less than 20mph, -4x + 3y - 20 > 0, x : F, y : mph | |
| -- 45 minute risk -- | |
| Between 10F and -5F, less than 20mph, -4x + 3y - 20 < 0, x : F, y : mph | |
| Between 10F and -5F, greater than 20mph | |
| Less than -5F, less than 10 mph | |
| Less than -5F, more than 10 mph, -4x + 3y - 170 > 0, x : F, y : mph | |
| -- 5 minute risk -- | |
| Less than -5F, more than 10 mph, -4x + 3y - 170 < 0, x : F, y : mph | |
| Less than -35F, more than 10 mp | |
| **/ | |
| if( bp == bp_mouth || bp == bp_foot_r || bp == bp_foot_l || bp == bp_hand_r || bp == bp_hand_l ) { | |
| // Handle the frostbite timer | |
| // Need temps in F, windPower already in mph | |
| int wetness_percentage = 100 * body_wetness[bp] / drench_capacity[bp]; // 0 - 100 | |
| // Warmth gives a slight buff to temperature resistance | |
| // Wetness gives a heavy nerf to temperature resistance | |
| int Ftemperature = static_cast<int>( player_local_temp + | |
| warmth( bp ) * 0.2 - 20 * wetness_percentage / 100 ); | |
| // Windchill reduced by your armor | |
| int FBwindPower = static_cast<int>( total_windpower * ( 1 - get_wind_resistance( bp ) / 100.0 ) ); | |
| int intense = get_effect_int( effect_frostbite, bp ); | |
| // This has been broken down into 8 zones | |
| // Low risk zones (stops at frostnip) | |
| if( temp_cur[bp] < BODYTEMP_COLD && | |
| ( ( Ftemperature < 30 && Ftemperature >= 10 ) || | |
| ( Ftemperature < 10 && Ftemperature >= -5 && | |
| FBwindPower < 20 && -4 * Ftemperature + 3 * FBwindPower - 20 >= 0 ) ) ) { | |
| if( frostbite_timer[bp] < 2000 ) { | |
| frostbite_timer[bp] += 3; | |
| } | |
| if( one_in( 100 ) && !has_effect( effect_frostbite, bp ) ) { | |
| add_msg( m_warning, _( "Your %s will be frostnipped in the next few hours." ), | |
| body_part_name( bp ) ); | |
| } | |
| // Medium risk zones | |
| } else if( temp_cur[bp] < BODYTEMP_COLD && | |
| ( ( Ftemperature < 10 && Ftemperature >= -5 && FBwindPower < 20 && | |
| -4 * Ftemperature + 3 * FBwindPower - 20 < 0 ) || | |
| ( Ftemperature < 10 && Ftemperature >= -5 && FBwindPower >= 20 ) || | |
| ( Ftemperature < -5 && FBwindPower < 10 ) || | |
| ( Ftemperature < -5 && FBwindPower >= 10 && | |
| -4 * Ftemperature + 3 * FBwindPower - 170 >= 0 ) ) ) { | |
| frostbite_timer[bp] += 8; | |
| if( one_in( 100 ) && intense < 2 ) { | |
| add_msg( m_warning, _( "Your %s will be frostbitten within the hour!" ), | |
| body_part_name( bp ) ); | |
| } | |
| // High risk zones | |
| } else if( temp_cur[bp] < BODYTEMP_COLD && | |
| ( ( Ftemperature < -5 && FBwindPower >= 10 && | |
| -4 * Ftemperature + 3 * FBwindPower - 170 < 0 ) || | |
| ( Ftemperature < -35 && FBwindPower >= 10 ) ) ) { | |
| frostbite_timer[bp] += 72; | |
| if( one_in( 100 ) && intense < 2 ) { | |
| add_msg( m_warning, _( "Your %s will be frostbitten any minute now!" ), | |
| body_part_name( bp ) ); | |
| } | |
| // Risk free, so reduce frostbite timer | |
| } else { | |
| frostbite_timer[bp] -= 3; | |
| } | |
| // Handle the bestowing of frostbite | |
| if( frostbite_timer[bp] < 0 ) { | |
| frostbite_timer[bp] = 0; | |
| } else if( frostbite_timer[bp] > 4200 ) { | |
| // This ensures that the player will recover in at most 3 hours. | |
| frostbite_timer[bp] = 4200; | |
| } | |
| // Frostbite, no recovery possible | |
| if( frostbite_timer[bp] >= 3600 ) { | |
| add_effect( effect_frostbite, 1_turns, bp, true, 2 ); | |
| remove_effect( effect_frostbite_recovery, bp ); | |
| // Else frostnip, add recovery if we were frostbitten | |
| } else if( frostbite_timer[bp] >= 1800 ) { | |
| if( intense == 2 ) { | |
| add_effect( effect_frostbite_recovery, 1_turns, bp, true ); | |
| } | |
| add_effect( effect_frostbite, 1_turns, bp, true, 1 ); | |
| // Else fully recovered | |
| } else if( frostbite_timer[bp] == 0 ) { | |
| remove_effect( effect_frostbite, bp ); | |
| remove_effect( effect_frostbite_recovery, bp ); | |
| } | |
| } | |
| // Warn the player if condition worsens | |
| if( temp_before > BODYTEMP_FREEZING && temp_after < BODYTEMP_FREEZING ) { | |
| //~ %s is bodypart | |
| add_msg( m_warning, _( "You feel your %s beginning to go numb from the cold!" ), | |
| body_part_name( bp ) ); | |
| } else if( temp_before > BODYTEMP_VERY_COLD && temp_after < BODYTEMP_VERY_COLD ) { | |
| //~ %s is bodypart | |
| add_msg( m_warning, _( "You feel your %s getting very cold." ), | |
| body_part_name( bp ) ); | |
| } else if( temp_before > BODYTEMP_COLD && temp_after < BODYTEMP_COLD ) { | |
| //~ %s is bodypart | |
| add_msg( m_warning, _( "You feel your %s getting chilly." ), | |
| body_part_name( bp ) ); | |
| } else if( temp_before < BODYTEMP_SCORCHING && temp_after > BODYTEMP_SCORCHING ) { | |
| //~ %s is bodypart | |
| add_msg( m_bad, _( "You feel your %s getting red hot from the heat!" ), | |
| body_part_name( bp ) ); | |
| } else if( temp_before < BODYTEMP_VERY_HOT && temp_after > BODYTEMP_VERY_HOT ) { | |
| //~ %s is bodypart | |
| add_msg( m_warning, _( "You feel your %s getting very hot." ), | |
| body_part_name( bp ) ); | |
| } else if( temp_before < BODYTEMP_HOT && temp_after > BODYTEMP_HOT ) { | |
| //~ %s is bodypart | |
| add_msg( m_warning, _( "You feel your %s getting warm." ), | |
| body_part_name( bp ) ); | |
| } | |
| // Warn the player that wind is going to be a problem. | |
| // But only if it can be a problem, no need to spam player with "wind chills your scorching body" | |
| if( temp_conv[bp] <= BODYTEMP_COLD && windchill < -10 && one_in( 200 ) ) { | |
| add_msg( m_bad, _( "The wind is making your %s feel quite cold." ), | |
| body_part_name( bp ) ); | |
| } else if( temp_conv[bp] <= BODYTEMP_COLD && windchill < -20 && one_in( 100 ) ) { | |
| add_msg( m_bad, | |
| _( "The wind is very strong, you should find some more wind-resistant clothing for your %s." ), | |
| body_part_name( bp ) ); | |
| } else if( temp_conv[bp] <= BODYTEMP_COLD && windchill < -30 && one_in( 50 ) ) { | |
| add_msg( m_bad, _( "Your clothing is not providing enough protection from the wind for your %s!" ), | |
| body_part_name( bp ) ); | |
| } | |
| } | |
| } | |
| bool player::can_use_floor_warmth() const | |
| { | |
| // TODO: Reading? Waiting? | |
| return in_sleep_state(); | |
| } | |
| int player::floor_bedding_warmth( const tripoint &pos ) | |
| { | |
| const trap &trap_at_pos = g->m.tr_at( pos ); | |
| const ter_id ter_at_pos = g->m.ter( pos ); | |
| const furn_id furn_at_pos = g->m.furn( pos ); | |
| int floor_bedding_warmth = 0; | |
| const optional_vpart_position vp = g->m.veh_at( pos ); | |
| const bool veh_bed = static_cast<bool>( vp.part_with_feature( "BED", true ) ); | |
| const bool veh_seat = static_cast<bool>( vp.part_with_feature( "SEAT", true ) ); | |
| // Search the floor for bedding | |
| if( furn_at_pos != f_null ) { | |
| floor_bedding_warmth += furn_at_pos.obj().floor_bedding_warmth; | |
| } else if( !trap_at_pos.is_null() ) { | |
| floor_bedding_warmth += trap_at_pos.floor_bedding_warmth; | |
| } else if( veh_bed && veh_seat ) { | |
| // BED+SEAT is intentionally worse than just BED | |
| floor_bedding_warmth += 250; | |
| } else if( veh_bed ) { | |
| floor_bedding_warmth += 300; | |
| } else if( veh_seat ) { | |
| floor_bedding_warmth += 200; | |
| } else if( ter_at_pos == t_improvised_shelter ) { | |
| floor_bedding_warmth -= 500; | |
| } else { | |
| floor_bedding_warmth -= 2000; | |
| } | |
| return floor_bedding_warmth; | |
| } | |
| int player::floor_item_warmth( const tripoint &pos ) | |
| { | |
| if( !g->m.has_items( pos ) ) { | |
| return 0; | |
| } | |
| int item_warmth = 0; | |
| // Search the floor for items | |
| const auto floor_item = g->m.i_at( pos ); | |
| for( const auto &elem : floor_item ) { | |
| if( !elem.is_armor() ) { | |
| continue; | |
| } | |
| // Items that are big enough and covers the torso are used to keep warm. | |
| // Smaller items don't do as good a job | |
| if( elem.volume() > 250_ml && | |
| ( elem.covers( bp_torso ) || elem.covers( bp_leg_l ) || | |
| elem.covers( bp_leg_r ) ) ) { | |
| item_warmth += 60 * elem.get_warmth() * elem.volume() / 2500_ml; | |
| } | |
| } | |
| return item_warmth; | |
| } | |
| int player::floor_warmth( const tripoint &pos ) const | |
| { | |
| const int item_warmth = floor_item_warmth( pos ); | |
| int bedding_warmth = floor_bedding_warmth( pos ); | |
| // If the PC has fur, etc, that will apply too | |
| int floor_mut_warmth = bodytemp_modifier_traits_floor(); | |
| // DOWN does not provide floor insulation, though. | |
| // Better-than-light fur or being in one's shell does. | |
| if( ( !( has_trait( trait_DOWN ) ) ) && ( floor_mut_warmth >= 200 ) ) { | |
| bedding_warmth = std::max( 0, bedding_warmth ); | |
| } | |
| return ( item_warmth + bedding_warmth + floor_mut_warmth ); | |
| } | |
| int player::bodytemp_modifier_traits( bool overheated ) const | |
| { | |
| int mod = 0; | |
| for( auto &iter : my_mutations ) { | |
| mod += overheated ? iter.first->bodytemp_min : | |
| iter.first->bodytemp_max; | |
| } | |
| return mod; | |
| } | |
| int player::bodytemp_modifier_traits_floor() const | |
| { | |
| int mod = 0; | |
| for( auto &iter : my_mutations ) { | |
| mod += iter.first->bodytemp_sleep; | |
| } | |
| return mod; | |
| } | |
| int player::temp_corrected_by_climate_control( int temperature ) const | |
| { | |
| const int variation = int( BODYTEMP_NORM * 0.5 ); | |
| if( temperature < BODYTEMP_SCORCHING + variation && | |
| temperature > BODYTEMP_FREEZING - variation ) { | |
| if( temperature > BODYTEMP_SCORCHING ) { | |
| temperature = BODYTEMP_VERY_HOT; | |
| } else if( temperature > BODYTEMP_VERY_HOT ) { | |
| temperature = BODYTEMP_HOT; | |
| } else if( temperature > BODYTEMP_HOT ) { | |
| temperature = BODYTEMP_NORM; | |
| } else if( temperature < BODYTEMP_FREEZING ) { | |
| temperature = BODYTEMP_VERY_COLD; | |
| } else if( temperature < BODYTEMP_VERY_COLD ) { | |
| temperature = BODYTEMP_COLD; | |
| } else if( temperature < BODYTEMP_COLD ) { | |
| temperature = BODYTEMP_NORM; | |
| } | |
| } | |
| return temperature; | |
| } | |
| int player::blood_loss( body_part bp ) const | |
| { | |
| int hp_cur_sum = 1; | |
| int hp_max_sum = 1; | |
| if( bp == bp_leg_l || bp == bp_leg_r ) { | |
| hp_cur_sum = hp_cur[hp_leg_l] + hp_cur[hp_leg_r]; | |
| hp_max_sum = hp_max[hp_leg_l] + hp_max[hp_leg_r]; | |
| } else if( bp == bp_arm_l || bp == bp_arm_r ) { | |
| hp_cur_sum = hp_cur[hp_arm_l] + hp_cur[hp_arm_r]; | |
| hp_max_sum = hp_max[hp_arm_l] + hp_max[hp_arm_r]; | |
| } else if( bp == bp_torso ) { | |
| hp_cur_sum = hp_cur[hp_torso]; | |
| hp_max_sum = hp_max[hp_torso]; | |
| } else if( bp == bp_head ) { | |
| hp_cur_sum = hp_cur[hp_head]; | |
| hp_max_sum = hp_max[hp_head]; | |
| } | |
| hp_cur_sum = std::min( hp_max_sum, std::max( 0, hp_cur_sum ) ); | |
| return 100 - ( 100 * hp_cur_sum ) / hp_max_sum; | |
| } | |
| void player::temp_equalizer( body_part bp1, body_part bp2 ) | |
| { | |
| // Body heat is moved around. | |
| // Shift in one direction only, will be shifted in the other direction separately. | |
| int diff = static_cast<int>( ( temp_cur[bp2] - temp_cur[bp1] ) * | |
| 0.0001 ); // If bp1 is warmer, it will lose heat | |
| temp_cur[bp1] += diff; | |
| } | |
| int player::kcal_speed_penalty() | |
| { | |
| static const std::vector<std::pair<float, float>> starv_thresholds = { { | |
| std::make_pair( 0.0f, -90.0f ), | |
| std::make_pair( character_weight_category::emaciated, -50.f ), | |
| std::make_pair( character_weight_category::underweight, -25.0f ), | |
| std::make_pair( character_weight_category::normal, 0.0f ) | |
| } | |
| }; | |
| if( get_kcal_percent() > 0.95f ) { | |
| // @TODO: get speed penalties for being too fat, too | |
| return 0; | |
| } else { | |
| return round( multi_lerp( starv_thresholds, get_bmi() ) ); | |
| } | |
| } | |
| int player::thirst_speed_penalty( int thirst ) | |
| { | |
| // We die at 1200 thirst | |
| // Start by dropping speed really fast, but then level it off a bit | |
| static const std::vector<std::pair<float, float>> thirst_thresholds = {{ | |
| std::make_pair( 40.0f, 0.0f ), | |
| std::make_pair( 300.0f, -25.0f ), | |
| std::make_pair( 600.0f, -50.0f ), | |
| std::make_pair( 1200.0f, -75.0f ) | |
| } | |
| }; | |
| return static_cast<int>( multi_lerp( thirst_thresholds, thirst ) ); | |
| } | |
| void player::recalc_speed_bonus() | |
| { | |
| // Minus some for weight... | |
| int carry_penalty = 0; | |
| if( weight_carried() > weight_capacity() && !has_trait( trait_id( "DEBUG_STORAGE" ) ) ) { | |
| carry_penalty = 25 * ( weight_carried() - weight_capacity() ) / ( weight_capacity() ); | |
| } | |
| mod_speed_bonus( -carry_penalty ); | |
| mod_speed_bonus( -get_pain_penalty().speed ); | |
| if( get_thirst() > 40 ) { | |
| mod_speed_bonus( thirst_speed_penalty( get_thirst() ) ); | |
| } | |
| // fat or underweight, you get slower. cumulative with hunger | |
| mod_speed_bonus( kcal_speed_penalty() ); | |
| for( const auto &maps : *effects ) { | |
| for( auto i : maps.second ) { | |
| bool reduced = resists_effect( i.second ); | |
| mod_speed_bonus( i.second.get_mod( "SPEED", reduced ) ); | |
| } | |
| } | |
| // add martial arts speed bonus | |
| mod_speed_bonus( mabuff_speed_bonus() ); | |
| // Not sure why Sunlight Dependent is here, but OK | |
| // Ectothermic/COLDBLOOD4 is intended to buff folks in the Summer | |
| // Threshold-crossing has its charms ;-) | |
| if( g != nullptr ) { | |
| if( has_trait( trait_SUNLIGHT_DEPENDENT ) && !g->is_in_sunlight( pos() ) ) { | |
| mod_speed_bonus( -( g->light_level( posz() ) >= 12 ? 5 : 10 ) ); | |
| } | |
| const float temperature_speed_modifier = mutation_value( "temperature_speed_modifier" ); | |
| if( temperature_speed_modifier != 0 ) { | |
| const auto player_local_temp = g->weather.get_temperature( pos() ); | |
| if( has_trait( trait_COLDBLOOD4 ) || player_local_temp < 65 ) { | |
| mod_speed_bonus( ( player_local_temp - 65 ) * temperature_speed_modifier ); | |
| } | |
| } | |
| } | |
| if( has_artifact_with( AEP_SPEED_UP ) ) { | |
| mod_speed_bonus( 20 ); | |
| } | |
| if( has_artifact_with( AEP_SPEED_DOWN ) ) { | |
| mod_speed_bonus( -20 ); | |
| } | |
| float speed_modifier = Character::mutation_value( "speed_modifier" ); | |
| set_speed_bonus( static_cast<int>( get_speed() * speed_modifier ) - get_speed_base() ); | |
| if( has_bionic( bio_speed ) ) { // multiply by 1.1 | |
| set_speed_bonus( static_cast<int>( get_speed() * 1.1 ) - get_speed_base() ); | |
| } | |
| // Speed cannot be less than 25% of base speed, so minimal speed bonus is -75% base speed. | |
| const int min_speed_bonus = static_cast<int>( -0.75 * get_speed_base() ); | |
| if( get_speed_bonus() < min_speed_bonus ) { | |
| set_speed_bonus( min_speed_bonus ); | |
| } | |
| } | |
| int player::run_cost( int base_cost, bool diag ) const | |
| { | |
| float movecost = static_cast<float>( base_cost ); | |
| if( diag ) { | |
| movecost *= 0.7071f; // because everything here assumes 100 is base | |
| } | |
| float stamina_modifier; | |
| const bool flatground = movecost < 105; | |
| // The "FLAT" tag includes soft surfaces, so not a good fit. | |
| const bool on_road = flatground && g->m.has_flag( "ROAD", pos() ); | |
| const bool on_fungus = g->m.has_flag_ter_or_furn( "FUNGUS", pos() ); | |
| if( movecost > 100 ) { | |
| movecost *= Character::mutation_value( "movecost_obstacle_modifier" ); | |
| if( movecost < 100 ) { | |
| movecost = 100; | |
| } | |
| } | |
| if( !has_effect( effect_riding ) ) { | |
| if( movecost > 100 ) { | |
| movecost *= Character::mutation_value( "movecost_obstacle_modifier" ); | |
| if( movecost < 100 ) { | |
| movecost = 100; | |
| } | |
| } | |
| if( has_trait( trait_M_IMMUNE ) && on_fungus ) { | |
| if( movecost > 75 ) { | |
| movecost = | |
| 75; // Mycal characters are faster on their home territory, even through things like shrubs | |
| } | |
| } | |
| if( hp_cur[hp_leg_l] == 0 ) { | |
| movecost += 50; | |
| } else if( hp_cur[hp_leg_l] < hp_max[hp_leg_l] * .40 ) { | |
| movecost += 25; | |
| } | |
| if( hp_cur[hp_leg_r] == 0 ) { | |
| movecost += 50; | |
| } else if( hp_cur[hp_leg_r] < hp_max[hp_leg_r] * .40 ) { | |
| movecost += 25; | |
| } | |
| movecost *= Character::mutation_value( "movecost_modifier" ); | |
| if( flatground ) { | |
| movecost *= Character::mutation_value( "movecost_flatground_modifier" ); | |
| } | |
| if( has_trait( trait_PADDED_FEET ) && !footwear_factor() ) { | |
| movecost *= .9f; | |
| } | |
| if( has_active_bionic( bio_jointservo ) ) { | |
| if( move_mode == PMM_RUN ) { | |
| movecost *= 0.85f; | |
| } else { | |
| movecost *= 0.95f; | |
| } | |
| } else if( has_bionic( bio_jointservo ) ) { | |
| movecost *= 1.1f; | |
| } | |
| if( worn_with_flag( "SLOWS_MOVEMENT" ) ) { | |
| movecost *= 1.1f; | |
| } | |
| if( worn_with_flag( "FIN" ) ) { | |
| movecost *= 1.5f; | |
| } | |
| if( worn_with_flag( "ROLLER_INLINE" ) ) { | |
| if( on_road ) { | |
| movecost *= 0.5f; | |
| } else { | |
| movecost *= 1.5f; | |
| } | |
| } | |
| // Quad skates might be more stable than inlines, | |
| // but that also translates into a slower speed when on good surfaces. | |
| if( worn_with_flag( "ROLLER_QUAD" ) ) { | |
| if( on_road ) { | |
| movecost *= 0.7f; | |
| } else { | |
| movecost *= 1.3f; | |
| } | |
| } | |
| // Skates with only one wheel (roller shoes) are fairly less stable | |
| // and fairly slower as well | |
| if( worn_with_flag( "ROLLER_ONE" ) ) { | |
| if( on_road ) { | |
| movecost *= 0.85f; | |
| } else { | |
| movecost *= 1.1f; | |
| } | |
| } | |
| movecost += | |
| ( ( encumb( bp_foot_l ) + encumb( bp_foot_r ) ) * 2.5 + | |
| ( encumb( bp_leg_l ) + encumb( bp_leg_r ) ) * 1.5 ) / 10; | |
| // ROOTS3 does slow you down as your roots are probing around for nutrients, | |
| // whether you want them to or not. ROOTS1 is just too squiggly without shoes | |
| // to give you some stability. Plants are a bit of a slow-mover. Deal. | |
| const bool mutfeet = has_trait( trait_LEG_TENTACLES ) || has_trait( trait_PADDED_FEET ) || | |
| has_trait( trait_HOOVES ) || has_trait( trait_TOUGH_FEET ) || has_trait( trait_ROOTS2 ); | |
| if( !is_wearing_shoes( side::LEFT ) && !mutfeet ) { | |
| movecost += 8; | |
| } | |
| if( !is_wearing_shoes( side::RIGHT ) && !mutfeet ) { | |
| movecost += 8; | |
| } | |
| if( has_trait( trait_ROOTS3 ) && g->m.has_flag( "DIGGABLE", pos() ) ) { | |
| movecost += 10 * footwear_factor(); | |
| } | |
| // Both walk and run speed drop to half their maximums as stamina approaches 0. | |
| // Convert stamina to a float first to allow for decimal place carrying | |
| stamina_modifier = ( static_cast<float>( stamina ) / get_stamina_max() + 1 ) / 2; | |
| } else { | |
| stamina_modifier = 1.0; | |
| } | |
| if( move_mode == PMM_RUN && stamina > 0 ) { | |
| // Rationale: Average running speed is 2x walking speed. (NOT sprinting) | |
| stamina_modifier *= 2.0; | |
| } | |
| if( move_mode == PMM_CROUCH ) { | |
| stamina_modifier *= 0.5; | |
| } | |
| movecost /= stamina_modifier; | |
| if( diag ) { | |
| movecost *= M_SQRT2; | |
| } | |
| return static_cast<int>( movecost ); | |
| } | |
| int player::swim_speed() const | |
| { | |
| int ret; | |
| if( has_effect( effect_riding ) && mounted_creature != nullptr ) { | |
| monster *mon = mounted_creature.get(); | |
| // no difference in swim speed by monster type yet. | |
| // TODO : difference in swim speed by monster type. | |
| // No monsters are currently mountable and can swim, though mods may allow this. | |
| if( mon->has_flag( MF_SWIMS ) ) { | |
| ret = 25; | |
| ret += get_weight() / 120_gram - 50 * mon->get_size(); | |
| return ret; | |
| } | |
| } | |
| const auto usable = exclusive_flag_coverage( "ALLOWS_NATURAL_ATTACKS" ); | |
| float hand_bonus_mult = ( usable.test( bp_hand_l ) ? 0.5f : 0.0f ) + | |
| ( usable.test( bp_hand_r ) ? 0.5f : 0.0f ); | |
| if( !has_trait( trait_AMPHIBIAN ) ) { | |
| ret = 440 + weight_carried() / 60_gram - 50 * get_skill_level( skill_swimming ); | |
| /** AMPHIBIAN increases base swim speed */ | |
| } else { | |
| ret = 200 + weight_carried() / 120_gram - 50 * get_skill_level( skill_swimming ); | |
| } | |
| /** @EFFECT_STR increases swim speed bonus from PAWS */ | |
| if( has_trait( trait_PAWS ) ) { | |
| ret -= hand_bonus_mult * ( 20 + str_cur * 3 ); | |
| } | |
| /** @EFFECT_STR increases swim speed bonus from PAWS_LARGE */ | |
| if( has_trait( trait_PAWS_LARGE ) ) { | |
| ret -= hand_bonus_mult * ( 20 + str_cur * 4 ); | |
| } | |
| /** @EFFECT_STR increases swim speed bonus from swim_fins */ | |
| if( worn_with_flag( "FIN", bp_foot_l ) || worn_with_flag( "FIN", bp_foot_r ) ) { | |
| if( worn_with_flag( "FIN", bp_foot_l ) && worn_with_flag( "FIN", bp_foot_r ) ) { | |
| ret -= ( 15 * str_cur ); | |
| } else { | |
| ret -= ( 15 * str_cur ) / 2; | |
| } | |
| } | |
| /** @EFFECT_STR increases swim speed bonus from WEBBED */ | |
| if( has_trait( trait_WEBBED ) ) { | |
| ret -= hand_bonus_mult * ( 60 + str_cur * 5 ); | |
| } | |
| /** @EFFECT_STR increases swim speed bonus from TAIL_FIN */ | |
| if( has_trait( trait_TAIL_FIN ) ) { | |
| ret -= 100 + str_cur * 10; | |
| } | |
| if( has_trait( trait_SLEEK_SCALES ) ) { | |
| ret -= 100; | |
| } | |
| if( has_trait( trait_LEG_TENTACLES ) ) { | |
| ret -= 60; | |
| } | |
| if( has_trait( trait_FAT ) ) { | |
| ret -= 30; | |
| } | |
| /** @EFFECT_SWIMMING increases swim speed */ | |
| ret += ( 50 - get_skill_level( skill_swimming ) * 2 ) * ( ( encumb( bp_leg_l ) + encumb( | |
| bp_leg_r ) ) / 10 ); | |
| ret += ( 80 - get_skill_level( skill_swimming ) * 3 ) * ( encumb( bp_torso ) / 10 ); | |
| if( get_skill_level( skill_swimming ) < 10 ) { | |
| for( auto &i : worn ) { | |
| ret += i.volume() / 125_ml * ( 10 - get_skill_level( skill_swimming ) ); | |
| } | |
| } | |
| /** @EFFECT_STR increases swim speed */ | |
| /** @EFFECT_DEX increases swim speed */ | |
| ret -= str_cur * 6 + dex_cur * 4; | |
| if( worn_with_flag( "FLOTATION" ) ) { | |
| ret = std::min( ret, 400 ); | |
| ret = std::max( ret, 200 ); | |
| } | |
| // If (ret > 500), we can not swim; so do not apply the underwater bonus. | |
| if( underwater && ret < 500 ) { | |
| ret -= 50; | |
| } | |
| if( ret < 30 ) { | |
| ret = 30; | |
| } | |
| return ret; | |
| } | |
| bool player::digging() const | |
| { | |
| return false; | |
| } | |
| bool player::is_on_ground() const | |
| { | |
| return hp_cur[hp_leg_l] == 0 || hp_cur[hp_leg_r] == 0 || has_effect( effect_downed ); | |
| } | |
| bool player::is_elec_immune() const | |
| { | |
| return is_immune_damage( DT_ELECTRIC ); | |
| } | |
| bool player::is_immune_effect( const efftype_id &eff ) const | |
| { | |
| if( eff == effect_downed ) { | |
| return is_throw_immune() || ( has_trait( trait_LEG_TENT_BRACE ) && footwear_factor() == 0 ); | |
| } else if( eff == effect_onfire ) { | |
| return is_immune_damage( DT_HEAT ); | |
| } else if( eff == effect_deaf ) { | |
| return worn_with_flag( "DEAF" ) || worn_with_flag( "PARTIAL_DEAF" ) || has_bionic( bio_ears ) || | |
| is_wearing( "rm13_armor_on" ); | |
| } else if( eff == effect_corroding ) { | |
| return is_immune_damage( DT_ACID ) || has_trait( trait_SLIMY ) || has_trait( trait_VISCOUS ); | |
| } else if( eff == effect_nausea ) { | |
| return has_trait( trait_STRONGSTOMACH ); | |
| } | |
| return false; | |
| } | |
| float player::stability_roll() const | |
| { | |
| /** @EFFECT_STR improves player stability roll */ | |
| /** @EFFECT_PER slightly improves player stability roll */ | |
| /** @EFFECT_DEX slightly improves player stability roll */ | |
| /** @EFFECT_MELEE improves player stability roll */ | |
| return get_melee() + get_str() + ( get_per() / 3.0f ) + ( get_dex() / 4.0f ); | |
| } | |
| bool player::is_immune_damage( const damage_type dt ) const | |
| { | |
| switch( dt ) { | |
| case DT_NULL: | |
| return true; | |
| case DT_TRUE: | |
| return false; | |
| case DT_BIOLOGICAL: | |
| return false; | |
| case DT_BASH: | |
| return false; | |
| case DT_CUT: | |
| return false; | |
| case DT_ACID: | |
| return has_trait( trait_ACIDPROOF ); | |
| case DT_STAB: | |
| return false; | |
| case DT_HEAT: | |
| return has_trait( trait_M_SKIN2 ) || has_trait( trait_M_SKIN3 ); | |
| case DT_COLD: | |
| return false; | |
| case DT_ELECTRIC: | |
| return has_active_bionic( bio_faraday ) || | |
| worn_with_flag( "ELECTRIC_IMMUNE" ) || | |
| has_artifact_with( AEP_RESIST_ELECTRICITY ); | |
| default: | |
| return true; | |
| } | |
| } | |
| double player::recoil_vehicle() const | |
| { | |
| // TODO: vary penalty dependent upon vehicle part on which player is boarded | |
| if( in_vehicle ) { | |
| if( const optional_vpart_position vp = g->m.veh_at( pos() ) ) { | |
| return static_cast<double>( abs( vp->vehicle().velocity ) ) * 3 / 100; | |
| } | |
| } | |
| return 0; | |
| } | |
| double player::recoil_total() const | |
| { | |
| return recoil + recoil_vehicle(); | |
| } | |
| bool player::is_underwater() const | |
| { | |
| return underwater; | |
| } | |
| bool player::is_hallucination() const | |
| { | |
| return false; | |
| } | |
| void player::set_underwater( bool u ) | |
| { | |
| if( underwater != u ) { | |
| underwater = u; | |
| recalc_sight_limits(); | |
| } | |
| } | |
| nc_color player::basic_symbol_color() const | |
| { | |
| if( has_effect( effect_onfire ) ) { | |
| return c_red; | |
| } | |
| if( has_effect( effect_stunned ) ) { | |
| return c_light_blue; | |
| } | |
| if( has_effect( effect_boomered ) ) { | |
| return c_pink; | |
| } | |
| if( has_active_mutation( trait_id( "SHELL2" ) ) ) { | |
| return c_magenta; | |
| } | |
| if( underwater ) { | |
| return c_blue; | |
| } | |
| if( has_active_bionic( bio_cloak ) || has_artifact_with( AEP_INVISIBLE ) || | |
| has_active_optcloak() || has_trait( trait_DEBUG_CLOAK ) ) { | |
| return c_dark_gray; | |
| } | |
| return c_white; | |
| } | |
| /** | |
| * Adds an event to the memorial log, to be written to the memorial file when | |
| * the character dies. The message should contain only the informational string, | |
| * as the timestamp and location will be automatically prepended. | |
| */ | |
| void player::add_memorial_log( const std::string &male_msg, const std::string &female_msg ) | |
| { | |
| const std::string &msg = male ? male_msg : female_msg; | |
| if( msg.empty() ) { | |
| return; | |
| } | |
| const oter_id &cur_ter = overmap_buffer.ter( global_omt_location() ); | |
| const std::string &location = cur_ter->get_name(); | |
| std::stringstream log_message; | |
| log_message << "| " << to_string( time_point( calendar::turn ) ) << " | " << location << " | " << | |
| msg; | |
| memorial_log.push_back( log_message.str() ); | |
| } | |
| /** | |
| * Loads the data in a memorial file from the given ifstream. All the memorial | |
| * entry lines begin with a pipe (|). | |
| * @param fin The ifstream to read the memorial entries from. | |
| */ | |
| void player::load_memorial_file( std::istream &fin ) | |
| { | |
| std::string entry; | |
| memorial_log.clear(); | |
| while( fin.peek() == '|' ) { | |
| getline( fin, entry ); | |
| // strip all \r from end of string | |
| while( *entry.rbegin() == '\r' ) { | |
| entry.pop_back(); | |
| } | |
| memorial_log.push_back( entry ); | |
| } | |
| } | |
| /** | |
| * Concatenates all of the memorial log entries, delimiting them with newlines, | |
| * and returns the resulting string. Used for saving and for writing out to the | |
| * memorial file. | |
| */ | |
| std::string player::dump_memorial() const | |
| { | |
| static const char *eol = cata_files::eol(); | |
| std::stringstream output; | |
| for( auto &elem : memorial_log ) { | |
| output << elem << eol; | |
| } | |
| return output.str(); | |
| } | |
| void player::mod_stat( const std::string &stat, float modifier ) | |
| { | |
| if( stat == "thirst" ) { | |
| mod_thirst( modifier ); | |
| } else if( stat == "fatigue" ) { | |
| mod_fatigue( modifier ); | |
| } else if( stat == "oxygen" ) { | |
| oxygen += modifier; | |
| } else if( stat == "stamina" ) { | |
| if( stamina + modifier < 0 ) { | |
| add_effect( effect_winded, 10_turns ); | |
| } | |
| stamina += modifier; | |
| stamina = std::min( stamina, get_stamina_max() ); | |
| stamina = std::max( 0, stamina ); | |
| } else { | |
| // Fall through to the creature method. | |
| Character::mod_stat( stat, modifier ); | |
| } | |
| } | |
| time_duration player::estimate_effect_dur( const skill_id &relevant_skill, | |
| const efftype_id &target_effect, const time_duration &error_magnitude, | |
| int threshold, const Creature &target ) const | |
| { | |
| const time_duration zero_duration = 0_turns; | |
| int skill_lvl = get_skill_level( relevant_skill ); | |
| time_duration estimate = std::max( zero_duration, target.get_effect_dur( target_effect ) + | |
| rng( -1, 1 ) * error_magnitude * | |
| rng( 0, std::max( 0, threshold - skill_lvl ) ) ); | |
| return estimate; | |
| } | |
| bool player::has_conflicting_trait( const trait_id &flag ) const | |
| { | |
| return ( has_opposite_trait( flag ) || has_lower_trait( flag ) || has_higher_trait( flag ) || | |
| has_same_type_trait( flag ) ); | |
| } | |
| bool player::has_opposite_trait( const trait_id &flag ) const | |
| { | |
| for( auto &i : flag->cancels ) { | |
| if( has_trait( i ) ) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool player::has_lower_trait( const trait_id &flag ) const | |
| { | |
| for( auto &i : flag->prereqs ) { | |
| if( has_trait( i ) || has_lower_trait( i ) ) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool player::has_higher_trait( const trait_id &flag ) const | |
| { | |
| for( auto &i : flag->replacements ) { | |
| if( has_trait( i ) || has_higher_trait( i ) ) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool player::has_same_type_trait( const trait_id &flag ) const | |
| { | |
| for( auto &i : get_mutations_in_types( flag->types ) ) { | |
| if( has_trait( i ) && flag != i ) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool player::crossed_threshold() const | |
| { | |
| for( auto &mut : my_mutations ) { | |
| if( mut.first->threshold ) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool player::purifiable( const trait_id &flag ) const | |
| { | |
| return flag->purifiable; | |
| } | |
| void player::build_mut_dependency_map( const trait_id &mut, | |
| std::unordered_map<trait_id, int> &dependency_map, int distance ) | |
| { | |
| // Skip base traits and traits we've seen with a lower distance | |
| const auto lowest_distance = dependency_map.find( mut ); | |
| if( !has_base_trait( mut ) && ( lowest_distance == dependency_map.end() || | |
| distance < lowest_distance->second ) ) { | |
| dependency_map[mut] = distance; | |
| // Recurse over all prerequisite and replacement mutations | |
| const auto &mdata = mut.obj(); | |
| for( auto &i : mdata.prereqs ) { | |
| build_mut_dependency_map( i, dependency_map, distance + 1 ); | |
| } | |
| for( auto &i : mdata.prereqs2 ) { | |
| build_mut_dependency_map( i, dependency_map, distance + 1 ); | |
| } | |
| for( auto &i : mdata.replacements ) { | |
| build_mut_dependency_map( i, dependency_map, distance + 1 ); | |
| } | |
| } | |
| } | |
| void player::set_highest_cat_level() | |
| { | |
| mutation_category_level.clear(); | |
| // For each of our mutations... | |
| for( auto &mut : my_mutations ) { | |
| // ...build up a map of all prerequisite/replacement mutations along the tree, along with their distance from the current mutation | |
| std::unordered_map<trait_id, int> dependency_map; | |
| build_mut_dependency_map( mut.first, dependency_map, 0 ); | |
| // Then use the map to set the category levels | |
| for( auto &i : dependency_map ) { | |
| const auto &mdata = i.first.obj(); | |
| for( auto &cat : mdata.category ) { | |
| // Decay category strength based on how far it is from the current mutation | |
| mutation_category_level[cat] += 8 / static_cast<int>( std::pow( 2, i.second ) ); | |
| } | |
| } | |
| } | |
| } | |
| /// Returns the mutation category with the highest strength | |
| std::string player::get_highest_category() const | |
| { | |
| int iLevel = 0; | |
| std::string sMaxCat; | |
| for( const auto &elem : mutation_category_level ) { | |
| if( elem.second > iLevel ) { | |
| sMaxCat = elem.first; | |
| iLevel = elem.second; | |
| } else if( elem.second == iLevel ) { | |
| sMaxCat.clear(); // no category on ties | |
| } | |
| } | |
| return sMaxCat; | |
| } | |
| /// Returns a randomly selected dream | |
| std::string player::get_category_dream( const std::string &cat, | |
| int strength ) const | |
| { | |
| std::vector<dream> valid_dreams; | |
| //Pull the list of dreams | |
| for( auto &i : dreams ) { | |
| //Pick only the ones matching our desired category and strength | |
| if( ( i.category == cat ) && ( i.strength == strength ) ) { | |
| // Put the valid ones into our list | |
| valid_dreams.push_back( i ); | |
| } | |
| } | |
| if( valid_dreams.empty() ) { | |
| return ""; | |
| } | |
| const dream &selected_dream = random_entry( valid_dreams ); | |
| return random_entry( selected_dream.messages() ); | |
| } | |
| bool player::in_climate_control() | |
| { | |
| bool regulated_area = false; | |
| // Check | |
| if( has_active_bionic( bio_climate ) ) { | |
| return true; | |
| } | |
| if( has_trait( trait_M_SKIN3 ) && g->m.has_flag_ter_or_furn( "FUNGUS", pos() ) && | |
| in_sleep_state() ) { | |
| return true; | |
| } | |
| for( auto &w : worn ) { | |
| if( w.active && w.is_power_armor() ) { | |
| return true; | |
| } | |
| if( worn_with_flag( "CLIMATE_CONTROL" ) ) { | |
| return true; | |
| } | |
| } | |
| if( calendar::turn >= next_climate_control_check ) { | |
| // save CPU and simulate acclimation. | |
| next_climate_control_check = calendar::turn + 20_turns; | |
| if( const optional_vpart_position vp = g->m.veh_at( pos() ) ) { | |
| regulated_area = ( | |
| vp->is_inside() && // Already checks for opened doors | |
| vp->vehicle().total_power_w( true ) > 0 // Out of gas? No AC for you! | |
| ); // TODO: (?) Force player to scrounge together an AC unit | |
| } | |
| // TODO: AC check for when building power is implemented | |
| last_climate_control_ret = regulated_area; | |
| if( !regulated_area ) { | |
| // Takes longer to cool down / warm up with AC, than it does to step outside and feel cruddy. | |
| next_climate_control_check += 40_turns; | |
| } | |
| } else { | |
| return last_climate_control_ret; | |
| } | |
| return regulated_area; | |
| } | |
| std::list<item *> player::get_radio_items() | |
| { | |
| std::list<item *> rc_items; | |
| const invslice &stacks = inv.slice(); | |
| for( auto &stack : stacks ) { | |
| item &stack_iter = stack->front(); | |
| if( stack_iter.has_flag( "RADIO_ACTIVATION" ) ) { | |
| rc_items.push_back( &stack_iter ); | |
| } | |
| } | |
| for( auto &elem : worn ) { | |
| if( elem.has_flag( "RADIO_ACTIVATION" ) ) { | |
| rc_items.push_back( &elem ); | |
| } | |
| } | |
| if( is_armed() ) { | |
| if( weapon.has_flag( "RADIO_ACTIVATION" ) ) { | |
| rc_items.push_back( &weapon ); | |
| } | |
| } | |
| return rc_items; | |
| } | |
| std::list<item *> player::get_artifact_items() | |
| { | |
| std::list<item *> art_items; | |
| const invslice &stacks = inv.slice(); | |
| for( auto &stack : stacks ) { | |
| item &stack_iter = stack->front(); | |
| if( stack_iter.is_artifact() ) { | |
| art_items.push_back( &stack_iter ); | |
| } | |
| } | |
| for( auto &elem : worn ) { | |
| if( elem.is_artifact() ) { | |
| art_items.push_back( &elem ); | |
| } | |
| } | |
| if( is_armed() ) { | |
| if( weapon.is_artifact() ) { | |
| art_items.push_back( &weapon ); | |
| } | |
| } | |
| return art_items; | |
| } | |
| bool player::has_active_optcloak() const | |
| { | |
| for( auto &w : worn ) { | |
| if( w.active && w.has_flag( "ACTIVE_CLOAKING" ) ) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| void player::charge_power( int amount ) | |
| { | |
| power_level = clamp( power_level + amount, 0, max_power_level ); | |
| } | |
| /* | |
| * Calculate player brightness based on the brightest active item, as | |
| * per itype tag LIGHT_* and optional CHARGEDIM ( fade starting at 20% charge ) | |
| * item.light.* is -unimplemented- for the moment, as it is a custom override for | |
| * applying light sources/arcs with specific angle and direction. | |
| */ | |
| float player::active_light() const | |
| { | |
| float lumination = 0; | |
| int maxlum = 0; | |
| has_item_with( [&maxlum]( const item & it ) { | |
| const int lumit = it.getlight_emit(); | |
| if( maxlum < lumit ) { | |
| maxlum = lumit; | |
| } | |
| return false; // continue search, otherwise has_item_with would cancel the search | |
| } ); | |
| lumination = static_cast<float>( maxlum ); | |
| if( lumination < 60 && has_active_bionic( bio_flashlight ) ) { | |
| lumination = 60; | |
| } else if( lumination < 25 && has_artifact_with( AEP_GLOW ) ) { | |
| lumination = 25; | |
| } else if( lumination < 5 && ( has_effect( effect_glowing ) || | |
| ( has_active_bionic( bio_tattoo_led ) || | |
| has_effect( effect_glowy_led ) ) ) ) { | |
| lumination = 5; | |
| } | |
| return lumination; | |
| } | |
| tripoint player::global_square_location() const | |
| { | |
| return g->m.getabs( position ); | |
| } | |
| tripoint player::global_sm_location() const | |
| { | |
| return ms_to_sm_copy( global_square_location() ); | |
| } | |
| tripoint player::global_omt_location() const | |
| { | |
| return ms_to_omt_copy( global_square_location() ); | |
| } | |
| const tripoint &player::pos() const | |
| { | |
| return position; | |
| } | |
| int player::sight_range( int light_level ) const | |
| { | |
| /* Via Beer-Lambert we have: | |
| * light_level * (1 / exp( LIGHT_TRANSPARENCY_OPEN_AIR * distance) ) <= LIGHT_AMBIENT_LOW | |
| * Solving for distance: | |
| * 1 / exp( LIGHT_TRANSPARENCY_OPEN_AIR * distance ) <= LIGHT_AMBIENT_LOW / light_level | |
| * 1 <= exp( LIGHT_TRANSPARENCY_OPEN_AIR * distance ) * LIGHT_AMBIENT_LOW / light_level | |
| * light_level <= exp( LIGHT_TRANSPARENCY_OPEN_AIR * distance ) * LIGHT_AMBIENT_LOW | |
| * log(light_level) <= LIGHT_TRANSPARENCY_OPEN_AIR * distance + log(LIGHT_AMBIENT_LOW) | |
| * log(light_level) - log(LIGHT_AMBIENT_LOW) <= LIGHT_TRANSPARENCY_OPEN_AIR * distance | |
| * log(LIGHT_AMBIENT_LOW / light_level) <= LIGHT_TRANSPARENCY_OPEN_AIR * distance | |
| * log(LIGHT_AMBIENT_LOW / light_level) * (1 / LIGHT_TRANSPARENCY_OPEN_AIR) <= distance | |
| */ | |
| int range = static_cast<int>( -log( get_vision_threshold( static_cast<int>( g->m.ambient_light_at( | |
| pos() ) ) ) / | |
| static_cast<float>( light_level ) ) * | |
| ( 1.0 / LIGHT_TRANSPARENCY_OPEN_AIR ) ); | |
| // int range = log(light_level * LIGHT_AMBIENT_LOW) / LIGHT_TRANSPARENCY_OPEN_AIR; | |
| // Clamp to [1, sight_max]. | |
| return std::max( 1, std::min( range, sight_max ) ); | |
| } | |
| int player::unimpaired_range() const | |
| { | |
| return std::min( sight_max, 60 ); | |
| } | |
| bool player::overmap_los( const tripoint &omt, int sight_points ) | |
| { | |
| const tripoint ompos = global_omt_location(); | |
| if( omt.x < ompos.x - sight_points || omt.x > ompos.x + sight_points || | |
| omt.y < ompos.y - sight_points || omt.y > ompos.y + sight_points ) { | |
| // Outside maximum sight range | |
| return false; | |
| } | |
| const std::vector<tripoint> line = line_to( ompos, omt, 0, 0 ); | |
| for( size_t i = 0; i < line.size() && sight_points >= 0; i++ ) { | |
| const tripoint &pt = line[i]; | |
| const oter_id &ter = overmap_buffer.ter( pt ); | |
| sight_points -= static_cast<int>( ter->get_see_cost() ); | |
| if( sight_points < 0 ) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| int player::overmap_sight_range( int light_level ) const | |
| { | |
| int sight = sight_range( light_level ); | |
| if( sight < SEEX ) { | |
| return 0; | |
| } | |
| if( sight <= SEEX * 4 ) { | |
| return ( sight / ( SEEX / 2 ) ); | |
| } | |
| sight = 6; | |
| // The higher your perception, the farther you can see. | |
| sight += static_cast<int>( get_per() / 2 ); | |
| // The higher up you are, the farther you can see. | |
| sight += std::max( 0, posz() ) * 2; | |
| // Mutations like Scout and Topographagnosia affect how far you can see. | |
| sight += Character::mutation_value( "overmap_sight" ); | |
| float multiplier = Character::mutation_value( "overmap_multiplier" ); | |
| // Binoculars double your sight range. | |
| const bool has_optic = ( has_item_with_flag( "ZOOM" ) || has_bionic( bio_eye_optic ) ); | |
| if( has_optic ) { | |
| multiplier += 1; | |
| } | |
| sight = round( sight * multiplier ); | |
| return std::max( sight, 3 ); | |
| } | |
| #define MAX_CLAIRVOYANCE 40 | |
| int player::clairvoyance() const | |
| { | |
| if( vision_mode_cache[VISION_CLAIRVOYANCE_SUPER] ) { | |
| return MAX_CLAIRVOYANCE; | |
| } | |
| if( vision_mode_cache[VISION_CLAIRVOYANCE_PLUS] ) { | |
| return 8; | |
| } | |
| if( vision_mode_cache[VISION_CLAIRVOYANCE] ) { | |
| return 3; | |
| } | |
| return 0; | |
| } | |
| bool player::sight_impaired() const | |
| { | |
| return ( ( ( has_effect( effect_boomered ) || has_effect( effect_no_sight ) || | |
| has_effect( effect_darkness ) ) && | |
| ( !( has_trait( trait_PER_SLIME_OK ) ) ) ) || | |
| ( underwater && !has_bionic( bio_membrane ) && !has_trait( trait_MEMBRANE ) && | |
| !worn_with_flag( "SWIM_GOGGLES" ) && !has_trait( trait_PER_SLIME_OK ) && | |
| !has_trait( trait_CEPH_EYES ) && !has_trait( trait_SEESLEEP ) ) || | |
| ( ( has_trait( trait_MYOPIC ) || has_trait( trait_URSINE_EYE ) ) && | |
| !worn_with_flag( "FIX_NEARSIGHT" ) && | |
| !has_effect( effect_contacts ) && | |
| !has_bionic( bio_eye_optic ) ) || | |
| has_trait( trait_PER_SLIME ) ); | |
| } | |
| bool player::has_two_arms() const | |
| { | |
| // If you've got a blaster arm, low hp arm, or you're inside a shell then you don't have two | |
| // arms to use. | |
| return !( ( has_bionic( bio_blaster ) || hp_cur[hp_arm_l] < hp_max[hp_arm_l] * 0.125 || | |
| hp_cur[hp_arm_r] < hp_max[hp_arm_r] * 0.125 ) || has_active_mutation( trait_id( "SHELL2" ) ) ); | |
| } | |
| bool player::avoid_trap( const tripoint &pos, const trap &tr ) const | |
| { | |
| /** @EFFECT_DEX increases chance to avoid traps */ | |
| /** @EFFECT_DODGE increases chance to avoid traps */ | |
| int myroll = dice( 3, dex_cur + get_skill_level( skill_dodge ) * 1.5 ); | |
| int traproll; | |
| if( tr.can_see( pos, *this ) ) { | |
| traproll = dice( 3, tr.get_avoidance() ); | |
| } else { | |
| traproll = dice( 6, tr.get_avoidance() ); | |
| } | |
| if( has_trait( trait_LIGHTSTEP ) ) { | |
| myroll += dice( 2, 6 ); | |
| } | |
| if( has_trait( trait_CLUMSY ) ) { | |
| myroll -= dice( 2, 6 ); | |
| } | |
| return myroll >= traproll; | |
| } | |
| bool player::has_alarm_clock() const | |
| { | |
| return ( has_item_with_flag( "ALARMCLOCK", true ) || | |
| ( g->m.veh_at( pos() ) && | |
| !empty( g->m.veh_at( pos() )->vehicle().get_avail_parts( "ALARMCLOCK" ) ) ) || | |
| has_bionic( bio_watch ) ); | |
| } | |
| bool player::has_watch() const | |
| { | |
| return ( has_item_with_flag( "WATCH", true ) || | |
| ( g->m.veh_at( pos() ) && | |
| !empty( g->m.veh_at( pos() )->vehicle().get_avail_parts( "WATCH" ) ) ) || | |
| has_bionic( bio_watch ) ); | |
| } | |
| void player::pause() | |
| { | |
| moves = 0; | |
| recoil = MAX_RECOIL; | |
| // Train swimming if underwater | |
| if( !in_vehicle ) { | |
| if( underwater ) { | |
| practice( skill_swimming, 1 ); | |
| drench( 100, { { | |
| bp_leg_l, bp_leg_r, bp_torso, bp_arm_l, | |
| bp_arm_r, bp_head, bp_eyes, bp_mouth, | |
| bp_foot_l, bp_foot_r, bp_hand_l, bp_hand_r | |
| } | |
| }, true ); | |
| } else if( g->m.has_flag( TFLAG_DEEP_WATER, pos() ) ) { | |
| practice( skill_swimming, 1 ); | |
| // Same as above, except no head/eyes/mouth | |
| drench( 100, { { | |
| bp_leg_l, bp_leg_r, bp_torso, bp_arm_l, | |
| bp_arm_r, bp_foot_l, bp_foot_r, bp_hand_l, | |
| bp_hand_r | |
| } | |
| }, true ); | |
| } else if( g->m.has_flag( "SWIMMABLE", pos() ) ) { | |
| drench( 40, { { bp_foot_l, bp_foot_r, bp_leg_l, bp_leg_r } }, false ); | |
| } | |
| } | |
| // Try to put out clothing/hair fire | |
| if( has_effect( effect_onfire ) ) { | |
| time_duration total_removed = 0_turns; | |
| time_duration total_left = 0_turns; | |
| bool on_ground = has_effect( effect_downed ); | |
| for( const body_part bp : all_body_parts ) { | |
| effect &eff = get_effect( effect_onfire, bp ); | |
| if( eff.is_null() ) { | |
| continue; | |
| } | |
| // TODO: Tools and skills | |
| total_left += eff.get_duration(); | |
| // Being on the ground will smother the fire much faster because you can roll | |
| const time_duration dur_removed = on_ground ? eff.get_duration() / 2 + 2_turns : 1_turns; | |
| eff.mod_duration( -dur_removed ); | |
| total_removed += dur_removed; | |
| } | |
| // Don't drop on the ground when the ground is on fire | |
| if( total_left > 1_minutes && !is_dangerous_fields( g->m.field_at( pos() ) ) ) { | |
| add_effect( effect_downed, 2_turns, num_bp, false, 0, true ); | |
| add_msg_player_or_npc( m_warning, | |
| _( "You roll on the ground, trying to smother the fire!" ), | |
| _( "<npcname> rolls on the ground!" ) ); | |
| } else if( total_removed > 0_turns ) { | |
| add_msg_player_or_npc( m_warning, | |
| _( "You attempt to put out the fire on you!" ), | |
| _( "<npcname> attempts to put out the fire on them!" ) ); | |
| } | |
| } | |
| if( is_npc() ) { | |
| // The stuff below doesn't apply to NPCs | |
| // search_surroundings should eventually do, though | |
| return; | |
| } | |
| if( in_vehicle && one_in( 8 ) ) { | |
| VehicleList vehs = g->m.get_vehicles(); | |
| vehicle *veh = nullptr; | |
| for( auto &v : vehs ) { | |
| veh = v.v; | |
| if( veh && veh->is_moving() && veh->player_in_control( *this ) ) { | |
| double exp_temp = 1 + veh->total_mass() / 400.0_kilogram + | |
| std::abs( veh->velocity / 3200.0 ); | |
| int experience = static_cast<int>( exp_temp ); | |
| if( exp_temp - experience > 0 && x_in_y( exp_temp - experience, 1.0 ) ) { | |
| experience++; | |
| } | |
| practice( skill_id( "driving" ), experience ); | |
| break; | |
| } | |
| } | |
| } | |
| search_surroundings(); | |
| } | |
| void player::set_movement_mode( const player_movemode new_mode ) | |
| { | |
| switch( new_mode ) { | |
| case PMM_WALK: { | |
| if( has_effect( effect_riding ) ) { | |
| add_msg( _( "You nudge your steed to a steady trot." ) ); | |
| } else { | |
| add_msg( _( "You start walking." ) ); | |
| } | |
| break; | |
| } | |
| case PMM_RUN: { | |
| if( stamina > 0 && !has_effect( effect_winded ) ) { | |
| if( is_hauling() ) { | |
| stop_hauling(); | |
| } | |
| if( has_effect( effect_riding ) ) { | |
| add_msg( _( "You spur your steed into a gallop." ) ); | |
| } else { | |
| add_msg( _( "You start running." ) ); | |
| } | |
| } else { | |
| if( has_effect( effect_riding ) ) { | |
| add_msg( m_bad, _( "Your steed is too tired to go faster." ) ); | |
| } else { | |
| add_msg( m_bad, _( "You're too tired to run." ) ); | |
| } | |
| } | |
| break; | |
| } | |
| case PMM_CROUCH: { | |
| if( has_effect( effect_riding ) ) { | |
| add_msg( _( "You slow your steed to a walk." ) ); | |
| } else { | |
| add_msg( _( "You start crouching." ) ); | |
| } | |
| break; | |
| } | |
| default: { | |
| return; | |
| } | |
| } | |
| move_mode = new_mode; | |
| } | |
| bool player::movement_mode_is( const player_movemode mode ) const | |
| { | |
| return move_mode == mode; | |
| } | |
| void player::toggle_run_mode() | |
| { | |
| if( move_mode == PMM_RUN ) { | |
| set_movement_mode( PMM_WALK ); | |
| } else { | |
| set_movement_mode( PMM_RUN ); | |
| } | |
| } | |
| void player::toggle_crouch_mode() | |
| { | |
| if( move_mode == PMM_CROUCH ) { | |
| set_movement_mode( PMM_WALK ); | |
| } else { | |
| set_movement_mode( PMM_CROUCH ); | |
| } | |
| } | |
| void player::reset_move_mode() | |
| { | |
| if( move_mode != PMM_WALK ) { | |
| set_movement_mode( PMM_WALK ); | |
| } | |
| } | |
| void player::cycle_move_mode() | |
| { | |
| unsigned char as_uchar = static_cast<unsigned char>( move_mode ); | |
| as_uchar = ( as_uchar + 1 + PMM_COUNT ) % PMM_COUNT; | |
| set_movement_mode( static_cast<player_movemode>( as_uchar ) ); | |
| } | |
| void player::search_surroundings() | |
| { | |
| if( controlling_vehicle ) { | |
| return; | |
| } | |
| // Search for traps in a larger area than before because this is the only | |
| // way we can "find" traps that aren't marked as visible. | |
| // Detection formula takes care of likelihood of seeing within this range. | |
| for( const tripoint &tp : g->m.points_in_radius( pos(), 5 ) ) { | |
| const trap &tr = g->m.tr_at( tp ); | |
| if( tr.is_null() || tp == pos() ) { | |
| continue; | |
| } | |
| if( has_active_bionic( bio_ground_sonar ) && !knows_trap( tp ) && | |
| ( tr.loadid == tr_beartrap_buried || | |
| tr.loadid == tr_landmine_buried || tr.loadid == tr_sinkhole ) ) { | |
| const std::string direction = direction_name( direction_from( pos(), tp ) ); | |
| add_msg_if_player( m_warning, _( "Your ground sonar detected a %1$s to the %2$s!" ), | |
| tr.name(), direction ); | |
| add_known_trap( tp, tr ); | |
| } | |
| if( !sees( tp ) ) { | |
| continue; | |
| } | |
| if( tr.is_always_invisible() || tr.can_see( tp, *this ) ) { | |
| // Already seen, or can never be seen | |
| continue; | |
| } | |
| // Chance to detect traps we haven't yet seen. | |
| if( tr.detect_trap( tp, *this ) ) { | |
| if( tr.get_visibility() > 0 ) { | |
| // Only bug player about traps that aren't trivial to spot. | |
| const std::string direction = direction_name( | |
| direction_from( pos(), tp ) ); | |
| add_msg_if_player( _( "You've spotted a %1$s to the %2$s!" ), | |
| tr.name(), direction ); | |
| } | |
| add_known_trap( tp, tr ); | |
| } | |
| } | |
| } | |
| int player::read_speed( bool return_stat_effect ) const | |
| { | |
| // Stat window shows stat effects on based on current stat | |
| const int intel = get_int(); | |
| /** @EFFECT_INT increases reading speed */ | |
| int ret = to_moves<int>( 1_minutes ) - to_moves<int>( 3_seconds ) * ( intel - 8 ); | |
| if( has_bionic( afs_bio_linguistic_coprocessor ) ) { // Aftershock | |
| ret *= .85; | |
| } | |
| if( has_trait( trait_FASTREADER ) ) { | |
| ret *= .8; | |
| } | |
| if( has_trait( trait_PROF_DICEMASTER ) ) { | |
| ret *= .9; | |
| } | |
| if( has_trait( trait_SLOWREADER ) ) { | |
| ret *= 1.3; | |
| } | |
| if( ret < to_moves<int>( 1_seconds ) ) { | |
| ret = to_moves<int>( 1_seconds ); | |
| } | |
| // return_stat_effect actually matters here | |
| return return_stat_effect ? ret : ret / 10; | |
| } | |
| int player::rust_rate( bool return_stat_effect ) const | |
| { | |
| if( get_option<std::string>( "SKILL_RUST" ) == "off" ) { | |
| return 0; | |
| } | |
| // Stat window shows stat effects on based on current stat | |
| int intel = get_int(); | |
| /** @EFFECT_INT reduces skill rust */ | |
| int ret = ( ( get_option<std::string>( "SKILL_RUST" ) == "vanilla" || | |
| get_option<std::string>( "SKILL_RUST" ) == "capped" ) ? 500 : 500 - 35 * ( intel - 8 ) ); | |
| ret *= mutation_value( "skill_rust_multiplier" ); | |
| if( ret < 0 ) { | |
| ret = 0; | |
| } | |
| // return_stat_effect actually matters here | |
| return ( return_stat_effect ? ret : ret / 10 ); | |
| } | |
| int player::talk_skill() const | |
| { | |
| /** @EFFECT_INT slightly increases talking skill */ | |
| /** @EFFECT_PER slightly increases talking skill */ | |
| /** @EFFECT_SPEECH increases talking skill */ | |
| int ret = get_int() + get_per() + get_skill_level( skill_id( "speech" ) ) * 3; | |
| return ret; | |
| } | |
| int player::intimidation() const | |
| { | |
| /** @EFFECT_STR increases intimidation factor */ | |
| int ret = get_str() * 2; | |
| if( weapon.is_gun() ) { | |
| ret += 10; | |
| } | |
| if( weapon.damage_melee( DT_BASH ) >= 12 || | |
| weapon.damage_melee( DT_CUT ) >= 12 || | |
| weapon.damage_melee( DT_STAB ) >= 12 ) { | |
| ret += 5; | |
| } | |
| if( stim > 20 ) { | |
| ret += 2; | |
| } | |
| if( has_effect( effect_drunk ) ) { | |
| ret -= 4; | |
| } | |
| return ret; | |
| } | |
| bool player::is_dead_state() const | |
| { | |
| return hp_cur[hp_head] <= 0 || hp_cur[hp_torso] <= 0; | |
| } | |
| void player::on_dodge( Creature *source, float difficulty ) | |
| { | |
| static const matec_id tec_none( "tec_none" ); | |
| // Each avoided hit consumes an available dodge | |
| // When no more available we are likely to fail player::dodge_roll | |
| dodges_left--; | |
| // dodging throws of our aim unless we are either skilled at dodging or using a small weapon | |
| if( is_armed() && weapon.is_gun() ) { | |
| recoil += std::max( weapon.volume() / 250_ml - get_skill_level( skill_dodge ), 0 ) * rng( 0, 100 ); | |
| recoil = std::min( MAX_RECOIL, recoil ); | |
| } | |
| // Even if we are not to train still call practice to prevent skill rust | |
| difficulty = std::max( difficulty, 0.0f ); | |
| practice( skill_dodge, difficulty * 2, difficulty ); | |
| ma_ondodge_effects(); | |
| // For adjacent attackers check for techniques usable upon successful dodge | |
| if( source && square_dist( pos(), source->pos() ) == 1 ) { | |
| matec_id tec = pick_technique( *source, used_weapon(), false, true, false ); | |
| if( tec != tec_none ) { | |
| melee_attack( *source, false, tec ); | |
| } | |
| } | |
| } | |
| void player::on_hit( Creature *source, body_part bp_hit, | |
| float /*difficulty*/, dealt_projectile_attack const *const proj ) | |
| { | |
| check_dead_state(); | |
| if( source == nullptr || proj != nullptr ) { | |
| return; | |
| } | |
| bool u_see = g->u.sees( *this ); | |
| if( has_active_bionic( bionic_id( "bio_ods" ) ) && power_level > 5 ) { | |
| if( is_player() ) { | |
| add_msg( m_good, _( "Your offensive defense system shocks %s in mid-attack!" ), | |
| source->disp_name() ); | |
| } else if( u_see ) { | |
| add_msg( _( "%1$s's offensive defense system shocks %2$s in mid-attack!" ), | |
| disp_name(), | |
| source->disp_name() ); | |
| } | |
| int shock = rng( 1, 4 ); | |
| charge_power( -shock ); | |
| damage_instance ods_shock_damage; | |
| ods_shock_damage.add_damage( DT_ELECTRIC, shock * 5 ); | |
| // Should hit body part used for attack | |
| source->deal_damage( this, bp_torso, ods_shock_damage ); | |
| } | |
| if( !wearing_something_on( bp_hit ) && | |
| ( has_trait( trait_SPINES ) || has_trait( trait_QUILLS ) ) ) { | |
| int spine = rng( 1, has_trait( trait_QUILLS ) ? 20 : 8 ); | |
| if( !is_player() ) { | |
| if( u_see ) { | |
| add_msg( _( "%1$s's %2$s puncture %3$s in mid-attack!" ), name, | |
| ( has_trait( trait_QUILLS ) ? _( "quills" ) : _( "spines" ) ), | |
| source->disp_name() ); | |
| } | |
| } else { | |
| add_msg( m_good, _( "Your %1$s puncture %2$s in mid-attack!" ), | |
| ( has_trait( trait_QUILLS ) ? _( "quills" ) : _( "spines" ) ), | |
| source->disp_name() ); | |
| } | |
| damage_instance spine_damage; | |
| spine_damage.add_damage( DT_STAB, spine ); | |
| source->deal_damage( this, bp_torso, spine_damage ); | |
| } | |
| if( ( !( wearing_something_on( bp_hit ) ) ) && ( has_trait( trait_THORNS ) ) && | |
| ( !( source->has_weapon() ) ) ) { | |
| if( !is_player() ) { | |
| if( u_see ) { | |
| add_msg( _( "%1$s's %2$s scrape %3$s in mid-attack!" ), name, | |
| _( "thorns" ), source->disp_name() ); | |
| } | |
| } else { | |
| add_msg( m_good, _( "Your thorns scrape %s in mid-attack!" ), source->disp_name() ); | |
| } | |
| int thorn = rng( 1, 4 ); | |
| damage_instance thorn_damage; | |
| thorn_damage.add_damage( DT_CUT, thorn ); | |
| // In general, critters don't have separate limbs | |
| // so safer to target the torso | |
| source->deal_damage( this, bp_torso, thorn_damage ); | |
| } | |
| if( ( !( wearing_something_on( bp_hit ) ) ) && ( has_trait( trait_CF_HAIR ) ) ) { | |
| if( !is_player() ) { | |
| if( u_see ) { | |
| add_msg( _( "%1$s gets a load of %2$s's %3$s stuck in!" ), source->disp_name(), | |
| name, ( _( "hair" ) ) ); | |
| } | |
| } else { | |
| add_msg( m_good, _( "Your hairs detach into %s!" ), source->disp_name() ); | |
| } | |
| source->add_effect( effect_stunned, 2_turns ); | |
| if( one_in( 3 ) ) { // In the eyes! | |
| source->add_effect( effect_blind, 2_turns ); | |
| } | |
| } | |
| if( worn_with_flag( "REQUIRES_BALANCE" ) && !has_effect( effect_downed ) ) { | |
| int rolls = 4; | |
| if( worn_with_flag( "ROLLER_ONE" ) ) { | |
| rolls += 2; | |
| } | |
| if( has_trait( trait_PROF_SKATER ) ) { | |
| rolls--; | |
| } | |
| if( has_trait( trait_DEFT ) ) { | |
| rolls--; | |
| } | |
| if( has_trait( trait_CLUMSY ) ) { | |
| rolls++; | |
| } | |
| if( stability_roll() < dice( rolls, 10 ) ) { | |
| if( !is_player() ) { | |
| if( u_see ) { | |
| add_msg( _( "%1$s loses their balance while being hit!" ), name ); | |
| } | |
| } else { | |
| add_msg( m_bad, _( "You lose your balance while being hit!" ) ); | |
| } | |
| add_effect( effect_downed, 2_turns ); | |
| } | |
| } | |
| } | |
| int player::get_lift_assist() const | |
| { | |
| int result = 0; | |
| const std::vector<npc *> helpers = g->u.get_crafting_helpers(); | |
| for( const npc *np : helpers ) { | |
| result += np->get_str(); | |
| } | |
| return result; | |
| } | |
| int player::get_num_crafting_helpers( int max ) const | |
| { | |
| std::vector<npc *> helpers = g->u.get_crafting_helpers(); | |
| return std::min( max, static_cast<int>( helpers.size() ) ); | |
| } | |
| void player::on_hurt( Creature *source, bool disturb /*= true*/ ) | |
| { | |
| if( has_trait( trait_ADRENALINE ) && !has_effect( effect_adrenaline ) && | |
| ( hp_cur[hp_head] < 25 || hp_cur[hp_torso] < 15 ) ) { | |
| add_effect( effect_adrenaline, 20_minutes ); | |
| } | |
| if( disturb ) { | |
| if( has_effect( effect_sleep ) && !has_effect( effect_narcosis ) ) { | |
| wake_up(); | |
| } | |
| if( !is_npc() && !has_effect( effect_narcosis ) ) { | |
| if( source != nullptr ) { | |
| g->cancel_activity_or_ignore_query( distraction_type::attacked, | |
| string_format( _( "You were attacked by %s!" ), | |
| source->disp_name() ) ); | |
| } else { | |
| g->cancel_activity_or_ignore_query( distraction_type::attacked, _( "You were hurt!" ) ); | |
| } | |
| } | |
| } | |
| if( is_dead_state() ) { | |
| set_killer( source ); | |
| } | |
| } | |
| bool player::immune_to( body_part bp, damage_unit dam ) const | |
| { | |
| if( has_trait( trait_DEBUG_NODMG ) || is_immune_damage( dam.type ) ) { | |
| return true; | |
| } | |
| passive_absorb_hit( bp, dam ); | |
| for( const item &cloth : worn ) { | |
| if( cloth.get_coverage() == 100 && cloth.covers( bp ) ) { | |
| cloth.mitigate_damage( dam ); | |
| } | |
| } | |
| return dam.amount <= 0; | |
| } | |
| dealt_damage_instance player::deal_damage( Creature *source, body_part bp, | |
| const damage_instance &d ) | |
| { | |
| if( has_trait( trait_DEBUG_NODMG ) ) { | |
| return dealt_damage_instance(); | |
| } | |
| //damage applied here | |
| dealt_damage_instance dealt_dams = Creature::deal_damage( source, bp, d ); | |
| //block reduction should be by applied this point | |
| int dam = dealt_dams.total_damage(); | |
| // TODO: Pre or post blit hit tile onto "this"'s location here | |
| if( dam > 0 && g->u.sees( pos() ) ) { | |
| g->draw_hit_player( *this, dam ); | |
| if( is_player() && source ) { | |
| //monster hits player melee | |
| SCT.add( posx(), posy(), | |
| direction_from( 0, 0, posx() - source->posx(), posy() - source->posy() ), | |
| get_hp_bar( dam, get_hp_max( player::bp_to_hp( bp ) ) ).first, m_bad, | |
| body_part_name( bp ), m_neutral ); | |
| } | |
| } | |
| // handle snake artifacts | |
| if( has_artifact_with( AEP_SNAKES ) && dam >= 6 ) { | |
| int snakes = dam / 6; | |
| std::vector<tripoint> valid; | |
| for( const tripoint &dest : g->m.points_in_radius( pos(), 1 ) ) { | |
| if( g->is_empty( dest ) ) { | |
| valid.push_back( dest ); | |
| } | |
| } | |
| if( snakes > static_cast<int>( valid.size() ) ) { | |
| snakes = static_cast<int>( valid.size() ); | |
| } | |
| if( snakes == 1 ) { | |
| add_msg( m_warning, _( "A snake sprouts from your body!" ) ); | |
| } else if( snakes >= 2 ) { | |
| add_msg( m_warning, _( "Some snakes sprout from your body!" ) ); | |
| } | |
| for( int i = 0; i < snakes && !valid.empty(); i++ ) { | |
| const tripoint target = random_entry_removed( valid ); | |
| if( monster *const snake = g->summon_mon( mon_shadow_snake, target ) ) { | |
| snake->friendly = -1; | |
| } | |
| } | |
| } | |
| // And slimespawners too | |
| if( ( has_trait( trait_SLIMESPAWNER ) ) && ( dam >= 10 ) && one_in( 20 - dam ) ) { | |
| std::vector<tripoint> valid; | |
| for( const tripoint &dest : g->m.points_in_radius( pos(), 1 ) ) { | |
| if( g->is_empty( dest ) ) { | |
| valid.push_back( dest ); | |
| } | |
| } | |
| add_msg( m_warning, _( "Slime is torn from you, and moves on its own!" ) ); | |
| int numslime = 1; | |
| for( int i = 0; i < numslime && !valid.empty(); i++ ) { | |
| const tripoint target = random_entry_removed( valid ); | |
| if( monster *const slime = g->summon_mon( mon_player_blob, target ) ) { | |
| slime->friendly = -1; | |
| } | |
| } | |
| } | |
| //Acid blood effects. | |
| bool u_see = g->u.sees( *this ); | |
| int cut_dam = dealt_dams.type_damage( DT_CUT ); | |
| if( source && has_trait( trait_ACIDBLOOD ) && !one_in( 3 ) && | |
| ( dam >= 4 || cut_dam > 0 ) && ( rl_dist( g->u.pos(), source->pos() ) <= 1 ) ) { | |
| if( is_player() ) { | |
| add_msg( m_good, _( "Your acidic blood splashes %s in mid-attack!" ), | |
| source->disp_name() ); | |
| } else if( u_see ) { | |
| add_msg( _( "%1$s's acidic blood splashes on %2$s in mid-attack!" ), | |
| disp_name(), source->disp_name() ); | |
| } | |
| damage_instance acidblood_damage; | |
| acidblood_damage.add_damage( DT_ACID, rng( 4, 16 ) ); | |
| if( !one_in( 4 ) ) { | |
| source->deal_damage( this, bp_arm_l, acidblood_damage ); | |
| source->deal_damage( this, bp_arm_r, acidblood_damage ); | |
| } else { | |
| source->deal_damage( this, bp_torso, acidblood_damage ); | |
| source->deal_damage( this, bp_head, acidblood_damage ); | |
| } | |
| } | |
| int recoil_mul = 100; | |
| switch( bp ) { | |
| case bp_eyes: | |
| if( dam > 5 || cut_dam > 0 ) { | |
| const time_duration minblind = std::max( 1_turns, 1_turns * ( dam + cut_dam ) / 10 ); | |
| const time_duration maxblind = std::min( 5_turns, 1_turns * ( dam + cut_dam ) / 4 ); | |
| add_effect( effect_blind, rng( minblind, maxblind ) ); | |
| } | |
| break; | |
| case bp_torso: | |
| break; | |
| case bp_hand_l: // Fall through to arms | |
| case bp_arm_l: | |
| // Hit to arms/hands are really bad to our aim | |
| case bp_hand_r: // Fall through to arms | |
| case bp_arm_r: | |
| recoil_mul = 200; | |
| break; | |
| case bp_foot_l: // Fall through to legs | |
| case bp_leg_l: | |
| break; | |
| case bp_foot_r: // Fall through to legs | |
| case bp_leg_r: | |
| break; | |
| case bp_mouth: // Fall through to head damage | |
| case bp_head: | |
| // TODO: Some daze maybe? Move drain? | |
| break; | |
| default: | |
| debugmsg( "Wacky body part hit!" ); | |
| } | |
| // TODO: Scale with damage in a way that makes sense for power armors, plate armor and naked skin. | |
| recoil += recoil_mul * weapon.volume() / 250_ml; | |
| recoil = std::min( MAX_RECOIL, recoil ); | |
| //looks like this should be based off of dealt damages, not d as d has no damage reduction applied. | |
| // Skip all this if the damage isn't from a creature. e.g. an explosion. | |
| if( source != nullptr ) { | |
| if( source->has_flag( MF_GRABS ) && !source->is_hallucination() ) { | |
| /** @EFFECT_DEX increases chance to avoid being grabbed, if DEX>STR */ | |
| /** @EFFECT_STR increases chance to avoid being grabbed, if STR>DEX */ | |
| if( has_grab_break_tec() && get_grab_resist() > 0 && | |
| ( get_dex() > get_str() ? rng( 0, get_dex() ) : rng( 0, get_str() ) ) > | |
| rng( 0, 10 ) ) { | |
| if( has_effect( effect_grabbed ) ) { | |
| add_msg_if_player( m_warning, _( "You are being grabbed by %s, but you bat it away!" ), | |
| source->disp_name() ); | |
| } else { | |
| add_msg_player_or_npc( m_info, _( "You are being grabbed by %s, but you break its grab!" ), | |
| _( "<npcname> are being grabbed by %s, but they break its grab!" ), | |
| source->disp_name() ); | |
| } | |
| } else { | |
| int prev_effect = get_effect_int( effect_grabbed ); | |
| add_effect( effect_grabbed, 2_turns, bp_torso, false, prev_effect + 2 ); | |
| add_msg_player_or_npc( m_bad, _( "You are grabbed by %s!" ), _( "<npcname> is grabbed by %s!" ), | |
| source->disp_name() ); | |
| } | |
| } | |
| } | |
| if( get_option<bool>( "FILTHY_WOUNDS" ) ) { | |
| int sum_cover = 0; | |
| for( const item &i : worn ) { | |
| if( i.covers( bp ) && i.is_filthy() ) { | |
| sum_cover += i.get_coverage(); | |
| } | |
| } | |
| // Chance of infection is damage (with cut and stab x4) * sum of coverage on affected body part, in percent. | |
| // i.e. if the body part has a sum of 100 coverage from filthy clothing, | |
| // each point of damage has a 1% change of causing infection. | |
| if( sum_cover > 0 ) { | |
| const int cut_type_dam = dealt_dams.type_damage( DT_CUT ) + dealt_dams.type_damage( DT_STAB ); | |
| const int combined_dam = dealt_dams.type_damage( DT_BASH ) + ( cut_type_dam * 4 ); | |
| const int infection_chance = ( combined_dam * sum_cover ) / 100; | |
| if( x_in_y( infection_chance, 100 ) ) { | |
| if( has_effect( effect_bite, bp ) ) { | |
| add_effect( effect_bite, 40_minutes, bp, true ); | |
| } else if( has_effect( effect_infected, bp ) ) { | |
| add_effect( effect_infected, 25_minutes, bp, true ); | |
| } else { | |
| add_effect( effect_bite, 1_turns, bp, true ); | |
| } | |
| add_msg_if_player( _( "Filth from your clothing has implanted deep in the wound." ) ); | |
| } | |
| } | |
| } | |
| on_hurt( source ); | |
| return dealt_dams; | |
| } | |
| void player::mod_pain( int npain ) | |
| { | |
| if( npain > 0 ) { | |
| if( has_trait( trait_NOPAIN ) || has_effect( effect_narcosis ) ) { | |
| return; | |
| } | |
| // always increase pain gained by one from these bad mutations | |
| if( has_trait( trait_MOREPAIN ) ) { | |
| npain += std::max( 1, roll_remainder( npain * 0.25 ) ); | |
| } else if( has_trait( trait_MOREPAIN2 ) ) { | |
| npain += std::max( 1, roll_remainder( npain * 0.5 ) ); | |
| } else if( has_trait( trait_MOREPAIN3 ) ) { | |
| npain += std::max( 1, roll_remainder( npain * 1.0 ) ); | |
| } | |
| if( npain > 1 ) { | |
| // if it's 1 it'll just become 0, which is bad | |
| if( has_trait( trait_PAINRESIST_TROGLO ) ) { | |
| npain = roll_remainder( npain * 0.5 ); | |
| } else if( has_trait( trait_PAINRESIST ) ) { | |
| npain = roll_remainder( npain * 0.67 ); | |
| } | |
| } | |
| } | |
| Creature::mod_pain( npain ); | |
| } | |
| void player::set_pain( int npain ) | |
| { | |
| const int prev_pain = get_perceived_pain(); | |
| Creature::set_pain( npain ); | |
| const int cur_pain = get_perceived_pain(); | |
| if( cur_pain != prev_pain ) { | |
| react_to_felt_pain( cur_pain - prev_pain ); | |
| on_stat_change( "perceived_pain", cur_pain ); | |
| } | |
| } | |
| int player::get_perceived_pain() const | |
| { | |
| if( get_effect_int( effect_adrenaline ) > 1 ) { | |
| return 0; | |
| } | |
| return std::max( get_pain() - get_painkiller(), 0 ); | |
| } | |
| void player::mod_painkiller( int npkill ) | |
| { | |
| set_painkiller( pkill + npkill ); | |
| } | |
| void player::set_painkiller( int npkill ) | |
| { | |
| npkill = std::max( npkill, 0 ); | |
| if( pkill != npkill ) { | |
| const int prev_pain = get_perceived_pain(); | |
| pkill = npkill; | |
| on_stat_change( "pkill", pkill ); | |
| const int cur_pain = get_perceived_pain(); | |
| if( cur_pain != prev_pain ) { | |
| react_to_felt_pain( cur_pain - prev_pain ); | |
| on_stat_change( "perceived_pain", cur_pain ); | |
| } | |
| } | |
| } | |
| int player::get_painkiller() const | |
| { | |
| return pkill; | |
| } | |
| void player::react_to_felt_pain( int intensity ) | |
| { | |
| if( intensity <= 0 ) { | |
| return; | |
| } | |
| if( is_player() && intensity >= 2 ) { | |
| g->cancel_activity_or_ignore_query( distraction_type::pain, _( "Ouch, something hurts!" ) ); | |
| } | |
| // Only a large pain burst will actually wake people while sleeping. | |
| if( has_effect( effect_sleep ) && !has_effect( effect_narcosis ) ) { | |
| int pain_thresh = rng( 3, 5 ); | |
| if( has_trait( trait_HEAVYSLEEPER ) ) { | |
| pain_thresh += 2; | |
| } else if( has_trait( trait_HEAVYSLEEPER2 ) ) { | |
| pain_thresh += 5; | |
| } | |
| if( intensity >= pain_thresh ) { | |
| wake_up(); | |
| } | |
| } | |
| } | |
| int player::reduce_healing_effect( const efftype_id &eff_id, int remove_med, body_part hurt ) | |
| { | |
| effect &e = get_effect( eff_id, hurt ); | |
| int intensity = e.get_intensity(); | |
| if( remove_med < intensity ) { | |
| if( eff_id == effect_bandaged ) { | |
| add_msg_if_player( m_bad, _( "Bandages on your %s were damaged!" ), body_part_name( hurt ) ); | |
| } else if( eff_id == effect_disinfected ) { | |
| add_msg_if_player( m_bad, _( "You got some filth on your disinfected %s!" ), | |
| body_part_name( hurt ) ); | |
| } | |
| } else { | |
| if( eff_id == effect_bandaged ) { | |
| add_msg_if_player( m_bad, _( "Bandages on your %s were destroyed!" ), body_part_name( hurt ) ); | |
| } else if( eff_id == effect_disinfected ) { | |
| add_msg_if_player( m_bad, _( "Your %s is no longer disinfected!" ), body_part_name( hurt ) ); | |
| } | |
| } | |
| e.mod_duration( -6_hours * remove_med ); | |
| return intensity; | |
| } | |
| /* | |
| Where damage to player is actually applied to hit body parts | |
| Might be where to put bleed stuff rather than in player::deal_damage() | |
| */ | |
| void player::apply_damage( Creature *source, body_part hurt, int dam, const bool bypass_med ) | |
| { | |
| if( is_dead_state() || has_trait( trait_DEBUG_NODMG ) ) { | |
| // don't do any more damage if we're already dead | |
| // Or if we're debugging and don't want to die | |
| return; | |
| } | |
| hp_part hurtpart = bp_to_hp( hurt ); | |
| if( hurtpart == num_hp_parts ) { | |
| debugmsg( "Wacky body part hurt!" ); | |
| hurtpart = hp_torso; | |
| } | |
| mod_pain( dam / 2 ); | |
| hp_cur[hurtpart] -= dam; | |
| if( hp_cur[hurtpart] < 0 ) { | |
| lifetime_stats.damage_taken += hp_cur[hurtpart]; | |
| hp_cur[hurtpart] = 0; | |
| } | |
| if( hp_cur[hurtpart] <= 0 && ( source == nullptr || !source->is_hallucination() ) ) { | |
| if( has_effect( effect_mending, hurt ) ) { | |
| effect &e = get_effect( effect_mending, hurt ); | |
| float remove_mend = dam / 20.0f; | |
| e.mod_duration( -e.get_max_duration() * remove_mend ); | |
| } | |
| } | |
| lifetime_stats.damage_taken += dam; | |
| if( dam > get_painkiller() ) { | |
| on_hurt( source ); | |
| } | |
| if( !bypass_med ) { | |
| // remove healing effects if damaged | |
| int remove_med = roll_remainder( dam / 5.0f ); | |
| if( remove_med > 0 && has_effect( effect_bandaged, hurt ) ) { | |
| remove_med -= reduce_healing_effect( effect_bandaged, remove_med, hurt ); | |
| } | |
| if( remove_med > 0 && has_effect( effect_disinfected, hurt ) ) { | |
| // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores) | |
| remove_med -= reduce_healing_effect( effect_disinfected, remove_med, hurt ); | |
| } | |
| } | |
| } | |
| void player::heal( body_part healed, int dam ) | |
| { | |
| hp_part healpart; | |
| switch( healed ) { | |
| case bp_eyes: // Fall through to head damage | |
| case bp_mouth: // Fall through to head damage | |
| case bp_head: | |
| healpart = hp_head; | |
| break; | |
| case bp_torso: | |
| healpart = hp_torso; | |
| break; | |
| case bp_hand_l: | |
| // Shouldn't happen, but fall through to arms | |
| debugmsg( "Heal against hands!" ); | |
| /* fallthrough */ | |
| case bp_arm_l: | |
| healpart = hp_arm_l; | |
| break; | |
| case bp_hand_r: | |
| // Shouldn't happen, but fall through to arms | |
| debugmsg( "Heal against hands!" ); | |
| /* fallthrough */ | |
| case bp_arm_r: | |
| healpart = hp_arm_r; | |
| break; | |
| case bp_foot_l: | |
| // Shouldn't happen, but fall through to legs | |
| debugmsg( "Heal against feet!" ); | |
| /* fallthrough */ | |
| case bp_leg_l: | |
| healpart = hp_leg_l; | |
| break; | |
| case bp_foot_r: | |
| // Shouldn't happen, but fall through to legs | |
| debugmsg( "Heal against feet!" ); | |
| /* fallthrough */ | |
| case bp_leg_r: | |
| healpart = hp_leg_r; | |
| break; | |
| default: | |
| debugmsg( "Wacky body part healed!" ); | |
| healpart = hp_torso; | |
| } | |
| heal( healpart, dam ); | |
| } | |
| void player::heal( hp_part healed, int dam ) | |
| { | |
| if( hp_cur[healed] > 0 ) { | |
| hp_cur[healed] += dam; | |
| if( hp_cur[healed] > hp_max[healed] ) { | |
| lifetime_stats.damage_healed -= hp_cur[healed] - hp_max[healed]; | |
| hp_cur[healed] = hp_max[healed]; | |
| } | |
| lifetime_stats.damage_healed += dam; | |
| } | |
| } | |
| void player::healall( int dam ) | |
| { | |
| for( int healed_part = 0; healed_part < num_hp_parts; healed_part++ ) { | |
| heal( static_cast<hp_part>( healed_part ), dam ); | |
| healed_bp( healed_part, dam ); | |
| } | |
| } | |
| void player::hurtall( int dam, Creature *source, bool disturb /*= true*/ ) | |
| { | |
| if( is_dead_state() || has_trait( trait_DEBUG_NODMG ) || dam <= 0 ) { | |
| return; | |
| } | |
| for( int i = 0; i < num_hp_parts; i++ ) { | |
| const hp_part bp = static_cast<hp_part>( i ); | |
| // Don't use apply_damage here or it will annoy the player with 6 queries | |
| hp_cur[bp] -= dam; | |
| lifetime_stats.damage_taken += dam; | |
| if( hp_cur[bp] < 0 ) { | |
| lifetime_stats.damage_taken += hp_cur[bp]; | |
| hp_cur[bp] = 0; | |
| } | |
| } | |
| // Low pain: damage is spread all over the body, so not as painful as 6 hits in one part | |
| mod_pain( dam ); | |
| on_hurt( source, disturb ); | |
| } | |
| int player::hitall( int dam, int vary, Creature *source ) | |
| { | |
| int damage_taken = 0; | |
| for( int i = 0; i < num_hp_parts; i++ ) { | |
| const body_part bp = hp_to_bp( static_cast<hp_part>( i ) ); | |
| int ddam = vary ? dam * rng( 100 - vary, 100 ) / 100 : dam; | |
| int cut = 0; | |
| auto damage = damage_instance::physical( ddam, cut, 0 ); | |
| damage_taken += deal_damage( source, bp, damage ).total_damage(); | |
| } | |
| return damage_taken; | |
| } | |
| float player::fall_damage_mod() const | |
| { | |
| float ret = 1.0f; | |
| // Ability to land properly is 2x as important as dexterity itself | |
| /** @EFFECT_DEX decreases damage from falling */ | |
| /** @EFFECT_DODGE decreases damage from falling */ | |
| float dex_dodge = dex_cur / 2 + get_skill_level( skill_dodge ); | |
| // Penalize for wearing heavy stuff | |
| dex_dodge -= ( ( ( encumb( bp_leg_l ) + encumb( bp_leg_r ) ) / 2 ) + ( encumb( | |
| bp_torso ) / 1 ) ) / 10; | |
| // But prevent it from increasing damage | |
| dex_dodge = std::max( 0.0f, dex_dodge ); | |
| // 100% damage at 0, 75% at 10, 50% at 20 and so on | |
| ret *= ( 100.0f - ( dex_dodge * 4.0f ) ) / 100.0f; | |
| if( has_trait( trait_PARKOUR ) ) { | |
| ret *= 2.0f / 3.0f; | |
| } | |
| // TODO: Bonus for Judo, mutations. Penalty for heavy weight (including mutations) | |
| return std::max( 0.0f, ret ); | |
| } | |
| // force is maximum damage to hp before scaling | |
| int player::impact( const int force, const tripoint &p ) | |
| { | |
| // Falls over ~30m are fatal more often than not | |
| // But that would be quite a lot considering 21 z-levels in game | |
| // so let's assume 1 z-level is comparable to 30 force | |
| if( force <= 0 ) { | |
| return force; | |
| } | |
| // Damage modifier (post armor) | |
| float mod = 1.0f; | |
| int effective_force = force; | |
| int cut = 0; | |
| // Percentage armor penetration - armor won't help much here | |
| // TODO: Make cushioned items like bike helmets help more | |
| float armor_eff = 1.0f; | |
| // Shock Absorber CBM heavily reduces damage | |
| const bool shock_absorbers = has_active_bionic( bionic_id( "bio_shock_absorber" ) ); | |
| // Being slammed against things rather than landing means we can't | |
| // control the impact as well | |
| const bool slam = p != pos(); | |
| std::string target_name = "a swarm of bugs"; | |
| Creature *critter = g->critter_at( p ); | |
| if( critter != this && critter != nullptr ) { | |
| target_name = critter->disp_name(); | |
| // Slamming into creatures and NPCs | |
| // TODO: Handle spikes/horns and hard materials | |
| armor_eff = 0.5f; // 2x as much as with the ground | |
| // TODO: Modify based on something? | |
| mod = 1.0f; | |
| effective_force = force; | |
| } else if( const optional_vpart_position vp = g->m.veh_at( p ) ) { | |
| // Slamming into vehicles | |
| // TODO: Integrate it with vehicle collision function somehow | |
| target_name = vp->vehicle().disp_name(); | |
| if( vp.part_with_feature( "SHARP", true ) ) { | |
| // Now we're actually getting impaled | |
| cut = force; // Lots of fun | |
| } | |
| mod = slam ? 1.0f : fall_damage_mod(); | |
| armor_eff = 0.25f; // Not much | |
| if( !slam && vp->part_with_feature( "ROOF", true ) ) { | |
| // Roof offers better landing than frame or pavement | |
| effective_force /= 2; // TODO: Make this not happen with heavy duty/plated roof | |
| } | |
| } else { | |
| // Slamming into terrain/furniture | |
| target_name = g->m.disp_name( p ); | |
| int hard_ground = g->m.has_flag( TFLAG_DIGGABLE, p ) ? 0 : 3; | |
| armor_eff = 0.25f; // Not much | |
| // Get cut by stuff | |
| // This isn't impalement on metal wreckage, more like flying through a closed window | |
| cut = g->m.has_flag( TFLAG_SHARP, p ) ? 5 : 0; | |
| effective_force = force + hard_ground; | |
| mod = slam ? 1.0f : fall_damage_mod(); | |
| if( g->m.has_furn( p ) ) { | |
| // TODO: Make furniture matter | |
| } else if( g->m.has_flag( TFLAG_SWIMMABLE, p ) ) { | |
| // TODO: Some formula of swimming | |
| effective_force /= 4; | |
| } | |
| } | |
| // Rescale for huge force | |
| // At >30 force, proper landing is impossible and armor helps way less | |
| if( effective_force > 30 ) { | |
| // Armor simply helps way less | |
| armor_eff *= 30.0f / effective_force; | |
| if( mod < 1.0f ) { | |
| // Everything past 30 damage gets a worse modifier | |
| const float scaled_mod = std::pow( mod, 30.0f / effective_force ); | |
| const float scaled_damage = ( 30.0f * mod ) + scaled_mod * ( effective_force - 30.0f ); | |
| mod = scaled_damage / effective_force; | |
| } | |
| } | |
| if( !slam && mod < 1.0f && mod * force < 5 ) { | |
| // Perfect landing, no damage (regardless of armor) | |
| add_msg_if_player( m_warning, _( "You land on %s." ), target_name ); | |
| return 0; | |
| } | |
| // Shock absorbers kick in only when they need to, so if our other protections fail, fall back on them | |
| if( shock_absorbers ) { | |
| effective_force -= 15; // Provide a flat reduction to force | |
| if( mod > 0.25f ) { | |
| mod = 0.25f; // And provide a 75% reduction against that force if we don't have it already | |
| } | |
| if( effective_force < 0 ) { | |
| effective_force = 0; | |
| } | |
| } | |
| int total_dealt = 0; | |
| if( mod * effective_force >= 5 ) { | |
| for( int i = 0; i < num_hp_parts; i++ ) { | |
| const body_part bp = hp_to_bp( static_cast<hp_part>( i ) ); | |
| const int bash = effective_force * rng( 60, 100 ) / 100; | |
| damage_instance di; | |
| di.add_damage( DT_BASH, bash, 0, armor_eff, mod ); | |
| // No good way to land on sharp stuff, so here modifier == 1.0f | |
| di.add_damage( DT_CUT, cut, 0, armor_eff, 1.0f ); | |
| total_dealt += deal_damage( nullptr, bp, di ).total_damage(); | |
| } | |
| } | |
| if( total_dealt > 0 && is_player() ) { | |
| // "You slam against the dirt" is fine | |
| add_msg( m_bad, _( "You are slammed against %s for %d damage." ), | |
| target_name, total_dealt ); | |
| } else if( is_player() && shock_absorbers ) { | |
| add_msg( m_bad, _( "You are slammed against %s!" ), | |
| target_name, total_dealt ); | |
| add_msg( m_good, _( "...but your shock absorbers negate the damage!" ) ); | |
| } else if( slam ) { | |
| // Only print this line if it is a slam and not a landing | |
| // Non-players should only get this one: player doesn't know how much damage was dealt | |
| // and landing messages for each slammed creature would be too much | |
| add_msg_player_or_npc( m_bad, | |
| _( "You are slammed against %s." ), | |
| _( "<npcname> is slammed against %s." ), | |
| target_name ); | |
| } else { | |
| // No landing message for NPCs | |
| add_msg_if_player( m_warning, _( "You land on %s." ), target_name ); | |
| } | |
| if( x_in_y( mod, 1.0f ) ) { | |
| add_effect( effect_downed, rng( 1_turns, 1_turns + mod * 3_turns ) ); | |
| } | |
| return total_dealt; | |
| } | |
| void player::knock_back_from( const tripoint &p ) | |
| { | |
| if( p == pos() ) { | |
| return; | |
| } | |
| tripoint to = pos(); | |
| const tripoint dp = pos() - p; | |
| to.x += sgn( dp.x ); | |
| to.y += sgn( dp.y ); | |
| // First, see if we hit a monster | |
| if( monster *const critter = g->critter_at<monster>( to ) ) { | |
| deal_damage( critter, bp_torso, damage_instance( DT_BASH, critter->type->size ) ); | |
| add_effect( effect_stunned, 1_turns ); | |
| /** @EFFECT_STR_MAX allows knocked back player to knock back, damage, stun some monsters */ | |
| if( ( str_max - 6 ) / 4 > critter->type->size ) { | |
| critter->knock_back_from( pos() ); // Chain reaction! | |
| critter->apply_damage( this, bp_torso, ( str_max - 6 ) / 4 ); | |
| critter->add_effect( effect_stunned, 1_turns ); | |
| } else if( ( str_max - 6 ) / 4 == critter->type->size ) { | |
| critter->apply_damage( this, bp_torso, ( str_max - 6 ) / 4 ); | |
| critter->add_effect( effect_stunned, 1_turns ); | |
| } | |
| critter->check_dead_state(); | |
| add_msg_player_or_npc( _( "You bounce off a %s!" ), _( "<npcname> bounces off a %s!" ), | |
| critter->name() ); | |
| return; | |
| } | |
| if( npc *const np = g->critter_at<npc>( to ) ) { | |
| deal_damage( np, bp_torso, damage_instance( DT_BASH, np->get_size() ) ); | |
| add_effect( effect_stunned, 1_turns ); | |
| np->deal_damage( this, bp_torso, damage_instance( DT_BASH, 3 ) ); | |
| add_msg_player_or_npc( _( "You bounce off %s!" ), _( "<npcname> bounces off %s!" ), | |
| np->name ); | |
| np->check_dead_state(); | |
| return; | |
| } | |
| // If we're still in the function at this point, we're actually moving a tile! | |
| if( g->m.has_flag( "LIQUID", to ) && g->m.has_flag( TFLAG_DEEP_WATER, to ) ) { | |
| if( !is_npc() ) { | |
| avatar_action::swim( g->m, g->u, to ); | |
| } | |
| // TODO: NPCs can't swim! | |
| } else if( g->m.impassable( to ) ) { // Wait, it's a wall | |
| // It's some kind of wall. | |
| apply_damage( nullptr, bp_torso, | |
| 3 ); // TODO: who knocked us back? Maybe that creature should be the source of the damage? | |
| add_effect( effect_stunned, 2_turns ); | |
| add_msg_player_or_npc( _( "You bounce off a %s!" ), _( "<npcname> bounces off a %s!" ), | |
| g->m.obstacle_name( to ) ); | |
| } else { // It's no wall | |
| setpos( to ); | |
| } | |
| } | |
| int player::hp_percentage() const | |
| { | |
| int total_cur = 0; | |
| int total_max = 0; | |
| // Head and torso HP are weighted 3x and 2x, respectively | |
| total_cur = hp_cur[hp_head] * 3 + hp_cur[hp_torso] * 2; | |
| total_max = hp_max[hp_head] * 3 + hp_max[hp_torso] * 2; | |
| for( int i = hp_arm_l; i < num_hp_parts; i++ ) { | |
| total_cur += hp_cur[i]; | |
| total_max += hp_max[i]; | |
| } | |
| return ( 100 * total_cur ) / total_max; | |
| } | |
| // Returns the number of multiples of tick_length we would "pass" on our way `from` to `to` | |
| // For example, if `tick_length` is 1 hour, then going from 0:59 to 1:01 should return 1 | |
| inline int ticks_between( const time_point &from, const time_point &to, | |
| const time_duration &tick_length ) | |
| { | |
| return ( to_turn<int>( to ) / to_turns<int>( tick_length ) ) - ( to_turn<int> | |
| ( from ) / to_turns<int>( tick_length ) ); | |
| } | |
| void player::update_body() | |
| { | |
| update_body( calendar::turn - 1_turns, calendar::turn ); | |
| } | |
| void player::update_body( const time_point &from, const time_point &to ) | |
| { | |
| update_stamina( to_turns<int>( to - from ) ); | |
| update_stomach( from, to ); | |
| if( ticks_between( from, to, 3_minutes ) > 0 ) { | |
| magic.update_mana( *this, to_turns<float>( 3_minutes ) ); | |
| } | |
| const int five_mins = ticks_between( from, to, 5_minutes ); | |
| if( five_mins > 0 ) { | |
| check_needs_extremes(); | |
| update_needs( five_mins ); | |
| regen( five_mins ); | |
| // Note: mend ticks once per 5 minutes, but wants rate in TURNS, not 5 minute intervals | |
| mend( five_mins * MINUTES( 5 ) ); | |
| } | |
| if( ticks_between( from, to, 24_hours ) > 0 ) { | |
| enforce_minimum_healing(); | |
| } | |
| const int thirty_mins = ticks_between( from, to, 30_minutes ); | |
| if( thirty_mins > 0 ) { | |
| if( activity.is_null() ) { | |
| reset_activity_level(); | |
| } | |
| // Radiation kills health even at low doses | |
| update_health( has_trait( trait_RADIOGENIC ) ? 0 : -radiation ); | |
| get_sick(); | |
| } | |
| for( const auto &v : vitamin::all() ) { | |
| const time_duration rate = vitamin_rate( v.first ); | |
| if( rate > 0_turns ) { | |
| int qty = ticks_between( from, to, rate ); | |
| if( qty > 0 ) { | |
| vitamin_mod( v.first, 0 - qty ); | |
| } | |
| } else if( rate < 0_turns ) { | |
| // mutations can result in vitamins being generated (but never accumulated) | |
| int qty = ticks_between( from, to, -rate ); | |
| if( qty > 0 ) { | |
| vitamin_mod( v.first, qty ); | |
| } | |
| } | |
| } | |
| } | |
| void player::update_stomach( const time_point &from, const time_point &to ) | |
| { | |
| const needs_rates rates = calc_needs_rates(); | |
| // No food/thirst/fatigue clock at all | |
| const bool debug_ls = has_trait( trait_DEBUG_LS ); | |
| // No food/thirst, capped fatigue clock (only up to tired) | |
| const bool npc_no_food = is_npc() && get_option<bool>( "NO_NPC_FOOD" ); | |
| const bool foodless = debug_ls || npc_no_food; | |
| const bool mouse = has_trait( trait_NO_THIRST ); | |
| const bool mycus = has_trait( trait_M_DEPENDENT ); | |
| const float kcal_per_time = get_bmr() / ( 12.0f * 24.0f ); | |
| const int five_mins = ticks_between( from, to, 5_minutes ); | |
| if( five_mins > 0 ) { | |
| stomach.absorb_water( *this, 250_ml * five_mins ); | |
| guts.absorb_water( *this, 250_ml * five_mins ); | |
| } | |
| if( ticks_between( from, to, 30_minutes ) > 0 ) { | |
| // the stomach does not currently have rates of absorption, but this is where it goes | |
| stomach.calculate_absorbed( stomach.get_absorb_rates( true, rates ) ); | |
| guts.calculate_absorbed( guts.get_absorb_rates( false, rates ) ); | |
| stomach.store_absorbed( *this ); | |
| guts.store_absorbed( *this ); | |
| guts.bowel_movement( guts.get_pass_rates( false ) ); | |
| stomach.bowel_movement( stomach.get_pass_rates( true ), guts ); | |
| } | |
| if( stomach.time_since_ate() > 10_minutes ) { | |
| if( stomach.contains() >= stomach.capacity() && get_hunger() > -61 ) { | |
| // you're engorged! your stomach is full to bursting! | |
| set_hunger( -61 ); | |
| } else if( stomach.contains() >= stomach.capacity() / 2 && get_hunger() > -21 ) { | |
| // sated | |
| set_hunger( -21 ); | |
| } else if( stomach.contains() >= stomach.capacity() / 8 && get_hunger() > -1 ) { | |
| // that's really all the food you need to feel full | |
| set_hunger( -1 ); | |
| } else if( stomach.contains() == 0_ml ) { | |
| if( guts.get_calories() == 0 && guts.get_calories_absorbed() == 0 && | |
| get_stored_kcal() < get_healthy_kcal() && get_hunger() < 300 ) { | |
| // there's no food except what you have stored in fat | |
| set_hunger( 300 ); | |
| } else if( get_hunger() < 100 && ( ( guts.get_calories() == 0 && | |
| guts.get_calories_absorbed() == 0 && | |
| get_stored_kcal() >= get_healthy_kcal() ) || get_stored_kcal() < get_healthy_kcal() ) ) { | |
| set_hunger( 100 ); | |
| } else if( get_hunger() < 0 ) { | |
| set_hunger( 0 ); | |
| } | |
| } | |
| if( !foodless && rates.hunger > 0.0f ) { | |
| mod_hunger( roll_remainder( rates.hunger * five_mins ) ); | |
| // instead of hunger keeping track of how you're living, burn calories instead | |
| mod_stored_kcal( -roll_remainder( five_mins * kcal_per_time ) ); | |
| } | |
| } else | |
| // you fill up when you eat fast, but less so than if you eat slow | |
| // if you just ate but your stomach is still empty it will still | |
| // delay your filling up (drugs?) | |
| { | |
| if( stomach.contains() >= stomach.capacity() && get_hunger() > -61 ) { | |
| // you're engorged! your stomach is full to bursting! | |
| set_hunger( -61 ); | |
| } else if( stomach.contains() >= stomach.capacity() * 3 / 4 && get_hunger() > -21 ) { | |
| // sated | |
| set_hunger( -21 ); | |
| } else if( stomach.contains() >= stomach.capacity() / 2 && get_hunger() > -1 ) { | |
| // that's really all the food you need to feel full | |
| set_hunger( -1 ); | |
| } else if( stomach.contains() > 0_ml && get_kcal_percent() > 0.95 ) { | |
| // usually eating something cools your hunger | |
| set_hunger( 0 ); | |
| } | |
| } | |
| if( !foodless && rates.thirst > 0.0f ) { | |
| mod_thirst( roll_remainder( rates.thirst * five_mins ) ); | |
| } | |
| // Mycus and Metabolic Rehydration makes thirst unnecessary | |
| // since water is not limited by intake but by absorption, we can just set thirst to zero | |
| if( mycus || mouse ) { | |
| set_thirst( 0 ); | |
| } | |
| } | |
| void player::update_vitamins( const vitamin_id &vit ) | |
| { | |
| if( is_npc() ) { | |
| return; // NPCs cannot develop vitamin diseases | |
| } | |
| efftype_id def = vit.obj().deficiency(); | |
| efftype_id exc = vit.obj().excess(); | |
| int lvl = vit.obj().severity( vitamin_get( vit ) ); | |
| if( lvl <= 0 ) { | |
| remove_effect( def ); | |
| } | |
| if( lvl >= 0 ) { | |
| remove_effect( exc ); | |
| } | |
| if( lvl > 0 ) { | |
| if( has_effect( def, num_bp ) ) { | |
| get_effect( def, num_bp ).set_intensity( lvl, true ); | |
| } else { | |
| add_effect( def, 1_turns, num_bp, true, lvl ); | |
| } | |
| } | |
| if( lvl < 0 ) { | |
| if( has_effect( exc, num_bp ) ) { | |
| get_effect( exc, num_bp ).set_intensity( lvl, true ); | |
| } else { | |
| add_effect( exc, 1_turns, num_bp, true, lvl ); | |
| } | |
| } | |
| } | |
| void player::get_sick() | |
| { | |
| // NPCs are too dumb to handle infections now | |
| if( is_npc() || has_trait( trait_DISIMMUNE ) ) { | |
| // In a shocking twist, disease immunity prevents diseases. | |
| return; | |
| } | |
| if( has_effect( effect_flu ) || has_effect( effect_common_cold ) ) { | |
| // While it's certainly possible to get sick when you already are, | |
| // it wouldn't be very fun. | |
| return; | |
| } | |
| // Normal people get sick about 2-4 times/year. | |
| int base_diseases_per_year = 3; | |
| if( has_trait( trait_DISRESISTANT ) ) { | |
| // Disease resistant people only get sick once a year. | |
| base_diseases_per_year = 1; | |
| } | |
| // This check runs once every 30 minutes, so double to get hours, *24 to get days. | |
| const int checks_per_year = 2 * 24 * 365; | |
| // Health is in the range [-200,200]. | |
| // Diseases are half as common for every 50 health you gain. | |
| float health_factor = std::pow( 2.0f, get_healthy() / 50.0f ); | |
| int disease_rarity = static_cast<int>( checks_per_year * health_factor / base_diseases_per_year ); | |
| add_msg( m_debug, "disease_rarity = %d", disease_rarity ); | |
| if( one_in( disease_rarity ) ) { | |
| if( one_in( 6 ) ) { | |
| // The flu typically lasts 3-10 days. | |
| add_env_effect( effect_flu, bp_mouth, 3, rng( 3_days, 10_days ) ); | |
| } else { | |
| // A cold typically lasts 1-14 days. | |
| add_env_effect( effect_common_cold, bp_mouth, 3, rng( 1_days, 14_days ) ); | |
| } | |
| } | |
| } | |
| void player::check_needs_extremes() | |
| { | |
| // Check if we've overdosed... in any deadly way. | |
| if( stim > 250 ) { | |
| add_msg_if_player( m_bad, _( "You have a sudden heart attack!" ) ); | |
| add_memorial_log( pgettext( "memorial_male", "Died of a drug overdose." ), | |
| pgettext( "memorial_female", "Died of a drug overdose." ) ); | |
| hp_cur[hp_torso] = 0; | |
| } else if( stim < -200 || get_painkiller() > 240 ) { | |
| add_msg_if_player( m_bad, _( "Your breathing stops completely." ) ); | |
| add_memorial_log( pgettext( "memorial_male", "Died of a drug overdose." ), | |
| pgettext( "memorial_female", "Died of a drug overdose." ) ); | |
| hp_cur[hp_torso] = 0; | |
| } else if( has_effect( effect_jetinjector ) && get_effect_dur( effect_jetinjector ) > 40_minutes ) { | |
| if( !( has_trait( trait_NOPAIN ) ) ) { | |
| add_msg_if_player( m_bad, _( "Your heart spasms painfully and stops." ) ); | |
| } else { | |
| add_msg_if_player( _( "Your heart spasms and stops." ) ); | |
| } | |
| add_memorial_log( pgettext( "memorial_male", "Died of a healing stimulant overdose." ), | |
| pgettext( "memorial_female", "Died of a healing stimulant overdose." ) ); | |
| hp_cur[hp_torso] = 0; | |
| } else if( get_effect_dur( effect_adrenaline ) > 50_minutes ) { | |
| add_msg_if_player( m_bad, _( "Your heart spasms and stops." ) ); | |
| add_memorial_log( pgettext( "memorial_male", "Died of adrenaline overdose." ), | |
| pgettext( "memorial_female", "Died of adrenaline overdose." ) ); | |
| hp_cur[hp_torso] = 0; | |
| } | |
| // check if we've starved | |
| if( is_player() ) { | |
| if( get_stored_kcal() <= 0 ) { | |
| add_msg_if_player( m_bad, _( "You have starved to death." ) ); | |
| add_memorial_log( pgettext( "memorial_male", "Died of starvation." ), | |
| pgettext( "memorial_female", "Died of starvation." ) ); | |
| hp_cur[hp_torso] = 0; | |
| } else { | |
| if( calendar::once_every( 1_hours ) ) { | |
| if( get_kcal_percent() < 0.1f ) { | |
| add_msg_if_player( m_warning, _( "Food..." ) ); | |
| } else if( get_kcal_percent() < 0.25f ) { | |
| add_msg_if_player( m_warning, _( "You are STARVING!" ) ); | |
| } else if( get_kcal_percent() < 0.5f ) { | |
| add_msg_if_player( m_warning, _( "You feel like you haven't eaten in days..." ) ); | |
| } else if( get_kcal_percent() < 0.8f ) { | |
| add_msg_if_player( m_warning, _( "Your stomach feels so empty..." ) ); | |
| } | |
| } | |
| } | |
| } | |
| // Check if we're dying of thirst | |
| if( is_player() && get_thirst() >= 600 && ( stomach.get_water() == 0_ml || | |
| guts.get_water() == 0_ml ) ) { | |
| if( get_thirst() >= 1200 ) { | |
| add_msg_if_player( m_bad, _( "You have died of dehydration." ) ); | |
| add_memorial_log( pgettext( "memorial_male", "Died of thirst." ), | |
| pgettext( "memorial_female", "Died of thirst." ) ); | |
| hp_cur[hp_torso] = 0; | |
| } else if( get_thirst() >= 1000 && calendar::once_every( 30_minutes ) ) { | |
| add_msg_if_player( m_warning, _( "Even your eyes feel dry..." ) ); | |
| } else if( get_thirst() >= 800 && calendar::once_every( 30_minutes ) ) { | |
| add_msg_if_player( m_warning, _( "You are THIRSTY!" ) ); | |
| } else if( calendar::once_every( 30_minutes ) ) { | |
| add_msg_if_player( m_warning, _( "Your mouth feels so dry..." ) ); | |
| } | |
| } | |
| // Check if we're falling asleep, unless we're sleeping | |
| if( get_fatigue() >= EXHAUSTED + 25 && !in_sleep_state() ) { | |
| if( get_fatigue() >= MASSIVE_FATIGUE ) { | |
| add_msg_if_player( m_bad, _( "Survivor sleep now." ) ); | |
| add_memorial_log( pgettext( "memorial_male", "Succumbed to lack of sleep." ), | |
| pgettext( "memorial_female", "Succumbed to lack of sleep." ) ); | |
| mod_fatigue( -10 ); | |
| fall_asleep(); | |
| } else if( get_fatigue() >= 800 && calendar::once_every( 30_minutes ) ) { | |
| add_msg_if_player( m_warning, _( "Anywhere would be a good place to sleep..." ) ); | |
| } else if( calendar::once_every( 30_minutes ) ) { | |
| add_msg_if_player( m_warning, _( "You feel like you haven't slept in days." ) ); | |
| } | |
| } | |
| // Even if we're not Exhausted, we really should be feeling lack/sleep earlier | |
| // Penalties start at Dead Tired and go from there | |
| if( get_fatigue() >= DEAD_TIRED && !in_sleep_state() ) { | |
| if( get_fatigue() >= 700 ) { | |
| if( calendar::once_every( 30_minutes ) ) { | |
| add_msg_if_player( m_warning, _( "You're too physically tired to stop yawning." ) ); | |
| add_effect( effect_lack_sleep, 30_minutes + 1_turns ); | |
| } | |
| /** @EFFECT_INT slightly decreases occurrence of short naps when dead tired */ | |
| if( one_in( 50 + int_cur ) ) { | |
| // Rivet's idea: look out for microsleeps! | |
| fall_asleep( 30_seconds ); | |
| } | |
| } else if( get_fatigue() >= EXHAUSTED ) { | |
| if( calendar::once_every( 30_minutes ) ) { | |
| add_msg_if_player( m_warning, _( "How much longer until bedtime?" ) ); | |
| add_effect( effect_lack_sleep, 30_minutes + 1_turns ); | |
| } | |
| /** @EFFECT_INT slightly decreases occurrence of short naps when exhausted */ | |
| if( one_in( 100 + int_cur ) ) { | |
| fall_asleep( 30_seconds ); | |
| } | |
| } else if( get_fatigue() >= DEAD_TIRED && calendar::once_every( 30_minutes ) ) { | |
| add_msg_if_player( m_warning, _( "*yawn* You should really get some sleep." ) ); | |
| add_effect( effect_lack_sleep, 30_minutes + 1_turns ); | |
| } | |
| } | |
| // Sleep deprivation kicks in if lack of sleep is avoided with stimulants or otherwise for long periods of time | |
| int sleep_deprivation = get_sleep_deprivation(); | |
| float sleep_deprivation_pct = sleep_deprivation / static_cast<float>( SLEEP_DEPRIVATION_MASSIVE ); | |
| if( sleep_deprivation >= SLEEP_DEPRIVATION_HARMLESS && !in_sleep_state() ) { | |
| if( calendar::once_every( 60_minutes ) ) { | |
| if( sleep_deprivation < SLEEP_DEPRIVATION_MINOR ) { | |
| add_msg_if_player( m_warning, | |
| _( "Your mind feels tired. It's been a while since you've slept well." ) ); | |
| mod_fatigue( 1 ); | |
| } else if( sleep_deprivation < SLEEP_DEPRIVATION_SERIOUS ) { | |
| add_msg_if_player( m_bad, | |
| _( "Your mind feels foggy from lack of good sleep, and your eyes keep trying to close against your will." ) ); | |
| mod_fatigue( 5 ); | |
| if( one_in( 10 ) ) { | |
| mod_healthy_mod( -1, 0 ); | |
| } | |
| } else if( sleep_deprivation < SLEEP_DEPRIVATION_MAJOR ) { | |
| add_msg_if_player( m_bad, | |
| _( "Your mind feels weary, and you dread every wakeful minute that passes. You crave sleep, and feel like you're about to collapse." ) ); | |
| mod_fatigue( 10 ); | |
| if( one_in( 5 ) ) { | |
| mod_healthy_mod( -2, 0 ); | |
| } | |
| } else if( sleep_deprivation < SLEEP_DEPRIVATION_MASSIVE ) { | |
| add_msg_if_player( m_bad, | |
| _( "You haven't slept decently for so long that your whole body is screaming for mercy. It's a miracle that you're still awake, but it just feels like a curse now." ) ); | |
| mod_fatigue( 40 ); | |
| mod_healthy_mod( -5, 0 ); | |
| } | |
| // else you pass out for 20 hours, guaranteed | |
| // Microsleeps are slightly worse if you're sleep deprived, but not by much. (chance: 1 in (75 + int_cur) at lethal sleep deprivation) | |
| // Note: these can coexist with fatigue-related microsleeps | |
| /** @EFFECT_INT slightly decreases occurrence of short naps when sleep deprived */ | |
| if( one_in( static_cast<int>( sleep_deprivation_pct * 75 ) + int_cur ) ) { | |
| fall_asleep( 30_seconds ); | |
| } | |
| // Stimulants can be used to stay awake a while longer, but after a while you'll just collapse. | |
| bool can_pass_out = ( stim < 30 && sleep_deprivation >= SLEEP_DEPRIVATION_MINOR ) || | |
| sleep_deprivation >= SLEEP_DEPRIVATION_MAJOR; | |
| if( can_pass_out && calendar::once_every( 10_minutes ) ) { | |
| /** @EFFECT_PER slightly increases resilience against passing out from sleep deprivation */ | |
| if( one_in( static_cast<int>( ( 1 - sleep_deprivation_pct ) * 100 ) + per_cur ) || | |
| sleep_deprivation >= SLEEP_DEPRIVATION_MASSIVE ) { | |
| add_msg_player_or_npc( m_bad, | |
| _( "Your body collapses due to sleep deprivation, your neglected fatigue rushing back all at once, and you pass out on the spot." ) | |
| , _( "<npcname> collapses to the ground from exhaustion." ) ) ; | |
| if( get_fatigue() < EXHAUSTED ) { | |
| set_fatigue( EXHAUSTED ); | |
| } | |
| if( sleep_deprivation >= SLEEP_DEPRIVATION_MAJOR ) { | |
| fall_asleep( 20_hours ); | |
| } else if( sleep_deprivation >= SLEEP_DEPRIVATION_SERIOUS ) { | |
| fall_asleep( 16_hours ); | |
| } else { | |
| fall_asleep( 12_hours ); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| needs_rates player::calc_needs_rates() | |
| { | |
| effect &sleep = get_effect( effect_sleep ); | |
| const bool has_recycler = has_bionic( bio_recycler ); | |
| const bool asleep = !sleep.is_null(); | |
| needs_rates rates; | |
| rates.hunger = metabolic_rate(); | |
| // TODO: this is where calculating basal metabolic rate, in kcal per day would go | |
| rates.kcal = 2500.0; | |
| add_msg_if_player( m_debug, "Metabolic rate: %.2f", rates.hunger ); | |
| rates.thirst = get_option< float >( "PLAYER_THIRST_RATE" ); | |
| rates.thirst *= 1.0f + mutation_value( "thirst_modifier" ); | |
| if( worn_with_flag( "SLOWS_THIRST" ) ) { | |
| rates.thirst *= 0.7f; | |
| } | |
| rates.fatigue = get_option< float >( "PLAYER_FATIGUE_RATE" ); | |
| rates.fatigue *= 1.0f + mutation_value( "fatigue_modifier" ); | |
| // Note: intentionally not in metabolic rate | |
| if( has_recycler ) { | |
| // Recycler won't help much with mutant metabolism - it is intended for human one | |
| rates.hunger = std::min( rates.hunger, std::max( 0.5f, rates.hunger - 0.5f ) ); | |
| rates.thirst = std::min( rates.thirst, std::max( 0.5f, rates.thirst - 0.5f ) ); | |
| } | |
| if( asleep ) { | |
| rates.recovery = 1.0f + mutation_value( "fatigue_regen_modifier" ); | |
| if( !is_hibernating() ) { | |
| // Hunger and thirst advance more slowly while we sleep. This is the standard rate. | |
| rates.hunger *= 0.5f; | |
| rates.thirst *= 0.5f; | |
| const int intense = sleep.is_null() ? 0 : sleep.get_intensity(); | |
| // Accelerated recovery capped to 2x over 2 hours | |
| // After 16 hours of activity, equal to 7.25 hours of rest | |
| const int accelerated_recovery_chance = 24 - intense + 1; | |
| const float accelerated_recovery_rate = 1.0f / accelerated_recovery_chance; | |
| rates.recovery += accelerated_recovery_rate; | |
| } else { | |
| // Hunger and thirst advance *much* more slowly whilst we hibernate. | |
| rates.hunger *= ( 2.0f / 7.0f ); | |
| rates.thirst *= ( 2.0f / 7.0f ); | |
| } | |
| rates.recovery -= static_cast<float>( get_perceived_pain() ) / 60; | |
| } else { | |
| rates.recovery = 0; | |
| } | |
| if( has_activity( activity_id( "ACT_TREE_COMMUNION" ) ) ) { | |
| // Much of the body's needs are taken care of by the trees. | |
| // Hair Roots dont provide any bodily needs. | |
| if( has_trait( trait_ROOTS2 ) || has_trait( trait_ROOTS3 ) ) { | |
| rates.hunger *= 0.5f; | |
| rates.thirst *= 0.5f; | |
| rates.fatigue *= 0.5f; | |
| } | |
| } | |
| if( has_trait( trait_TRANSPIRATION ) ) { | |
| // Transpiration, the act of moving nutrients with evaporating water, can take a very heavy toll on your thirst when it's really hot. | |
| rates.thirst *= ( ( g->weather.get_temperature( pos() ) - 65 / 2 ) / 40.0f ); | |
| } | |
| if( is_npc() ) { | |
| rates.hunger *= 0.25f; | |
| rates.thirst *= 0.25f; | |
| } | |
| return rates; | |
| } | |
| void player::update_needs( int rate_multiplier ) | |
| { | |
| // Hunger, thirst, & fatigue up every 5 minutes | |
| effect &sleep = get_effect( effect_sleep ); | |
| // No food/thirst/fatigue clock at all | |
| const bool debug_ls = has_trait( trait_DEBUG_LS ); | |
| // No food/thirst, capped fatigue clock (only up to tired) | |
| const bool npc_no_food = is_npc() && get_option<bool>( "NO_NPC_FOOD" ); | |
| const bool asleep = !sleep.is_null(); | |
| const bool lying = asleep || has_effect( effect_lying_down ) || | |
| activity.id() == "ACT_TRY_SLEEP"; | |
| needs_rates rates = calc_needs_rates(); | |
| const bool wasnt_fatigued = get_fatigue() <= DEAD_TIRED; | |
| // Don't increase fatigue if sleeping or trying to sleep or if we're at the cap. | |
| if( get_fatigue() < 1050 && !asleep && !debug_ls ) { | |
| if( rates.fatigue > 0.0f ) { | |
| int fatigue_roll = roll_remainder( rates.fatigue * rate_multiplier ); | |
| mod_fatigue( fatigue_roll ); | |
| if( get_option< bool >( "SLEEP_DEPRIVATION" ) ) { | |
| // Synaptic regen bionic stops SD while awake and boosts it while sleeping | |
| if( !has_active_bionic( bio_synaptic_regen ) ) { | |
| // fatigue_roll should be around 1 - so the counter increases by 1 every minute on average, | |
| // but characters who need less sleep will also get less sleep deprived, and vice-versa. | |
| // Note: Since needs are updated in 5-minute increments, we have to multiply the roll again by | |
| // 5. If rate_multiplier is > 1, fatigue_roll will be higher and this will work out. | |
| mod_sleep_deprivation( fatigue_roll * 5 ); | |
| } | |
| } | |
| if( npc_no_food && get_fatigue() > TIRED ) { | |
| set_fatigue( TIRED ); | |
| set_sleep_deprivation( 0 ); | |
| } | |
| } | |
| } else if( asleep ) { | |
| if( rates.recovery > 0.0f ) { | |
| int recovered = roll_remainder( rates.recovery * rate_multiplier ); | |
| if( get_fatigue() - recovered < -20 ) { | |
| // Should be wake up, but that could prevent some retroactive regeneration | |
| sleep.set_duration( 1_turns ); | |
| mod_fatigue( -25 ); | |
| } else { | |
| mod_fatigue( -recovered ); | |
| if( get_option< bool >( "SLEEP_DEPRIVATION" ) ) { | |
| // Sleeping on the ground, no bionic = 1x rest_modifier | |
| // Sleeping on a bed, no bionic = 2x rest_modifier | |
| // Sleeping on a comfy bed, no bionic= 3x rest_modifier | |
| // Sleeping on the ground, bionic = 3x rest_modifier | |
| // Sleeping on a bed, bionic = 6x rest_modifier | |
| // Sleeping on a comfy bed, bionic = 9x rest_modifier | |
| float rest_modifier = ( has_active_bionic( bio_synaptic_regen ) ? 3 : 1 ); | |
| // Magnesium supplements also add a flat bonus to recovery speed | |
| if( has_effect( effect_magnesium_supplements ) ) { | |
| rest_modifier += 1; | |
| } | |
| comfort_level comfort = base_comfort_value( pos() ); | |
| if( comfort >= comfort_level::very_comfortable ) { | |
| rest_modifier *= 3; | |
| } else if( comfort >= comfort_level::comfortable ) { | |
| rest_modifier *= 2.5; | |
| } else if( comfort >= comfort_level::slightly_comfortable ) { | |
| rest_modifier *= 2; | |
| } | |
| // If we're just tired, we'll get a decent boost to our sleep quality. | |
| // The opposite is true for very tired characters. | |
| if( get_fatigue() < DEAD_TIRED ) { | |
| rest_modifier += 2; | |
| } else if( get_fatigue() >= EXHAUSTED ) { | |
| rest_modifier = ( rest_modifier > 2 ) ? rest_modifier - 2 : 1; | |
| } | |
| // Recovered is multiplied by 2 as well, since we spend 1/3 of the day sleeping | |
| mod_sleep_deprivation( -rest_modifier * ( recovered * 2 ) ); | |
| } | |
| } | |
| } | |
| } | |
| if( is_player() && wasnt_fatigued && get_fatigue() > DEAD_TIRED && !lying ) { | |
| if( !activity ) { | |
| add_msg_if_player( m_warning, _( "You're feeling tired. %s to lie down for sleep." ), | |
| press_x( ACTION_SLEEP ) ); | |
| } else { | |
| g->cancel_activity_query( _( "You're feeling tired." ) ); | |
| } | |
| } | |
| if( stim < 0 ) { | |
| stim = std::min( stim + rate_multiplier, 0 ); | |
| } else if( stim > 0 ) { | |
| stim = std::max( stim - rate_multiplier, 0 ); | |
| } | |
| if( get_painkiller() > 0 ) { | |
| mod_painkiller( -std::min( get_painkiller(), rate_multiplier ) ); | |
| } | |
| if( g->is_in_sunlight( pos() ) ) { | |
| if( has_bionic( bn_bio_solar ) ) { | |
| charge_power( rate_multiplier * 25 ); | |
| } | |
| if( has_active_bionic( bionic_id( "bio_cable" ) ) ) { | |
| if( is_wearing( "solarpack_on" ) ) { | |
| charge_power( rate_multiplier * 25 ); | |
| } | |
| if( is_wearing( "q_solarpack_on" ) ) { | |
| charge_power( rate_multiplier * 50 ); | |
| } | |
| } | |
| } | |
| // Huge folks take penalties for cramming themselves in vehicles | |
| if( in_vehicle && ( has_trait( trait_HUGE ) || has_trait( trait_HUGE_OK ) ) ) { | |
| add_msg_if_player( m_bad, | |
| _( "You're cramping up from stuffing yourself in this vehicle." ) ); | |
| if( is_npc() ) { | |
| npc &as_npc = dynamic_cast<npc &>( *this ); | |
| as_npc.complain_about( "cramped_vehicle", 1_hours, "<cramped_vehicle>", false ); | |
| } | |
| mod_pain_noresist( 2 * rng( 2, 3 ) ); | |
| focus_pool -= 1; | |
| } | |
| } | |
| void player::regen( int rate_multiplier ) | |
| { | |
| int pain_ticks = rate_multiplier; | |
| while( get_pain() > 0 && pain_ticks-- > 0 ) { | |
| mod_pain( -roll_remainder( 0.2f + get_pain() / 50.0f ) ); | |
| } | |
| float rest = rest_quality(); | |
| float heal_rate = healing_rate( rest ) * MINUTES( 5 ); | |
| if( heal_rate > 0.0f ) { | |
| healall( roll_remainder( rate_multiplier * heal_rate ) ); | |
| } else if( heal_rate < 0.0f ) { | |
| int rot_rate = roll_remainder( rate_multiplier * -heal_rate ); | |
| // Has to be in loop because some effects depend on rounding | |
| while( rot_rate-- > 0 ) { | |
| hurtall( 1, nullptr, false ); | |
| } | |
| } | |
| // include healing effects | |
| for( int i = 0; i < num_hp_parts; i++ ) { | |
| body_part bp = hp_to_bp( static_cast<hp_part>( i ) ); | |
| float healing = healing_rate_medicine( rest, bp ) * MINUTES( 5 ) ; | |
| int healing_apply = roll_remainder( healing ); | |
| healed_bp( i, healing_apply ); | |
| heal( bp, healing_apply ); | |
| if( damage_bandaged[i] > 0 ) { | |
| damage_bandaged[i] -= healing_apply; | |
| if( damage_bandaged[i] <= 0 ) { | |
| damage_bandaged[i] = 0; | |
| remove_effect( effect_bandaged, bp ); | |
| add_msg_if_player( _( "Bandaged wounds on your %s was healed." ), body_part_name( bp ) ); | |
| } | |
| } | |
| if( damage_disinfected[i] > 0 ) { | |
| damage_disinfected[i] -= healing_apply; | |
| if( damage_disinfected[i] <= 0 ) { | |
| damage_disinfected[i] = 0; | |
| remove_effect( effect_disinfected, bp ); | |
| add_msg_if_player( _( "Disinfected wounds on your %s was healed." ), body_part_name( bp ) ); | |
| } | |
| } | |
| // remove effects if the limb was healed by other way | |
| if( has_effect( effect_bandaged, bp ) && ( hp_cur[i] == hp_max[i] ) ) { | |
| damage_bandaged[i] = 0; | |
| remove_effect( effect_bandaged, bp ); | |
| add_msg_if_player( _( "Bandaged wounds on your %s was healed." ), body_part_name( bp ) ); | |
| } | |
| if( has_effect( effect_disinfected, bp ) && ( hp_cur[i] == hp_max[i] ) ) { | |
| damage_disinfected[i] = 0; | |
| remove_effect( effect_disinfected, bp ); | |
| add_msg_if_player( _( "Disinfected wounds on your %s was healed." ), body_part_name( bp ) ); | |
| } | |
| } | |
| if( radiation > 0 ) { | |
| radiation = std::max( 0, radiation - roll_remainder( rate_multiplier / 50.0f ) ); | |
| } | |
| } | |
| void player::update_stamina( int turns ) | |
| { | |
| float stamina_recovery = 0.0f; | |
| // Recover some stamina every turn. | |
| // Mutated stamina works even when winded | |
| float stamina_multiplier = ( !has_effect( effect_winded ) ? 1.0f : 0.1f ) + | |
| mutation_value( "stamina_regen_modifier" ); | |
| // But mouth encumbrance interferes, even with mutated stamina. | |
| stamina_recovery += stamina_multiplier * std::max( 1.0f, | |
| get_option<float>( "PLAYER_BASE_STAMINA_REGEN_RATE" ) - ( encumb( bp_mouth ) / 10.0f ) ); | |
| // TODO: recovering stamina causes hunger/thirst/fatigue. | |
| // TODO: Tiredness slowing recovery | |
| // stim recovers stamina (or impairs recovery) | |
| if( stim > 0 ) { | |
| // TODO: Make stamina recovery with stims cost health | |
| stamina_recovery += std::min( 5.0f, stim / 20.0f ); | |
| } else if( stim < 0 ) { | |
| // Affect it less near 0 and more near full | |
| // Negative stim kill at -200 | |
| // At -100 stim it inflicts -20 malus to regen at 100% stamina, | |
| // effectivly countering stamina gain of default 20, | |
| // at 50% stamina its -10 (50%), cuts by 25% at 25% stamina | |
| stamina_recovery += stim / 5.0f * stamina / get_stamina_max() ; | |
| } | |
| const int max_stam = get_stamina_max(); | |
| if( power_level >= 3 && has_active_bionic( bio_gills ) ) { | |
| int bonus = std::min<int>( power_level / 3, max_stam - stamina - stamina_recovery * turns ); | |
| // so the effective recovery is up to 5x default | |
| bonus = std::min( bonus, 4 * static_cast<int> | |
| ( get_option<float>( "PLAYER_BASE_STAMINA_REGEN_RATE" ) ) ); | |
| if( bonus > 0 ) { | |
| stamina_recovery += bonus; | |
| bonus /= 10; | |
| bonus = std::max( bonus, 1 ); | |
| charge_power( -bonus ); | |
| } | |
| } | |
| stamina += roll_remainder( stamina_recovery * turns ); | |
| add_msg( m_debug, "Stamina recovery: %d", roll_remainder( stamina_recovery * turns ) ); | |
| // Cap at max | |
| stamina = std::min( std::max( stamina, 0 ), max_stam ); | |
| } | |
| bool player::is_hibernating() const | |
| { | |
| // Hibernating only kicks in whilst Engorged; separate tracking for hunger/thirst here | |
| // as a safety catch. One test subject managed to get two Colds during hibernation; | |
| // since those add fatigue and dry out the character, the subject went for the full 10 days plus | |
| // a little, and came out of it well into Parched. Hibernating shouldn't endanger your | |
| // life like that--but since there's much less fluid reserve than food reserve, | |
| // simply using the same numbers won't work. | |
| return has_effect( effect_sleep ) && get_kcal_percent() > 0.8f && | |
| get_thirst() <= 80 && has_active_mutation( trait_id( "HIBERNATE" ) ); | |
| } | |
| void player::add_addiction( add_type type, int strength ) | |
| { | |
| if( type == ADD_NULL ) { | |
| return; | |
| } | |
| time_duration timer = 2_hours; | |
| if( has_trait( trait_ADDICTIVE ) ) { | |
| strength *= 2; | |
| timer = 1_hours; | |
| } else if( has_trait( trait_NONADDICTIVE ) ) { | |
| strength /= 2; | |
| timer = 6_hours; | |
| } | |
| //Update existing addiction | |
| for( auto &i : addictions ) { | |
| if( i.type != type ) { | |
| continue; | |
| } | |
| if( i.sated < 0_turns ) { | |
| i.sated = timer; | |
| } else if( i.sated < 10_minutes ) { | |
| i.sated += timer; // TODO: Make this variable? | |
| } else { | |
| i.sated += timer / 2; | |
| } | |
| if( i.intensity < MAX_ADDICTION_LEVEL && strength > i.intensity * rng( 2, 5 ) ) { | |
| i.intensity++; | |
| } | |
| add_msg( m_debug, "Updating addiction: %d intensity, %d sated", | |
| i.intensity, to_turns<int>( i.sated ) ); | |
| return; | |
| } | |
| // Add a new addiction | |
| const int roll = rng( 0, 100 ); | |
| add_msg( m_debug, "Addiction: roll %d vs strength %d", roll, strength ); | |
| if( roll < strength ) { | |
| //~ %s is addiction name | |
| const std::string &type_name = addiction_type_name( type ); | |
| add_memorial_log( pgettext( "memorial_male", "Became addicted to %s." ), | |
| pgettext( "memorial_female", "Became addicted to %s." ), | |
| type_name ); | |
| add_msg( m_debug, "%s got addicted to %s", disp_name(), type_name ); | |
| addictions.emplace_back( type, 1 ); | |
| } | |
| } | |
| bool player::has_addiction( add_type type ) const | |
| { | |
| return std::any_of( addictions.begin(), addictions.end(), | |
| [type]( const addiction & ad ) { | |
| return ad.type == type && ad.intensity >= MIN_ADDICTION_LEVEL; | |
| } ); | |
| } | |
| void player::rem_addiction( add_type type ) | |
| { | |
| auto iter = std::find_if( addictions.begin(), addictions.end(), | |
| [type]( const addiction & ad ) { | |
| return ad.type == type; | |
| } ); | |
| if( iter != addictions.end() ) { | |
| //~ %s is addiction name | |
| add_memorial_log( pgettext( "memorial_male", "Overcame addiction to %s." ), | |
| pgettext( "memorial_female", "Overcame addiction to %s." ), | |
| addiction_type_name( type ) ); | |
| addictions.erase( iter ); | |
| } | |
| } | |
| int player::addiction_level( add_type type ) const | |
| { | |
| auto iter = std::find_if( addictions.begin(), addictions.end(), | |
| [type]( const addiction & ad ) { | |
| return ad.type == type; | |
| } ); | |
| return iter != addictions.end() ? iter->intensity : 0; | |
| } | |
| void player::siphon( vehicle &veh, const itype_id &type ) | |
| { | |
| auto qty = veh.fuel_left( type ); | |
| if( qty <= 0 ) { | |
| add_msg( m_bad, _( "There is not enough %s left to siphon it." ), item::nname( type ) ); | |
| return; | |
| } | |
| item liquid( type, calendar::turn, qty ); | |
| if( liquid.is_food() ) { | |
| liquid.set_item_specific_energy( veh.fuel_specific_energy( type ) ); | |
| } | |
| if( liquid_handler::handle_liquid( liquid, nullptr, 1, nullptr, &veh ) ) { | |
| veh.drain( type, qty - liquid.charges ); | |
| } | |
| } | |
| void player::cough( bool harmful, int loudness ) | |
| { | |
| if( harmful ) { | |
| const int stam = stamina; | |
| const int malus = get_stamina_max() * 0.05; // 5% max stamina | |
| mod_stat( "stamina", -malus ); | |
| if( stam < malus && x_in_y( malus - stam, malus ) ) { | |
| apply_damage( nullptr, bp_torso, 1 ); | |
| } | |
| } | |
| if( has_effect( effect_cough_suppress ) ) { | |
| return; | |
| } | |
| if( !is_npc() ) { | |
| add_msg( m_bad, _( "You cough heavily." ) ); | |
| } | |
| sounds::sound( pos(), loudness, sounds::sound_t::speech, _( "a hacking cough." ), false, "misc", | |
| "cough" ); | |
| moves -= 80; | |
| if( has_effect( effect_sleep ) && !has_effect( effect_narcosis ) && | |
| ( ( harmful && one_in( 3 ) ) || one_in( 10 ) ) ) { | |
| wake_up(); | |
| } | |
| } | |
| void player::add_pain_msg( int val, body_part bp ) const | |
| { | |
| if( has_trait( trait_NOPAIN ) ) { | |
| return; | |
| } | |
| if( bp == num_bp ) { | |
| if( val > 20 ) { | |
| add_msg_if_player( _( "Your body is wracked with excruciating pain!" ) ); | |
| } else if( val > 10 ) { | |
| add_msg_if_player( _( "Your body is wracked with terrible pain!" ) ); | |
| } else if( val > 5 ) { | |
| add_msg_if_player( _( "Your body is wracked with pain!" ) ); | |
| } else if( val > 1 ) { | |
| add_msg_if_player( _( "Your body pains you!" ) ); | |
| } else { | |
| add_msg_if_player( _( "Your body aches." ) ); | |
| } | |
| } else { | |
| if( val > 20 ) { | |
| add_msg_if_player( _( "Your %s is wracked with excruciating pain!" ), | |
| body_part_name_accusative( bp ) ); | |
| } else if( val > 10 ) { | |
| add_msg_if_player( _( "Your %s is wracked with terrible pain!" ), | |
| body_part_name_accusative( bp ) ); | |
| } else if( val > 5 ) { | |
| add_msg_if_player( _( "Your %s is wracked with pain!" ), | |
| body_part_name_accusative( bp ) ); | |
| } else if( val > 1 ) { | |
| add_msg_if_player( _( "Your %s pains you!" ), | |
| body_part_name_accusative( bp ) ); | |
| } else { | |
| add_msg_if_player( _( "Your %s aches." ), | |
| body_part_name_accusative( bp ) ); | |
| } | |
| } | |
| } | |
| void player::print_health() const | |
| { | |
| if( !is_player() ) { | |
| return; | |
| } | |
| int current_health = get_healthy(); | |
| if( has_trait( trait_SELFAWARE ) ) { | |
| add_msg_if_player( _( "Your current health value is %d." ), current_health ); | |
| } | |
| if( current_health > 0 && | |
| ( has_effect( effect_common_cold ) || has_effect( effect_flu ) ) ) { | |
| return; | |
| } | |
| static const std::map<int, std::string> msg_categories = { | |
| { -100, "health_horrible" }, | |
| { -50, "health_very_bad" }, | |
| { -10, "health_bad" }, | |
| { 10, "" }, | |
| { 50, "health_good" }, | |
| { 100, "health_very_good" }, | |
| { INT_MAX, "health_great" } | |
| }; | |
| auto iter = msg_categories.lower_bound( current_health ); | |
| if( iter != msg_categories.end() && !iter->second.empty() ) { | |
| const std::string &msg = SNIPPET.random_from_category( iter->second ); | |
| add_msg_if_player( current_health > 0 ? m_good : m_bad, msg ); | |
| } | |
| } | |
| void player::process_one_effect( effect &it, bool is_new ) | |
| { | |
| bool reduced = resists_effect( it ); | |
| double mod = 1; | |
| body_part bp = it.get_bp(); | |
| int val = 0; | |
| // Still hardcoded stuff, do this first since some modify their other traits | |
| hardcoded_effects( it ); | |
| const auto get_effect = [&it, is_new]( const std::string & arg, bool reduced ) { | |
| if( is_new ) { | |
| return it.get_amount( arg, reduced ); | |
| } | |
| return it.get_mod( arg, reduced ); | |
| }; | |
| // Handle miss messages | |
| auto msgs = it.get_miss_msgs(); | |
| if( !msgs.empty() ) { | |
| for( const auto &i : msgs ) { | |
| add_miss_reason( _( i.first ), static_cast<unsigned>( i.second ) ); | |
| } | |
| } | |
| // Handle health mod | |
| val = get_effect( "H_MOD", reduced ); | |
| if( val != 0 ) { | |
| mod = 1; | |
| if( is_new || it.activated( calendar::turn, "H_MOD", val, reduced, mod ) ) { | |
| int bounded = bound_mod_to_vals( | |
| get_healthy_mod(), val, it.get_max_val( "H_MOD", reduced ), | |
| it.get_min_val( "H_MOD", reduced ) ); | |
| // This already applies bounds, so we pass them through. | |
| mod_healthy_mod( bounded, get_healthy_mod() + bounded ); | |
| } | |
| } | |
| // Handle health | |
| val = get_effect( "HEALTH", reduced ); | |
| if( val != 0 ) { | |
| mod = 1; | |
| if( is_new || it.activated( calendar::turn, "HEALTH", val, reduced, mod ) ) { | |
| mod_healthy( bound_mod_to_vals( get_healthy(), val, | |
| it.get_max_val( "HEALTH", reduced ), it.get_min_val( "HEALTH", reduced ) ) ); | |
| } | |
| } | |
| // Handle stim | |
| val = get_effect( "STIM", reduced ); | |
| if( val != 0 ) { | |
| mod = 1; | |
| if( is_new || it.activated( calendar::turn, "STIM", val, reduced, mod ) ) { | |
| stim += bound_mod_to_vals( stim, val, it.get_max_val( "STIM", reduced ), | |
| it.get_min_val( "STIM", reduced ) ); | |
| } | |
| } | |
| // Handle hunger | |
| val = get_effect( "HUNGER", reduced ); | |
| if( val != 0 ) { | |
| mod = 1; | |
| if( is_new || it.activated( calendar::turn, "HUNGER", val, reduced, mod ) ) { | |
| mod_hunger( bound_mod_to_vals( get_hunger(), val, it.get_max_val( "HUNGER", reduced ), | |
| it.get_min_val( "HUNGER", reduced ) ) ); | |
| } | |
| } | |
| // Handle thirst | |
| val = get_effect( "THIRST", reduced ); | |
| if( val != 0 ) { | |
| mod = 1; | |
| if( is_new || it.activated( calendar::turn, "THIRST", val, reduced, mod ) ) { | |
| mod_thirst( bound_mod_to_vals( get_thirst(), val, it.get_max_val( "THIRST", reduced ), | |
| it.get_min_val( "THIRST", reduced ) ) ); | |
| } | |
| } | |
| // Handle fatigue | |
| val = get_effect( "FATIGUE", reduced ); | |
| // Prevent ongoing fatigue effects while asleep. | |
| // These are meant to change how fast you get tired, not how long you sleep. | |
| if( val != 0 && !in_sleep_state() ) { | |
| mod = 1; | |
| if( is_new || it.activated( calendar::turn, "FATIGUE", val, reduced, mod ) ) { | |
| mod_fatigue( bound_mod_to_vals( get_fatigue(), val, it.get_max_val( "FATIGUE", reduced ), | |
| it.get_min_val( "FATIGUE", reduced ) ) ); | |
| } | |
| } | |
| // Handle Radiation | |
| val = get_effect( "RAD", reduced ); | |
| if( val != 0 ) { | |
| mod = 1; | |
| if( is_new || it.activated( calendar::turn, "RAD", val, reduced, mod ) ) { | |
| radiation += bound_mod_to_vals( radiation, val, it.get_max_val( "RAD", reduced ), 0 ); | |
| // Radiation can't go negative | |
| if( radiation < 0 ) { | |
| radiation = 0; | |
| } | |
| } | |
| } | |
| // Handle Pain | |
| val = get_effect( "PAIN", reduced ); | |
| if( val != 0 ) { | |
| mod = 1; | |
| if( it.get_sizing( "PAIN" ) ) { | |
| if( has_trait( trait_FAT ) ) { | |
| mod *= 1.5; | |
| } | |
| if( has_trait( trait_LARGE ) || has_trait( trait_LARGE_OK ) ) { | |
| mod *= 2; | |
| } | |
| if( has_trait( trait_HUGE ) || has_trait( trait_HUGE_OK ) ) { | |
| mod *= 3; | |
| } | |
| } | |
| if( is_new || it.activated( calendar::turn, "PAIN", val, reduced, mod ) ) { | |
| int pain_inc = bound_mod_to_vals( get_pain(), val, it.get_max_val( "PAIN", reduced ), 0 ); | |
| mod_pain( pain_inc ); | |
| if( pain_inc > 0 ) { | |
| add_pain_msg( val, bp ); | |
| } | |
| } | |
| } | |
| // Handle Damage | |
| val = get_effect( "HURT", reduced ); | |
| if( val != 0 ) { | |
| mod = 1; | |
| if( it.get_sizing( "HURT" ) ) { | |
| if( has_trait( trait_FAT ) ) { | |
| mod *= 1.5; | |
| } | |
| if( has_trait( trait_LARGE ) || has_trait( trait_LARGE_OK ) ) { | |
| mod *= 2; | |
| } | |
| if( has_trait( trait_HUGE ) || has_trait( trait_HUGE_OK ) ) { | |
| mod *= 3; | |
| } | |
| } | |
| if( is_new || it.activated( calendar::turn, "HURT", val, reduced, mod ) ) { | |
| if( bp == num_bp ) { | |
| if( val > 5 ) { | |
| add_msg_if_player( _( "Your %s HURTS!" ), body_part_name_accusative( bp_torso ) ); | |
| } else { | |
| add_msg_if_player( _( "Your %s hurts!" ), body_part_name_accusative( bp_torso ) ); | |
| } | |
| apply_damage( nullptr, bp_torso, val, true ); | |
| } else { | |
| if( val > 5 ) { | |
| add_msg_if_player( _( "Your %s HURTS!" ), body_part_name_accusative( bp ) ); | |
| } else { | |
| add_msg_if_player( _( "Your %s hurts!" ), body_part_name_accusative( bp ) ); | |
| } | |
| apply_damage( nullptr, bp, val, true ); | |
| } | |
| } | |
| } | |
| // Handle Sleep | |
| val = get_effect( "SLEEP", reduced ); | |
| if( val != 0 ) { | |
| mod = 1; | |
| if( is_new || it.activated( calendar::turn, "SLEEP", val, reduced, mod ) ) { | |
| add_msg_if_player( _( "You pass out!" ) ); | |
| fall_asleep( time_duration::from_turns( val ) ); | |
| } | |
| } | |
| // Handle painkillers | |
| val = get_effect( "PKILL", reduced ); | |
| if( val != 0 ) { | |
| mod = it.get_addict_mod( "PKILL", addiction_level( ADD_PKILLER ) ); | |
| if( is_new || it.activated( calendar::turn, "PKILL", val, reduced, mod ) ) { | |
| mod_painkiller( bound_mod_to_vals( pkill, val, it.get_max_val( "PKILL", reduced ), 0 ) ); | |
| } | |
| } | |
| // Handle coughing | |
| mod = 1; | |
| val = 0; | |
| if( it.activated( calendar::turn, "COUGH", val, reduced, mod ) ) { | |
| cough( it.get_harmful_cough() ); | |
| } | |
| // Handle vomiting | |
| mod = vomit_mod(); | |
| val = 0; | |
| if( it.activated( calendar::turn, "VOMIT", val, reduced, mod ) ) { | |
| vomit(); | |
| } | |
| // Handle stamina | |
| val = get_effect( "STAMINA", reduced ); | |
| if( val != 0 ) { | |
| mod = 1; | |
| if( is_new || it.activated( calendar::turn, "STAMINA", val, reduced, mod ) ) { | |
| mod_stat( "stamina", bound_mod_to_vals( stamina, val, | |
| it.get_max_val( "STAMINA", reduced ), | |
| it.get_min_val( "STAMINA", reduced ) ) ); | |
| } | |
| } | |
| // Speed and stats are handled in recalc_speed_bonus and reset_stats respectively | |
| } | |
| void player::process_effects() | |
| { | |
| //Special Removals | |
| if( has_effect( effect_darkness ) && g->is_in_sunlight( pos() ) ) { | |
| remove_effect( effect_darkness ); | |
| } | |
| if( has_trait( trait_M_IMMUNE ) && has_effect( effect_fungus ) ) { | |
| vomit(); | |
| remove_effect( effect_fungus ); | |
| add_msg_if_player( m_bad, _( "We have mistakenly colonized a local guide! Purging now." ) ); | |
| } | |
| if( has_trait( trait_PARAIMMUNE ) && ( has_effect( effect_dermatik ) || | |
| has_effect( effect_tapeworm ) || | |
| has_effect( effect_bloodworms ) || has_effect( effect_brainworms ) || | |
| has_effect( effect_paincysts ) ) ) { | |
| remove_effect( effect_dermatik ); | |
| remove_effect( effect_tapeworm ); | |
| remove_effect( effect_bloodworms ); | |
| remove_effect( effect_brainworms ); | |
| remove_effect( effect_paincysts ); | |
| add_msg_if_player( m_good, _( "Something writhes and inside of you as it dies." ) ); | |
| } | |
| if( has_trait( trait_ACIDBLOOD ) && ( has_effect( effect_dermatik ) || | |
| has_effect( effect_bloodworms ) || | |
| has_effect( effect_brainworms ) ) ) { | |
| remove_effect( effect_dermatik ); | |
| remove_effect( effect_bloodworms ); | |
| remove_effect( effect_brainworms ); | |
| } | |
| if( has_trait( trait_EATHEALTH ) && has_effect( effect_tapeworm ) ) { | |
| remove_effect( effect_tapeworm ); | |
| add_msg_if_player( m_good, _( "Your bowels gurgle as something inside them dies." ) ); | |
| } | |
| if( has_trait( trait_INFIMMUNE ) && ( has_effect( effect_bite ) || has_effect( effect_infected ) || | |
| has_effect( effect_recover ) ) ) { | |
| remove_effect( effect_bite ); | |
| remove_effect( effect_infected ); | |
| remove_effect( effect_recover ); | |
| } | |
| //Human only effects | |
| for( auto &elem : *effects ) { | |
| for( auto &_effect_it : elem.second ) { | |
| process_one_effect( _effect_it.second, false ); | |
| } | |
| } | |
| Creature::process_effects(); | |
| } | |
| double player::vomit_mod() | |
| { | |
| double mod = 1; | |
| if( has_effect( effect_weed_high ) ) { | |
| mod *= .1; | |
| } | |
| if( has_trait( trait_STRONGSTOMACH ) ) { | |
| mod *= .5; | |
| } | |
| if( has_trait( trait_WEAKSTOMACH ) ) { | |
| mod *= 2; | |
| } | |
| if( has_trait( trait_NAUSEA ) ) { | |
| mod *= 3; | |
| } | |
| if( has_trait( trait_VOMITOUS ) ) { | |
| mod *= 3; | |
| } | |
| // If you're already nauseous, any food in your stomach greatly | |
| // increases chance of vomiting. Liquids don't provoke vomiting, though. | |
| if( stomach.contains() != 0_ml && has_effect( effect_nausea ) ) { | |
| mod *= 5 * get_effect_int( effect_nausea ); | |
| } | |
| return mod; | |
| } | |
| void player::suffer() | |
| { | |
| // TODO: Remove this section and encapsulate hp_cur | |
| for( int i = 0; i < num_hp_parts; i++ ) { | |
| body_part bp = hp_to_bp( static_cast<hp_part>( i ) ); | |
| if( hp_cur[i] <= 0 ) { | |
| add_effect( effect_disabled, 1_turns, bp, true ); | |
| } | |
| } | |
| for( size_t i = 0; i < my_bionics->size(); i++ ) { | |
| if( ( *my_bionics )[i].powered ) { | |
| process_bionic( i ); | |
| } | |
| } | |
| for( auto &mut : my_mutations ) { | |
| auto &tdata = mut.second; | |
| if( !tdata.powered ) { | |
| continue; | |
| } | |
| const auto &mdata = mut.first.obj(); | |
| if( tdata.powered && tdata.charge > 0 ) { | |
| // Already-on units just lose a bit of charge | |
| tdata.charge--; | |
| } else { | |
| // Not-on units, or those with zero charge, have to pay the power cost | |
| if( mdata.cooldown > 0 ) { | |
| tdata.powered = true; | |
| tdata.charge = mdata.cooldown - 1; | |
| } | |
| if( mdata.hunger ) { | |
| // does not directly modify hunger, but burns kcal | |
| mod_stored_nutr( mdata.cost ); | |
| if( get_bmi() < character_weight_category::underweight ) { | |
| add_msg_if_player( m_warning, _( "You're too malnourished to keep your %s going." ), mdata.name() ); | |
| tdata.powered = false; | |
| } | |
| } | |
| if( mdata.thirst ) { | |
| mod_thirst( mdata.cost ); | |
| if( get_thirst() >= 260 ) { // Well into Dehydrated | |
| add_msg_if_player( m_warning, _( "You're too dehydrated to keep your %s going." ), mdata.name() ); | |
| tdata.powered = false; | |
| } | |
| } | |
| if( mdata.fatigue ) { | |
| mod_fatigue( mdata.cost ); | |
| if( get_fatigue() >= EXHAUSTED ) { // Exhausted | |
| add_msg_if_player( m_warning, _( "You're too exhausted to keep your %s going." ), mdata.name() ); | |
| tdata.powered = false; | |
| } | |
| } | |
| if( !tdata.powered ) { | |
| apply_mods( mut.first, false ); | |
| } | |
| } | |
| } | |
| if( underwater ) { | |
| if( !has_trait( trait_GILLS ) && !has_trait( trait_GILLS_CEPH ) ) { | |
| oxygen--; | |
| } | |
| if( oxygen < 12 && worn_with_flag( "REBREATHER" ) ) { | |
| oxygen += 12; | |
| } | |
| if( oxygen <= 5 ) { | |
| if( has_bionic( bio_gills ) && power_level >= 25 ) { | |
| oxygen += 5; | |
| charge_power( -25 ); | |
| } else { | |
| add_msg_if_player( m_bad, _( "You're drowning!" ) ); | |
| apply_damage( nullptr, bp_torso, rng( 1, 4 ) ); | |
| } | |
| } | |
| if( has_trait( trait_FRESHWATEROSMOSIS ) && !g->m.has_flag_ter( "SALT_WATER", pos() ) && | |
| get_thirst() > -60 ) { | |
| mod_thirst( -1 ); | |
| } | |
| } | |
| if( has_trait( trait_SHARKTEETH ) && one_turn_in( 24_hours ) ) { | |
| add_msg_if_player( m_neutral, _( "You shed a tooth!" ) ); | |
| g->m.spawn_item( pos(), "bone", 1 ); | |
| } | |
| if( has_active_mutation( trait_id( "WINGS_INSECT" ) ) ) { | |
| //~Sound of buzzing Insect Wings | |
| sounds::sound( pos(), 10, sounds::sound_t::movement, _( "BZZZZZ" ), false, "misc", "insect_wings" ); | |
| } | |
| bool wearing_shoes = is_wearing_shoes( side::LEFT ) || is_wearing_shoes( side::RIGHT ); | |
| int root_vitamins = 0; | |
| int root_water = 0; | |
| if( has_trait( trait_ROOTS3 ) && g->m.has_flag( "PLOWABLE", pos() ) && !wearing_shoes ) { | |
| root_vitamins += 1; | |
| if( get_thirst() <= -2000 ) { | |
| root_water += 51; | |
| } | |
| } | |
| if( x_in_y( root_vitamins, 576 ) ) { | |
| vitamin_mod( vitamin_id( "iron" ), 1, true ); | |
| vitamin_mod( vitamin_id( "calcium" ), 1, true ); | |
| mod_healthy_mod( 5, 50 ); | |
| } | |
| if( x_in_y( root_water, 2550 ) ) { | |
| // Plants draw some crazy amounts of water from the ground in real life, | |
| // so these numbers try to reflect that uncertain but large amount | |
| // this should take 12 hours to meet your daily needs with ROOTS2, and 8 with ROOTS3 | |
| mod_thirst( -1 ); | |
| } | |
| if( !in_sleep_state() ) { | |
| if( !has_trait( trait_id( "DEBUG_STORAGE" ) ) && ( weight_carried() > 4 * weight_capacity() ) ) { | |
| if( has_effect( effect_downed ) ) { | |
| add_effect( effect_downed, 1_turns, num_bp, false, 0, true ); | |
| } else { | |
| add_effect( effect_downed, 2_turns, num_bp, false, 0, true ); | |
| } | |
| } | |
| time_duration timer = -6_hours; | |
| if( has_trait( trait_ADDICTIVE ) ) { | |
| timer = -10_hours; | |
| } else if( has_trait( trait_NONADDICTIVE ) ) { | |
| timer = -3_hours; | |
| } | |
| for( auto &cur_addiction : addictions ) { | |
| if( cur_addiction.sated <= 0_turns && | |
| cur_addiction.intensity >= MIN_ADDICTION_LEVEL ) { | |
| addict_effect( *this, cur_addiction ); | |
| } | |
| cur_addiction.sated -= 1_turns; | |
| // Higher intensity addictions heal faster | |
| if( cur_addiction.sated - 10_minutes * cur_addiction.intensity < timer ) { | |
| if( cur_addiction.intensity <= 2 ) { | |
| rem_addiction( cur_addiction.type ); | |
| break; | |
| } else { | |
| cur_addiction.intensity--; | |
| cur_addiction.sated = 0_turns; | |
| } | |
| } | |
| } | |
| if( has_trait( trait_CHEMIMBALANCE ) ) { | |
| if( one_turn_in( 6_hours ) && !has_trait( trait_NOPAIN ) ) { | |
| add_msg_if_player( m_bad, _( "You suddenly feel sharp pain for no reason." ) ); | |
| mod_pain( 3 * rng( 1, 3 ) ); | |
| } | |
| if( one_turn_in( 6_hours ) ) { | |
| int pkilladd = 5 * rng( -1, 2 ); | |
| if( pkilladd > 0 ) { | |
| add_msg_if_player( m_bad, _( "You suddenly feel numb." ) ); | |
| } else if( ( pkilladd < 0 ) && ( !( has_trait( trait_NOPAIN ) ) ) ) { | |
| add_msg_if_player( m_bad, _( "You suddenly ache." ) ); | |
| } | |
| mod_painkiller( pkilladd ); | |
| } | |
| if( one_turn_in( 6_hours ) ) { | |
| add_msg_if_player( m_bad, _( "You feel dizzy for a moment." ) ); | |
| moves -= rng( 10, 30 ); | |
| } | |
| if( one_turn_in( 6_hours ) ) { | |
| int hungadd = 5 * rng( -1, 3 ); | |
| if( hungadd > 0 ) { | |
| add_msg_if_player( m_bad, _( "You suddenly feel hungry." ) ); | |
| } else { | |
| add_msg_if_player( m_good, _( "You suddenly feel a little full." ) ); | |
| } | |
| mod_hunger( hungadd ); | |
| } | |
| if( one_turn_in( 6_hours ) ) { | |
| add_msg_if_player( m_bad, _( "You suddenly feel thirsty." ) ); | |
| mod_thirst( 5 * rng( 1, 3 ) ); | |
| } | |
| if( one_turn_in( 6_hours ) ) { | |
| add_msg_if_player( m_good, _( "You feel fatigued all of a sudden." ) ); | |
| mod_fatigue( 10 * rng( 2, 4 ) ); | |
| } | |
| if( one_turn_in( 8_hours ) ) { | |
| if( one_in( 3 ) ) { | |
| add_morale( MORALE_FEELING_GOOD, 20, 100 ); | |
| } else { | |
| add_morale( MORALE_FEELING_BAD, -20, -100 ); | |
| } | |
| } | |
| if( one_turn_in( 6_hours ) ) { | |
| if( one_in( 3 ) ) { | |
| add_msg_if_player( m_bad, _( "You suddenly feel very cold." ) ); | |
| temp_cur.fill( BODYTEMP_VERY_COLD ); | |
| } else { | |
| add_msg_if_player( m_bad, _( "You suddenly feel cold." ) ); | |
| temp_cur.fill( BODYTEMP_COLD ); | |
| } | |
| } | |
| if( one_turn_in( 6_hours ) ) { | |
| if( one_in( 3 ) ) { | |
| add_msg_if_player( m_bad, _( "You suddenly feel very hot." ) ); | |
| temp_cur.fill( BODYTEMP_VERY_HOT ); | |
| } else { | |
| add_msg_if_player( m_bad, _( "You suddenly feel hot." ) ); | |
| temp_cur.fill( BODYTEMP_HOT ); | |
| } | |
| } | |
| } | |
| if( ( has_trait( trait_SCHIZOPHRENIC ) || has_artifact_with( AEP_SCHIZO ) ) ) { | |
| if( is_player() ) { | |
| bool done_effect = false; | |
| // Sound | |
| if( one_turn_in( 4_hours ) ) { | |
| sound_hallu(); | |
| } | |
| // Follower turns hostile | |
| if( !done_effect && one_turn_in( 4_hours ) ) { | |
| std::vector<std::shared_ptr<npc>> followers = overmap_buffer.get_npcs_near_player( 12 ); | |
| std::string who_gets_angry = name; | |
| if( !followers.empty() ) { | |
| who_gets_angry = random_entry_ref( followers )->name; | |
| } | |
| add_msg( m_bad, _( "%1$s gets angry!" ), who_gets_angry ); | |
| done_effect = true; | |
| } | |
| // Monster dies | |
| if( !done_effect && one_turn_in( 6_hours ) ) { | |
| // TODO: move to monster group json | |
| static const mtype_id mon_zombie( "mon_zombie" ); | |
| static const mtype_id mon_zombie_fat( "mon_zombie_fat" ); | |
| static const mtype_id mon_zombie_fireman( "mon_zombie_fireman" ); | |
| static const mtype_id mon_zombie_cop( "mon_zombie_cop" ); | |
| static const mtype_id mon_zombie_soldier( "mon_zombie_soldier" ); | |
| static const std::array<mtype_id, 5> monsters = { { | |
| mon_zombie, mon_zombie_fat, mon_zombie_fireman, mon_zombie_cop, mon_zombie_soldier | |
| } | |
| }; | |
| add_msg( _( "%s dies!" ), random_entry_ref( monsters )->nname() ); | |
| done_effect = true; | |
| } | |
| // Limb Breaks | |
| if( !done_effect && one_turn_in( 4_hours ) ) { | |
| add_msg( m_bad, _( "Your limb breaks!" ) ); | |
| done_effect = true; | |
| } | |
| // NPC chat | |
| if( !done_effect && one_turn_in( 4_hours ) ) { | |
| std::string i_name = Name::generate( one_in( 2 ) ); | |
| std::string i_talk = SNIPPET.random_from_category( "<lets_talk>" ); | |
| parse_tags( i_talk, *this, *this ); | |
| add_msg( _( "%1$s says: \"%2$s\"" ), i_name, i_talk ); | |
| done_effect = true; | |
| } | |
| // Skill raise | |
| if( !done_effect && one_turn_in( 12_hours ) ) { | |
| skill_id raised_skill = Skill::random_skill(); | |
| add_msg( m_good, _( "You increase %1$s to level %2$d." ), raised_skill.obj().name(), | |
| get_skill_level( raised_skill ) + 1 ); | |
| done_effect = true; | |
| } | |
| // Talk to self | |
| if( !done_effect && one_turn_in( 4_hours ) ) { | |
| std::vector<std::string> talk_s{ _( "Hey, can you hear me?" ), | |
| _( "Don't touch me." ), | |
| _( "What's your name?" ), | |
| _( "I thought you were my friend." ), | |
| _( "How are you today?" ), | |
| _( "Shut up! Don't lie to me." ), | |
| _( "Why would you do that?" ), | |
| _( "Please, don't go." ), | |
| _( "Don't leave me alone!" ), | |
| _( "Yeah, sure." ), | |
| _( "No way, man." ), | |
| _( "Do you really think so?" ), | |
| _( "Is it really time for that?" ), | |
| _( "Sorry, I can't hear you." ), | |
| _( "You've told me already." ), | |
| _( "I know!" ), | |
| _( "Why are you following me?" ), | |
| _( "This place is dangerous, you shouldn't be here." ), | |
| _( "What are you doing out here?" ), | |
| _( "That's not true, is it?" ), | |
| _( "Are you hurt?" ) }; | |
| add_msg( _( "%1$s says: \"%2$s\"" ), name, random_entry_ref( talk_s ) ); | |
| done_effect = true; | |
| } | |
| // Talking weapon | |
| if( !done_effect && !weapon.is_null() ) { | |
| // If player has a weapon, picks a message from said weapon | |
| // Weapon tells player to kill a monster if any are nearby | |
| // Weapon is concerned for player if bleeding | |
| // Weapon is concerned for itself if damaged | |
| // Otherwise random chit-chat | |
| std::string i_name_w = weapon.has_var( "item_label" ) ? | |
| weapon.get_var( "item_label" ) : | |
| _( "Your " ) + weapon.type_name(); | |
| std::vector<std::weak_ptr<monster>> mons = g->all_monsters().items; | |
| std::string i_talk_w; | |
| bool does_talk = false; | |
| if( !mons.empty() && one_turn_in( 12_minutes ) ) { | |
| std::vector<std::string> mon_near{ _( "Hey, let's go kill that %1$s!" ), | |
| _( "Did you see that %1$s!?" ), | |
| _( "I want to kill that %1$s!" ), | |
| _( "Let me kill that %1$s!" ), | |
| _( "Hey, I need to kill that %1$s!" ), | |
| _( "I want to watch that %1$s bleed!" ), | |
| _( "Wait, that %1$s needs to die!" ), | |
| _( "Go kill that %1$s!" ), | |
| _( "Look at that %1$s!" ), | |
| _( "That %1$s doesn't deserve to live!" ) }; | |
| std::vector<std::string> seen_mons; | |
| for( auto &n : mons ) { | |
| if( sees( *n.lock() ) ) { | |
| seen_mons.emplace_back( n.lock()->get_name() ); | |
| } | |
| } | |
| if( !seen_mons.empty() ) { | |
| std::string talk_w = random_entry_ref( mon_near ); | |
| i_talk_w = string_format( talk_w, random_entry_ref( seen_mons ) ); | |
| does_talk = true; | |
| } | |
| } else if( has_effect( effect_bleed ) && one_turn_in( 5_minutes ) ) { | |
| std::vector<std::string> bleeding{ _( "Hey, you're bleeding." ), | |
| _( "Your wound looks pretty bad." ), | |
| _( "Shouldn't you put a bandage on that?" ), | |
| _( "Please don't die! No one else lets me kill things!" ), | |
| _( "You look hurt, did I do that?" ), | |
| _( "Are you supposed to be bleeding?" ), | |
| _( "You're not going to die, are you?" ), | |
| _( "Kill a few more before you bleed out!" ) }; | |
| i_talk_w = random_entry_ref( bleeding ); | |
| does_talk = true; | |
| } else if( weapon.damage() >= weapon.max_damage() / 3 && one_turn_in( 1_hours ) ) { | |
| std::vector<std::string> damaged{ _( "Hey fix me up." ), | |
| _( "I need healing!" ), | |
| _( "I hurt all over..." ), | |
| _( "You can put me back together, right?" ), | |
| _( "I... I can't move my legs!" ), | |
| _( "Medic!" ), | |
| _( "I can still fight, don't replace me!" ), | |
| _( "They got me!" ), | |
| _( "Go on without me..." ), | |
| _( "Am I gonna die?" ) }; | |
| i_talk_w = random_entry_ref( damaged ); | |
| does_talk = true; | |
| } else if( one_turn_in( 4_hours ) ) { | |
| std::vector<std::string> misc{ _( "Let me kill something already!" ), | |
| _( "I'm your best friend, right?" ), | |
| _( "I love you!" ), | |
| _( "How are you today?" ), | |
| _( "Do you think it will rain today?" ), | |
| _( "Did you hear that?" ), | |
| _( "Try not to drop me." ), | |
| _( "How many do you think we've killed?" ), | |
| _( "I'll keep you safe!" ) }; | |
| i_talk_w = random_entry_ref( misc ); | |
| does_talk = true; | |
| } | |
| if( does_talk ) { | |
| add_msg( _( "%1$s says: \"%2$s\"" ), i_name_w, i_talk_w ); | |
| } | |
| done_effect = true; | |
| } | |
| // Bad feeling | |
| if( !done_effect && one_turn_in( 4_hours ) ) { | |
| add_msg( m_warning, _( "You get a bad feeling." ) ); | |
| add_morale( MORALE_FEELING_BAD, -50, -150 ); | |
| done_effect = true; | |
| } | |
| // Formication | |
| if( !done_effect && one_turn_in( 4_hours ) ) { | |
| body_part bp = random_body_part( true ); | |
| add_effect( effect_formication, 1_hours, bp ); | |
| done_effect = true; | |
| } | |
| // Numbness | |
| if( !done_effect && one_turn_in( 4_hours ) ) { | |
| add_msg( m_bad, _( "You suddenly feel so numb..." ) ); | |
| mod_painkiller( 25 ); | |
| done_effect = true; | |
| } | |
| // Hallucination | |
| if( !done_effect && one_turn_in( 6_hours ) ) { | |
| add_effect( effect_hallu, 6_hours ); | |
| done_effect = true; | |
| } | |
| // Visuals | |
| if( !done_effect && one_turn_in( 2_hours ) ) { | |
| add_effect( effect_visuals, rng( 15_turns, 60_turns ) ); | |
| done_effect = true; | |
| } | |
| // Shaking | |
| if( !done_effect && one_turn_in( 4_hours ) ) { | |
| add_msg( m_bad, _( "You start to shake uncontrollably." ) ); | |
| add_effect( effect_shakes, rng( 2_minutes, 5_minutes ) ); | |
| done_effect = true; | |
| } | |
| // Shout | |
| if( !done_effect && one_turn_in( 4_hours ) ) { | |
| std::vector<std::string> shouts{ _( "\"Get away from there!\"" ), | |
| _( "\"What do you think you're doing?\"" ), | |
| _( "\"Stop laughing at me!\"" ), | |
| _( "\"Don't point that thing at me!\"" ), | |
| _( "\"Stay away from me!\"" ), | |
| _( "\"No! Stop!\"" ), | |
| _( "\"Get the fuck away from me!\"" ), | |
| _( "\"That's not true!\"" ), | |
| _( "\"What do you want from me?\"" ), | |
| _( "\"I didn't mean to do it!\"" ), | |
| _( "\"It wasn't my fault!\"" ), | |
| _( "\"I had to do it!\"" ), | |
| _( "\"They made me do it!\"" ), | |
| _( "\"What are you!?\"" ), | |
| _( "\"I should never have trusted you!\"" ) }; | |
| std::string i_shout = random_entry_ref( shouts ); | |
| shout( "yourself shout, " + i_shout ); | |
| done_effect = true; | |
| } | |
| // Drop weapon | |
| if( !done_effect && one_turn_in( 2_days ) ) { | |
| if( !weapon.is_null() ) { | |
| std::string i_name_w = weapon.has_var( "item_label" ) ? | |
| weapon.get_var( "item_label" ) : | |
| "your " + weapon.type_name(); | |
| std::vector<std::string> drops{ "%1$s starts burning your hands!", | |
| "%1$s feels freezing cold!", | |
| "An electric shock shoots into your hand from %1$s!", | |
| "%1$s lied to you.", | |
| "%1$s said something stupid.", | |
| "%1$s is running away!" }; | |
| std::string str = string_format( random_entry_ref( drops ), i_name_w ); | |
| str[0] = toupper( str[0] ); | |
| add_msg( m_bad, str ); | |
| drop( get_item_position( &weapon ), pos() ); | |
| } | |
| // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores) | |
| done_effect = true; | |
| } | |
| } | |
| } | |
| if( ( has_trait( trait_NARCOLEPTIC ) || has_artifact_with( AEP_SCHIZO ) ) ) { | |
| if( one_turn_in( 8_hours ) ) { | |
| add_msg( m_bad, _( "You're suddenly overcome with the urge to sleep and you pass out." ) ); | |
| add_effect( effect_lying_down, 20_minutes ); | |
| } | |
| } | |
| if( has_trait( trait_JITTERY ) && !has_effect( effect_shakes ) ) { | |
| if( stim > 50 && one_in( to_turns<int>( 30_minutes ) - ( stim * 6 ) ) ) { | |
| add_effect( effect_shakes, 30_minutes + 1_turns * stim ); | |
| } else if( ( get_hunger() > 80 || get_kcal_percent() < 1.0f ) && get_hunger() > 0 && | |
| one_in( to_turns<int>( 50_minutes ) - ( get_hunger() * 6 ) ) ) { | |
| add_effect( effect_shakes, 40_minutes ); | |
| } | |
| } | |
| if( has_trait( trait_MOODSWINGS ) && one_turn_in( 6_hours ) ) { | |
| if( rng( 1, 20 ) > 9 ) { // 55% chance | |
| add_morale( MORALE_MOODSWING, -100, -500 ); | |
| } else { // 45% chance | |
| add_morale( MORALE_MOODSWING, 100, 500 ); | |
| } | |
| } | |
| if( has_trait( trait_VOMITOUS ) && one_turn_in( 7_hours ) ) { | |
| vomit(); | |
| } | |
| if( has_trait( trait_SHOUT1 ) && one_turn_in( 6_hours ) ) { | |
| shout(); | |
| } | |
| if( has_trait( trait_SHOUT2 ) && one_turn_in( 4_hours ) ) { | |
| shout(); | |
| } | |
| if( has_trait( trait_SHOUT3 ) && one_turn_in( 3_hours ) ) { | |
| shout(); | |
| } | |
| if( has_trait( trait_M_SPORES ) && one_turn_in( 4_hours ) ) { | |
| spores(); | |
| } | |
| if( has_trait( trait_M_BLOSSOMS ) && one_turn_in( 3_hours ) ) { | |
| blossoms(); | |
| } | |
| } // Done with while-awake-only effects | |
| if( has_trait( trait_ASTHMA ) && | |
| one_in( ( to_turns<int>( 6_hours ) - stim * 300 ) * ( has_effect( effect_sleep ) ? 10 : 1 ) ) && | |
| !has_effect( effect_adrenaline ) & !has_effect( effect_datura ) ) { | |
| bool auto_use = has_charges( "inhaler", 1 ) || has_charges( "oxygen_tank", 1 ) || | |
| has_charges( "smoxygen_tank", 1 ); | |
| bool oxygenator = has_bionic( bio_gills ) && power_level >= 3; | |
| if( underwater ) { | |
| oxygen = oxygen / 2; | |
| auto_use = false; | |
| } | |
| if( in_sleep_state() && !has_effect( effect_narcosis ) ) { | |
| inventory map_inv; | |
| map_inv.form_from_map( g->u.pos(), 2 ); | |
| // check if character has an oxygenator first | |
| if( oxygenator ) { | |
| add_msg_if_player( m_bad, _( "You have an asthma attack!" ) ); | |
| charge_power( -3 ); | |
| add_msg_if_player( m_info, _( "You use your Oxygenator to clear it up, then go back to sleep." ) ); | |
| } else if( auto_use ) { | |
| add_msg_if_player( m_bad, _( "You have an asthma attack!" ) ); | |
| if( use_charges_if_avail( "inhaler", 1 ) ) { | |
| add_msg_if_player( m_info, _( "You use your inhaler and go back to sleep." ) ); | |
| } else if( use_charges_if_avail( "oxygen_tank", 1 ) || | |
| use_charges_if_avail( "smoxygen_tank", 1 ) ) { | |
| add_msg_if_player( m_info, | |
| _( "You take a deep breath from your oxygen tank and go back to sleep." ) ); | |
| } | |
| // check if an inhaler is somewhere near | |
| } else if( map_inv.has_charges( "inhaler", 1 ) || map_inv.has_charges( "oxygen_tank", 1 ) || | |
| map_inv.has_charges( "smoxygen_tank", 1 ) ) { | |
| add_msg_if_player( m_bad, _( "You have an asthma attack!" ) ); | |
| // create new variable to resolve a reference issue | |
| int amount = 1; | |
| if( !g->m.use_charges( g->u.pos(), 2, "inhaler", amount ).empty() ) { | |
| add_msg_if_player( m_info, _( "You use your inhaler and go back to sleep." ) ); | |
| } else if( !g->m.use_charges( g->u.pos(), 2, "oxygen_tank", amount ).empty() || | |
| !g->m.use_charges( g->u.pos(), 2, "smoxygen_tank", amount ).empty() ) { | |
| add_msg_if_player( m_info, | |
| _( "You take a deep breath from your oxygen tank and go back to sleep." ) ); | |
| } | |
| } else { | |
| add_effect( effect_asthma, rng( 5_minutes, 20_minutes ) ); | |
| if( has_effect( effect_sleep ) ) { | |
| wake_up(); | |
| } else { | |
| g->cancel_activity_or_ignore_query( distraction_type::asthma, _( "You have an asthma attack!" ) ); | |
| } | |
| } | |
| } else if( auto_use ) { | |
| int charges = 0; | |
| if( use_charges_if_avail( "inhaler", 1 ) ) { | |
| moves -= 40; | |
| charges = charges_of( "inhaler" ); | |
| add_msg_if_player( m_bad, _( "You have an asthma attack!" ) ); | |
| if( charges == 0 ) { | |
| add_msg_if_player( m_bad, _( "You use your last inhaler charge." ) ); | |
| } else { | |
| add_msg_if_player( m_info, ngettext( "You use your inhaler, only %d charge left.", | |
| "You use your inhaler, only %d charges left.", charges ), | |
| charges ); | |
| } | |
| } else if( use_charges_if_avail( "oxygen_tank", 1 ) || | |
| use_charges_if_avail( "smoxygen_tank", 1 ) ) { | |
| moves -= 500; // synched with use action | |
| charges = charges_of( "oxygen_tank" ) + charges_of( "smoxygen_tank" ); | |
| add_msg_if_player( m_bad, _( "You have an asthma attack!" ) ); | |
| if( charges == 0 ) { | |
| add_msg_if_player( m_bad, _( "You breathe in last bit of oxygen from the tank." ) ); | |
| } else { | |
| add_msg_if_player( m_info, | |
| ngettext( "You take a deep breath from your oxygen tank, only %d charge left.", | |
| "You take a deep breath from your oxygen tank, only %d charges left.", charges ), | |
| charges ); | |
| } | |
| } | |
| } else { | |
| add_effect( effect_asthma, rng( 5_minutes, 20_minutes ) ); | |
| if( !is_npc() ) { | |
| g->cancel_activity_or_ignore_query( distraction_type::asthma, _( "You have an asthma attack!" ) ); | |
| } | |
| } | |
| } | |
| double sleeve_factor = armwear_factor(); | |
| const bool has_hat = wearing_something_on( bp_head ); | |
| const bool leafy = has_trait( trait_LEAVES ) || has_trait( trait_LEAVES2 ) || | |
| has_trait( trait_LEAVES3 ); | |
| const bool leafier = has_trait( trait_LEAVES2 ) || has_trait( trait_LEAVES3 ); | |
| const bool leafiest = has_trait( trait_LEAVES3 ); | |
| int sunlight_nutrition = 0; | |
| if( leafy && g->is_in_sunlight( pos() ) ) { | |
| const int player_local_temp = g->weather.get_temperature( pos() ); | |
| int flux = ( player_local_temp - 65 ) / 2; | |
| if( !has_hat ) { | |
| sunlight_nutrition += 100 + flux; | |
| } | |
| if( leafier ) { | |
| int rate = ( ( 100 * sleeve_factor ) + flux ) * 2; | |
| sunlight_nutrition += rate * ( leafiest ? 2 : 1 ); | |
| } | |
| } | |
| if( x_in_y( sunlight_nutrition, 18000 ) ) { | |
| vitamin_mod( vitamin_id( "vitA" ), 1, true ); | |
| vitamin_mod( vitamin_id( "vitC" ), 1, true ); | |
| } | |
| if( x_in_y( sunlight_nutrition, 12000 ) ) { | |
| mod_hunger( -1 ); | |
| // photosynthesis absorbs kcal directly | |
| mod_stored_nutr( -1 ); | |
| } | |
| if( get_pain() > 0 ) { | |
| if( has_trait( trait_PAINREC1 ) && one_turn_in( 1_hours ) ) { | |
| mod_pain( -1 ); | |
| } | |
| if( has_trait( trait_PAINREC2 ) && one_turn_in( 30_minutes ) ) { | |
| mod_pain( -1 ); | |
| } | |
| if( has_trait( trait_PAINREC3 ) && one_turn_in( 15_minutes ) ) { | |
| mod_pain( -1 ); | |
| } | |
| } | |
| if( ( has_trait( trait_ALBINO ) || has_effect( effect_datura ) ) && | |
| g->is_in_sunlight( pos() ) && one_turn_in( 1_minutes ) ) { | |
| // Umbrellas can keep the sun off the skin and sunglasses - off the eyes. | |
| if( !weapon.has_flag( "RAIN_PROTECT" ) ) { | |
| add_msg_if_player( m_bad, _( "The sunlight is really irritating your skin." ) ); | |
| if( has_effect( effect_sleep ) && !has_effect( effect_narcosis ) ) { | |
| wake_up(); | |
| } | |
| if( one_turn_in( 1_minutes ) ) { | |
| mod_pain( 1 ); | |
| } else { | |
| focus_pool --; | |
| } | |
| } | |
| if( !( ( ( worn_with_flag( "SUN_GLASSES" ) ) || worn_with_flag( "BLIND" ) ) && | |
| ( wearing_something_on( bp_eyes ) ) ) | |
| && !has_bionic( bionic_id( "bio_sunglasses" ) ) ) { | |
| add_msg_if_player( m_bad, _( "The sunlight is really irritating your eyes." ) ); | |
| if( one_turn_in( 1_minutes ) ) { | |
| mod_pain( 1 ); | |
| } else { | |
| focus_pool --; | |
| } | |
| } | |
| } | |
| if( has_trait( trait_SUNBURN ) && g->is_in_sunlight( pos() ) && one_in( 10 ) ) { | |
| if( !( weapon.has_flag( "RAIN_PROTECT" ) ) ) { | |
| add_msg_if_player( m_bad, _( "The sunlight burns your skin!" ) ); | |
| if( has_effect( effect_sleep ) && !has_effect( effect_narcosis ) ) { | |
| wake_up(); | |
| } | |
| mod_pain( 1 ); | |
| hurtall( 1, nullptr ); | |
| } | |
| } | |
| if( ( has_trait( trait_TROGLO ) || has_trait( trait_TROGLO2 ) ) && | |
| g->is_in_sunlight( pos() ) && g->weather.weather == WEATHER_SUNNY ) { | |
| mod_str_bonus( -1 ); | |
| mod_dex_bonus( -1 ); | |
| add_miss_reason( _( "The sunlight distracts you." ), 1 ); | |
| mod_int_bonus( -1 ); | |
| mod_per_bonus( -1 ); | |
| } | |
| if( has_trait( trait_TROGLO2 ) && g->is_in_sunlight( pos() ) ) { | |
| mod_str_bonus( -1 ); | |
| mod_dex_bonus( -1 ); | |
| add_miss_reason( _( "The sunlight distracts you." ), 1 ); | |
| mod_int_bonus( -1 ); | |
| mod_per_bonus( -1 ); | |
| } | |
| if( has_trait( trait_TROGLO3 ) && g->is_in_sunlight( pos() ) ) { | |
| mod_str_bonus( -4 ); | |
| mod_dex_bonus( -4 ); | |
| add_miss_reason( _( "You can't stand the sunlight!" ), 4 ); | |
| mod_int_bonus( -4 ); | |
| mod_per_bonus( -4 ); | |
| } | |
| if( has_trait( trait_SORES ) ) { | |
| for( const body_part bp : all_body_parts ) { | |
| if( bp == bp_head ) { | |
| continue; | |
| } | |
| int sores_pain = 5 + 0.4 * abs( encumb( bp ) ); | |
| if( get_pain() < sores_pain ) { | |
| set_pain( sores_pain ); | |
| } | |
| } | |
| } | |
| //Web Weavers...weave web | |
| if( has_active_mutation( trait_WEB_WEAVER ) && !in_vehicle ) { | |
| // this adds intensity to if its not already there. | |
| g->m.add_field( pos(), fd_web, 1 ); | |
| } | |
| // Blind/Deaf for brief periods about once an hour, | |
| // and visuals about once every 30 min. | |
| if( has_trait( trait_PER_SLIME ) ) { | |
| if( one_turn_in( 1_hours ) && !has_effect( effect_deaf ) ) { | |
| add_msg_if_player( m_bad, _( "Suddenly, you can't hear anything!" ) ); | |
| add_effect( effect_deaf, rng( 20_minutes, 60_minutes ) ); | |
| } | |
| if( one_turn_in( 1_hours ) && !( has_effect( effect_blind ) ) ) { | |
| add_msg_if_player( m_bad, _( "Suddenly, your eyes stop working!" ) ); | |
| add_effect( effect_blind, rng( 2_minutes, 6_minutes ) ); | |
| } | |
| // Yes, you can be blind and hallucinate at the same time. | |
| // Your post-human biology is truly remarkable. | |
| if( one_turn_in( 30_minutes ) && !( has_effect( effect_visuals ) ) ) { | |
| add_msg_if_player( m_bad, _( "Your visual centers must be acting up..." ) ); | |
| add_effect( effect_visuals, rng( 36_minutes, 72_minutes ) ); | |
| } | |
| } | |
| if( has_trait( trait_WEB_SPINNER ) && !in_vehicle && one_in( 3 ) ) { | |
| // this adds intensity to if its not already there. | |
| g->m.add_field( pos(), fd_web, 1 ); | |
| } | |
| if( has_trait( trait_UNSTABLE ) && !has_trait( trait_CHAOTIC_BAD ) && one_turn_in( 48_hours ) ) { | |
| mutate(); | |
| } | |
| if( ( has_trait( trait_CHAOTIC ) || has_trait( trait_CHAOTIC_BAD ) ) && one_turn_in( 12_hours ) ) { | |
| mutate(); | |
| } | |
| if( has_artifact_with( AEP_MUTAGENIC ) && one_turn_in( 48_hours ) ) { | |
| mutate(); | |
| } | |
| if( has_artifact_with( AEP_FORCE_TELEPORT ) && one_turn_in( 1_hours ) ) { | |
| g->teleport( this ); | |
| } | |
| const bool needs_fire = !has_morale( MORALE_PYROMANIA_NEARFIRE ) && | |
| !has_morale( MORALE_PYROMANIA_STARTFIRE ); | |
| if( has_trait( trait_PYROMANIA ) && needs_fire && !in_sleep_state() && | |
| calendar::once_every( 2_hours ) ) { | |
| add_morale( MORALE_PYROMANIA_NOFIRE, -1, -30, 24_hours, 24_hours ); | |
| if( calendar::once_every( 4_hours ) ) { | |
| std::string smokin_hot_fiyah = SNIPPET.random_from_category( "pyromania_withdrawal" ); | |
| add_msg_if_player( m_bad, _( smokin_hot_fiyah ) ); | |
| } | |
| } | |
| if( has_trait( trait_KILLER ) && !has_morale( MORALE_KILLER_HAS_KILLED ) && | |
| calendar::once_every( 2_hours ) ) { | |
| add_morale( MORALE_KILLER_NEED_TO_KILL, -1, -30, 24_hours, 24_hours ); | |
| if( calendar::once_every( 4_hours ) ) { | |
| std::string snip = SNIPPET.random_from_category( "killer_withdrawal" ); | |
| add_msg_if_player( m_bad, _( snip ) ); | |
| } | |
| } | |
| // checking for radioactive items in inventory | |
| const int item_radiation = leak_level( "RADIOACTIVE" ); | |
| const int map_radiation = g->m.get_radiation( pos() ); | |
| float rads = map_radiation / 100.0f + item_radiation / 10.0f; | |
| int rad_mut = 0; | |
| if( has_trait( trait_RADIOACTIVE3 ) ) { | |
| rad_mut = 3; | |
| } else if( has_trait( trait_RADIOACTIVE2 ) ) { | |
| rad_mut = 2; | |
| } else if( has_trait( trait_RADIOACTIVE1 ) ) { | |
| rad_mut = 1; | |
| } | |
| // Spread less radiation when sleeping (slower metabolism etc.) | |
| // Otherwise it can quickly get to the point where you simply can't sleep at all | |
| const bool rad_mut_proc = rad_mut > 0 && | |
| x_in_y( rad_mut, in_sleep_state() ? HOURS( 3 ) : MINUTES( 30 ) ); | |
| bool has_helmet = false; | |
| const bool power_armored = is_wearing_power_armor( &has_helmet ); | |
| const bool rad_resist = power_armored || worn_with_flag( "RAD_RESIST" ); | |
| if( rad_mut > 0 ) { | |
| const bool kept_in = is_rad_immune() || ( rad_resist && !one_in( 4 ) ); | |
| if( kept_in ) { | |
| // As if standing on a map tile with radiation level equal to rad_mut | |
| rads += rad_mut / 100.0f; | |
| } | |
| if( rad_mut_proc && !kept_in ) { | |
| // Irradiate a random nearby point | |
| // If you can't, irradiate the player instead | |
| tripoint rad_point = pos() + point( rng( -3, 3 ), rng( -3, 3 ) ); | |
| // TODO: Radioactive vehicles? | |
| if( g->m.get_radiation( rad_point ) < rad_mut ) { | |
| g->m.adjust_radiation( rad_point, 1 ); | |
| } else { | |
| rads += rad_mut; | |
| } | |
| } | |
| } | |
| // Used to control vomiting from radiation to make it not-annoying | |
| bool radiation_increasing = irradiate( rads ); | |
| if( radiation_increasing && calendar::once_every( 3_minutes ) && has_bionic( bio_geiger ) ) { | |
| add_msg_if_player( m_warning, | |
| _( "You feel an anomalous sensation coming from your radiation sensors." ) ); | |
| } | |
| if( calendar::once_every( 15_minutes ) ) { | |
| if( radiation < 0 ) { | |
| radiation = 0; | |
| } else if( radiation > 2000 ) { | |
| radiation = 2000; | |
| } | |
| if( get_option<bool>( "RAD_MUTATION" ) && rng( 100, 10000 ) < radiation ) { | |
| mutate(); | |
| radiation -= 50; | |
| } else if( radiation > 50 && rng( 1, 3000 ) < radiation && ( stomach.contains() > 0_ml || | |
| radiation_increasing || !in_sleep_state() ) ) { | |
| vomit(); | |
| radiation -= 1; | |
| } | |
| } | |
| const bool radiogenic = has_trait( trait_RADIOGENIC ); | |
| if( radiogenic && calendar::once_every( 30_minutes ) && radiation > 0 ) { | |
| // At 200 irradiation, twice as fast as REGEN | |
| if( x_in_y( radiation, 200 ) ) { | |
| healall( 1 ); | |
| if( rad_mut == 0 ) { | |
| // Don't heal radiation if we're generating it naturally | |
| // That would counter the main downside of radioactivity | |
| radiation -= 5; | |
| } | |
| } | |
| } | |
| if( !radiogenic && radiation > 0 ) { | |
| // Even if you heal the radiation itself, the damage is done. | |
| const int hmod = get_healthy_mod(); | |
| if( hmod > 200 - radiation ) { | |
| set_healthy_mod( std::max( -200, 200 - radiation ) ); | |
| } | |
| } | |
| if( radiation > 200 && calendar::once_every( 10_minutes ) && x_in_y( radiation, 1000 ) ) { | |
| hurtall( 1, nullptr ); | |
| radiation -= 5; | |
| } | |
| if( reactor_plut || tank_plut || slow_rad ) { | |
| // Microreactor CBM and supporting bionics | |
| if( has_bionic( bio_reactor ) || has_bionic( bio_advreactor ) ) { | |
| //first do the filtering of plutonium from storage to reactor | |
| if( tank_plut > 0 ) { | |
| int plut_trans; | |
| if( has_active_bionic( bio_plut_filter ) ) { | |
| plut_trans = tank_plut * 0.025; | |
| } else { | |
| plut_trans = tank_plut * 0.005; | |
| } | |
| if( plut_trans < 1 ) { | |
| plut_trans = 1; | |
| } | |
| tank_plut -= plut_trans; | |
| reactor_plut += plut_trans; | |
| } | |
| //leaking radiation, reactor is unshielded, but still better than a simple tank | |
| slow_rad += ( ( tank_plut * 0.1 ) + ( reactor_plut * 0.01 ) ); | |
| //begin power generation | |
| if( reactor_plut > 0 ) { | |
| int power_gen = 0; | |
| if( has_bionic( bio_advreactor ) ) { | |
| if( ( reactor_plut * 0.05 ) > 2000 ) { | |
| power_gen = 2000; | |
| } else { | |
| power_gen = reactor_plut * 0.05; | |
| if( power_gen < 1 ) { | |
| power_gen = 1; | |
| } | |
| } | |
| slow_rad += ( power_gen * 3 ); | |
| while( slow_rad >= 50 ) { | |
| if( power_gen >= 1 ) { | |
| slow_rad -= 50; | |
| power_gen -= 1; | |
| reactor_plut -= 1; | |
| } else { | |
| break; | |
| } | |
| } | |
| } else if( has_bionic( bio_reactor ) ) { | |
| if( ( reactor_plut * 0.025 ) > 500 ) { | |
| power_gen = 500; | |
| } else { | |
| power_gen = reactor_plut * 0.025; | |
| if( power_gen < 1 ) { | |
| power_gen = 1; | |
| } | |
| } | |
| slow_rad += ( power_gen * 3 ); | |
| } | |
| reactor_plut -= power_gen; | |
| while( power_gen >= 250 ) { | |
| apply_damage( nullptr, bp_torso, 1 ); | |
| mod_pain( 1 ); | |
| add_msg_if_player( m_bad, _( "Your chest burns as your power systems overload!" ) ); | |
| charge_power( 50 ); | |
| power_gen -= 60; // ten units of power lost due to short-circuiting into you | |
| } | |
| charge_power( power_gen ); | |
| } | |
| } else { | |
| slow_rad += ( ( ( reactor_plut * 0.4 ) + ( tank_plut * 0.4 ) ) * 100 ); | |
| //plutonium in body without any kind of container. Not good at all. | |
| reactor_plut *= 0.6; | |
| tank_plut *= 0.6; | |
| } | |
| while( slow_rad >= 1000 ) { | |
| radiation += 1; | |
| slow_rad -= 1000; | |
| } | |
| } | |
| // Negative bionics effects | |
| if( has_bionic( bio_dis_shock ) && one_turn_in( 2_hours ) && !has_effect( effect_narcosis ) ) { | |
| add_msg_if_player( m_bad, _( "You suffer a painful electrical discharge!" ) ); | |
| mod_pain( 1 ); | |
| moves -= 150; | |
| if( weapon.typeId() == "e_handcuffs" && weapon.charges > 0 ) { | |
| weapon.charges -= rng( 1, 3 ) * 50; | |
| if( weapon.charges < 1 ) { | |
| weapon.charges = 1; | |
| } | |
| add_msg_if_player( m_good, _( "The %s seems to be affected by the discharge." ), | |
| weapon.tname() ); | |
| } | |
| sfx::play_variant_sound( "bionics", "elec_discharge", 100 ); | |
| } | |
| if( has_bionic( bio_dis_acid ) && one_turn_in( 150_minutes ) ) { | |
| add_msg_if_player( m_bad, _( "You suffer a burning acidic discharge!" ) ); | |
| hurtall( 1, nullptr ); | |
| sfx::play_variant_sound( "bionics", "acid_discharge", 100 ); | |
| sfx::do_player_death_hurt( g->u, false ); | |
| } | |
| if( has_bionic( bio_drain ) && power_level > 24 && one_turn_in( 1_hours ) ) { | |
| add_msg_if_player( m_bad, _( "Your batteries discharge slightly." ) ); | |
| charge_power( -25 ); | |
| sfx::play_variant_sound( "bionics", "elec_crackle_low", 100 ); | |
| } | |
| if( has_bionic( bio_noise ) && one_turn_in( 50_minutes ) && | |
| !has_effect( effect_narcosis ) ) { | |
| // TODO: NPCs with said bionic | |
| if( !is_deaf() ) { | |
| add_msg( m_bad, _( "A bionic emits a crackle of noise!" ) ); | |
| sfx::play_variant_sound( "bionics", "elec_blast", 100 ); | |
| } else { | |
| add_msg( m_bad, _( "You feel your faulty bionic shuddering." ) ); | |
| sfx::play_variant_sound( "bionics", "elec_blast_muffled", 100 ); | |
| } | |
| sounds::sound( pos(), 60, sounds::sound_t::movement, _( "Crackle!" ) ); //sfx above | |
| } | |
| if( has_bionic( bio_power_weakness ) && max_power_level > 0 && | |
| power_level >= max_power_level * .75 ) { | |
| mod_str_bonus( -3 ); | |
| } | |
| if( has_bionic( bio_trip ) && one_turn_in( 50_minutes ) && | |
| !has_effect( effect_visuals ) && | |
| !has_effect( effect_narcosis ) && !in_sleep_state() ) { | |
| add_msg_if_player( m_bad, _( "Your vision pixelates!" ) ); | |
| add_effect( effect_visuals, 10_minutes ); | |
| sfx::play_variant_sound( "bionics", "pixelated", 100 ); | |
| } | |
| if( has_bionic( bio_spasm ) && one_turn_in( 5_hours ) && !has_effect( effect_downed ) && | |
| !has_effect( effect_narcosis ) ) { | |
| add_msg_if_player( m_bad, | |
| _( "Your malfunctioning bionic causes you to spasm and fall to the floor!" ) ); | |
| mod_pain( 1 ); | |
| add_effect( effect_stunned, 1_turns ); | |
| add_effect( effect_downed, 1_turns, num_bp, false, 0, true ); | |
| sfx::play_variant_sound( "bionics", "elec_crackle_high", 100 ); | |
| } | |
| if( has_bionic( bio_shakes ) && power_level > 24 && one_turn_in( 2_hours ) ) { | |
| add_msg_if_player( m_bad, _( "Your bionics short-circuit, causing you to tremble and shiver." ) ); | |
| charge_power( -25 ); | |
| add_effect( effect_shakes, 5_minutes ); | |
| sfx::play_variant_sound( "bionics", "elec_crackle_med", 100 ); | |
| } | |
| if( has_bionic( bio_leaky ) && one_turn_in( 6_minutes ) ) { | |
| mod_healthy_mod( -1, -200 ); | |
| } | |
| if( has_bionic( bio_sleepy ) && one_turn_in( 50_minutes ) && !in_sleep_state() ) { | |
| mod_fatigue( 1 ); | |
| } | |
| if( has_bionic( bio_itchy ) && one_turn_in( 50_minutes ) && !has_effect( effect_formication ) && | |
| !has_effect( effect_narcosis ) ) { | |
| add_msg_if_player( m_bad, _( "Your malfunctioning bionic itches!" ) ); | |
| body_part bp = random_body_part( true ); | |
| add_effect( effect_formication, 10_minutes, bp ); | |
| } | |
| if( has_bionic( bio_glowy ) && !has_effect( effect_glowy_led ) && one_turn_in( 50_minutes ) && | |
| power_level > 1 ) { | |
| add_msg_if_player( m_bad, _( "Your malfunctioning bionic starts to glow!" ) ); | |
| add_effect( effect_glowy_led, 5_minutes ); | |
| charge_power( -1 ); | |
| } | |
| // Artifact effects | |
| if( has_artifact_with( AEP_ATTENTION ) ) { | |
| add_effect( effect_attention, 3_turns ); | |
| } | |
| // Stim +250 kills | |
| if( stim > 210 ) { | |
| if( one_turn_in( 2_minutes ) && !has_effect( effect_downed ) ) { | |
| add_msg_if_player( m_bad, _( "Your muscles spasm!" ) ); | |
| if( !has_effect( effect_downed ) ) { | |
| add_msg_if_player( m_bad, _( "You fall to the ground!" ) ); | |
| add_effect( effect_downed, rng( 6_turns, 20_turns ) ); | |
| } | |
| } | |
| } | |
| if( stim > 170 ) { | |
| if( !has_effect( effect_winded ) && calendar::once_every( 10_minutes ) ) { | |
| add_msg( m_bad, _( "You feel short of breath." ) ); | |
| add_effect( effect_winded, 10_minutes + 1_turns ); | |
| } | |
| } | |
| if( stim > 110 ) { | |
| if( !has_effect( effect_shakes ) && calendar::once_every( 10_minutes ) ) { | |
| add_msg( _( "You shake uncontrollably." ) ); | |
| add_effect( effect_shakes, 15_minutes + 1_turns ); | |
| } | |
| } | |
| if( stim > 75 ) { | |
| if( !one_turn_in( 2_minutes ) && !has_effect( effect_nausea ) ) { | |
| add_msg( _( "You feel nauseous..." ) ); | |
| add_effect( effect_nausea, 5_minutes ); | |
| } | |
| } | |
| // Stim -200 or painkillers 240 kills | |
| if( stim < -160 || pkill > 200 ) { | |
| if( one_turn_in( 3_minutes ) && !in_sleep_state() ) { | |
| add_msg_if_player( m_bad, _( "You black out!" ) ); | |
| const time_duration dur = rng( 30_minutes, 60_minutes ); | |
| add_effect( effect_downed, dur ); | |
| add_effect( effect_blind, dur ); | |
| fall_asleep( dur ); | |
| } | |
| } | |
| if( stim < -120 || pkill > 160 ) { | |
| if( !has_effect( effect_winded ) && calendar::once_every( 10_minutes ) ) { | |
| add_msg( m_bad, _( "Your breathing slows down." ) ); | |
| add_effect( effect_winded, 10_minutes + 1_turns ); | |
| } | |
| } | |
| if( stim < -85 || pkill > 145 ) { | |
| if( one_turn_in( 15_seconds ) ) { | |
| add_msg_if_player( m_bad, _( "You feel dizzy for a moment." ) ); | |
| mod_moves( -rng( 10, 30 ) ); | |
| if( one_in( 3 ) && !has_effect( effect_downed ) ) { | |
| add_msg_if_player( m_bad, _( "You stumble and fall over!" ) ); | |
| add_effect( effect_downed, rng( 3_turns, 10_turns ) ); | |
| } | |
| } | |
| } | |
| if( stim < -60 || pkill > 130 ) { | |
| if( calendar::once_every( 10_minutes ) ) { | |
| add_msg( m_warning, _( "You feel tired..." ) ); | |
| mod_fatigue( rng( 1, 2 ) ); | |
| } | |
| } | |
| int sleep_deprivation = !in_sleep_state() ? get_sleep_deprivation() : 0; | |
| // Stimulants can lessen the PERCEIVED effects of sleep deprivation, but | |
| // they do nothing to cure it. As such, abuse is even more dangerous now. | |
| if( stim > 0 ) { | |
| // 100% of blacking out = 20160sd ; Max. stim modifier = 12500sd @ 250stim | |
| // Note: Very high stim already has its own slew of bad effects, | |
| // so the "useful" part of this bonus is actually lower. | |
| sleep_deprivation -= stim * 50; | |
| } | |
| // Harmless warnings | |
| if( sleep_deprivation >= SLEEP_DEPRIVATION_HARMLESS ) { | |
| if( one_turn_in( 50_minutes ) ) { | |
| switch( dice( 1, 4 ) ) { | |
| default: | |
| case 1: | |
| add_msg_player_or_npc( m_warning, _( "You tiredly rub your eyes." ), | |
| _( "<npcname> tiredly rubs their eyes." ) ); | |
| break; | |
| case 2: | |
| add_msg_player_or_npc( m_warning, _( "You let out a small yawn." ), | |
| _( "<npcname> lets out a small yawn." ) ); | |
| break; | |
| case 3: | |
| add_msg_player_or_npc( m_warning, _( "You stretch your back." ), | |
| _( "<npcname> streches their back." ) ); | |
| break; | |
| case 4: | |
| add_msg_player_or_npc( m_warning, _( "You feel mentally tired." ), | |
| _( "<npcname> lets out a huge yawn." ) ); | |
| break; | |
| } | |
| } | |
| } | |
| // Minor discomfort | |
| if( sleep_deprivation >= SLEEP_DEPRIVATION_MINOR ) { | |
| if( one_turn_in( 75_minutes ) ) { | |
| add_msg_if_player( m_warning, _( "You feel lightheaded for a moment." ) ); | |
| moves -= 10; | |
| } | |
| if( one_turn_in( 100_minutes ) ) { | |
| add_msg_if_player( m_warning, _( "Your muscles spasm uncomfortably." ) ); | |
| mod_pain( 2 ); | |
| } | |
| if( !has_effect( effect_visuals ) && one_turn_in( 150_minutes ) ) { | |
| add_msg_if_player( m_warning, _( "Your vision blurs a little." ) ); | |
| add_effect( effect_visuals, rng( 1_minutes, 5_minutes ) ); | |
| } | |
| } | |
| // Slight disability | |
| if( sleep_deprivation >= SLEEP_DEPRIVATION_SERIOUS ) { | |
| if( one_turn_in( 75_minutes ) ) { | |
| add_msg_if_player( m_bad, _( "Your mind lapses into unawareness briefly." ) ); | |
| moves -= rng( 20, 80 ); | |
| } | |
| if( one_turn_in( 125_minutes ) ) { | |
| add_msg_if_player( m_bad, _( "Your muscles ache in stressfully unpredictable ways." ) ); | |
| mod_pain( rng( 2, 10 ) ); | |
| } | |
| if( one_turn_in( 5_hours ) ) { | |
| add_msg_if_player( m_bad, _( "You have a distractingly painful headache." ) ); | |
| mod_pain( rng( 10, 25 ) ); | |
| } | |
| } | |
| // Major disability, high chance of passing out also relevant | |
| if( sleep_deprivation >= SLEEP_DEPRIVATION_MAJOR ) { | |
| if( !has_effect( effect_nausea ) && one_turn_in( 500_minutes ) ) { | |
| add_msg_if_player( m_bad, _( "You feel heartburn and an acid taste in your mouth." ) ); | |
| mod_pain( 5 ); | |
| add_effect( effect_nausea, rng( 5_minutes, 30_minutes ) ); | |
| } | |
| if( one_turn_in( 5_hours ) ) { | |
| add_msg_if_player( m_bad, | |
| _( "Your mind is so tired that you feel you can't trust your eyes anymore." ) ); | |
| add_effect( effect_hallu, rng( 5_minutes, 60_minutes ) ); | |
| } | |
| if( !has_effect( effect_shakes ) && one_turn_in( 425_minutes ) ) { | |
| add_msg_if_player( m_bad, | |
| _( "Your muscles spasm uncontrollably, and you have trouble keeping your balance." ) ); | |
| add_effect( effect_shakes, 15_minutes ); | |
| } else if( has_effect( effect_shakes ) && one_turn_in( 75_seconds ) ) { | |
| moves -= 10; | |
| add_msg_player_or_npc( m_warning, _( "Your shaking legs make you stumble." ), | |
| _( "<npcname> stumbles." ) ); | |
| if( !has_effect( effect_downed ) && one_in( 10 ) ) { | |
| add_msg_player_or_npc( m_bad, _( "You fall over!" ), _( "<npcname> falls over!" ) ); | |
| add_effect( effect_downed, rng( 3_turns, 10_turns ) ); | |
| } | |
| } | |
| } | |
| } | |
| bool player::irradiate( float rads, bool bypass ) | |
| { | |
| int rad_mut = 0; | |
| if( has_trait( trait_RADIOACTIVE3 ) ) { | |
| rad_mut = 3; | |
| } else if( has_trait( trait_RADIOACTIVE2 ) ) { | |
| rad_mut = 2; | |
| } else if( has_trait( trait_RADIOACTIVE1 ) ) { | |
| rad_mut = 1; | |
| } | |
| if( rads > 0 ) { | |
| bool has_helmet = false; | |
| const bool power_armored = is_wearing_power_armor( &has_helmet ); | |
| const bool rad_resist = power_armored || worn_with_flag( "RAD_RESIST" ); | |
| if( is_rad_immune() && !bypass ) { | |
| // Power armor and some high-tech gear protects completely from radiation | |
| rads = 0.0f; | |
| } else if( rad_resist && !bypass ) { | |
| rads /= 4.0f; | |
| } | |
| if( has_effect( effect_iodine ) ) { | |
| // Radioactive mutation makes iodine less efficient (but more useful) | |
| rads *= 0.3f + 0.1f * rad_mut; | |
| } | |
| int rads_max = roll_remainder( rads ); | |
| radiation += rng( 0, rads_max ); | |
| // Apply rads to any radiation badges. | |
| for( item *const it : inv_dump() ) { | |
| if( it->typeId() != "rad_badge" ) { | |
| continue; | |
| } | |
| // Actual irradiation levels of badges and the player aren't precisely matched. | |
| // This is intentional. | |
| const int before = it->irradiation; | |
| const int delta = rng( 0, rads_max ); | |
| if( delta == 0 ) { | |
| continue; | |
| } | |
| it->irradiation += delta; | |
| // If in inventory (not worn), don't print anything. | |
| if( inv.has_item( *it ) ) { | |
| continue; | |
| } | |
| // If the color hasn't changed, don't print anything. | |
| const std::string &col_before = rad_badge_color( before ); | |
| const std::string &col_after = rad_badge_color( it->irradiation ); | |
| if( col_before == col_after ) { | |
| continue; | |
| } | |
| add_msg_if_player( m_warning, _( "Your radiation badge changes from %1$s to %2$s!" ), | |
| col_before, col_after ); | |
| } | |
| if( rads > 0.0f ) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| // At minimum level, return at_min, at maximum at_max | |
| static float addiction_scaling( float at_min, float at_max, float add_lvl ) | |
| { | |
| // Not addicted | |
| if( add_lvl < MIN_ADDICTION_LEVEL ) { | |
| return 1.0f; | |
| } | |
| return lerp( at_min, at_max, ( add_lvl - MIN_ADDICTION_LEVEL ) / MAX_ADDICTION_LEVEL ); | |
| } | |
| void player::mend( int rate_multiplier ) | |
| { | |
| // Wearing splints can slowly mend a broken limb back to 1 hp. | |
| bool any_broken = false; | |
| for( int i = 0; i < num_hp_parts; i++ ) { | |
| if( hp_cur[i] <= 0 ) { | |
| any_broken = true; | |
| break; | |
| } | |
| } | |
| if( !any_broken ) { | |
| return; | |
| } | |
| double healing_factor = 1.0; | |
| // Studies have shown that alcohol and tobacco use delay fracture healing time | |
| // Being under effect is 50% slowdown | |
| // Being addicted but not under effect scales from 25% slowdown to 75% slowdown | |
| // The improvement from being intoxicated over withdrawal is intended | |
| if( has_effect( effect_cig ) ) { | |
| healing_factor *= 0.5; | |
| } else { | |
| healing_factor *= addiction_scaling( 0.25f, 0.75f, addiction_level( ADD_CIG ) ); | |
| } | |
| if( has_effect( effect_drunk ) ) { | |
| healing_factor *= 0.5; | |
| } else { | |
| healing_factor *= addiction_scaling( 0.25f, 0.75f, addiction_level( ADD_ALCOHOL ) ); | |
| } | |
| if( radiation > 0 && !has_trait( trait_RADIOGENIC ) ) { | |
| healing_factor *= clamp( ( 1000.0f - radiation ) / 1000.0f, 0.0f, 1.0f ); | |
| } | |
| // Bed rest speeds up mending | |
| if( has_effect( effect_sleep ) ) { | |
| healing_factor *= 4.0; | |
| } else if( get_fatigue() > DEAD_TIRED ) { | |
| // but being dead tired does not... | |
| healing_factor *= 0.75; | |
| } else { | |
| // If not dead tired, resting without sleep also helps | |
| healing_factor *= 1.0f + rest_quality(); | |
| } | |
| // Being healthy helps. | |
| healing_factor *= 1.0f + get_healthy() / 200.0f; | |
| // Very hungry starts lowering the chance | |
| // square rooting the value makes the numbers drop off faster when below 1 | |
| healing_factor *= sqrt( static_cast<float>( get_stored_kcal() ) / static_cast<float> | |
| ( get_healthy_kcal() ) ); | |
| // Similar for thirst - starts at very thirsty, drops to 0 ~halfway between two last statuses | |
| healing_factor *= 1.0f - clamp( ( get_thirst() - 80.0f ) / 300.0f, 0.0f, 1.0f ); | |
| // Mutagenic healing factor! | |
| bool needs_splint = true; | |
| if( has_trait( trait_REGEN_LIZ ) ) { | |
| healing_factor *= 20.0; | |
| needs_splint = false; | |
| } else if( has_trait( trait_REGEN ) ) { | |
| healing_factor *= 16.0; | |
| } else if( has_trait( trait_FASTHEALER2 ) ) { | |
| healing_factor *= 4.0; | |
| } else if( has_trait( trait_FASTHEALER ) ) { | |
| healing_factor *= 2.0; | |
| } else if( has_trait( trait_SLOWHEALER ) ) { | |
| healing_factor *= 0.5; | |
| } | |
| add_msg( m_debug, "Limb mend healing factor: %.2f", healing_factor ); | |
| if( healing_factor <= 0.0f ) { | |
| // The section below assumes positive healing rate | |
| return; | |
| } | |
| for( int i = 0; i < num_hp_parts; i++ ) { | |
| const bool broken = ( hp_cur[i] <= 0 ); | |
| if( !broken ) { | |
| continue; | |
| } | |
| body_part part = hp_to_bp( static_cast<hp_part>( i ) ); | |
| if( needs_splint && !worn_with_flag( "SPLINT", part ) ) { | |
| continue; | |
| } | |
| const time_duration dur_inc = 1_turns * roll_remainder( rate_multiplier * healing_factor ); | |
| auto &eff = get_effect( effect_mending, part ); | |
| if( eff.is_null() ) { | |
| add_effect( effect_mending, dur_inc, part, true ); | |
| continue; | |
| } | |
| eff.set_duration( eff.get_duration() + dur_inc ); | |
| if( eff.get_duration() >= eff.get_max_duration() ) { | |
| hp_cur[i] = 1; | |
| remove_effect( effect_mending, part ); | |
| //~ %s is bodypart | |
| add_memorial_log( pgettext( "memorial_male", "Broken %s began to mend." ), | |
| pgettext( "memorial_female", "Broken %s began to mend." ), | |
| body_part_name( part ) ); | |
| //~ %s is bodypart | |
| add_msg_if_player( m_good, _( "Your %s has started to mend!" ), | |
| body_part_name( part ) ); | |
| } | |
| } | |
| } | |
| void player::sound_hallu() | |
| { | |
| // Random 'dangerous' sound from a random direction | |
| // 1/5 chance to be a loud sound | |
| std::vector<std::string> dir{ "north", | |
| "northeast", | |
| "northwest", | |
| "south", | |
| "southeast", | |
| "southwest", | |
| "east", | |
| "west" }; | |
| std::vector<std::string> dirz{ "and above you ", "and below you " }; | |
| std::vector<std::tuple<std::string, std::string, std::string>> desc{ | |
| std::make_tuple( "whump!", "smash_fail", "t_door_c" ), | |
| std::make_tuple( "crash!", "smash_success", "t_door_c" ), | |
| std::make_tuple( "glass breaking!", "smash_success", "t_window_domestic" ) }; | |
| std::vector<std::tuple<std::string, std::string, std::string>> desc_big{ | |
| std::make_tuple( "huge explosion!", "explosion", "default" ), | |
| std::make_tuple( "bang!", "fire_gun", "glock_19" ), | |
| std::make_tuple( "blam!", "fire_gun", "mossberg_500" ), | |
| std::make_tuple( "crash!", "smash_success", "t_wall" ), | |
| std::make_tuple( "SMASH!", "smash_success", "t_wall" ) }; | |
| std::string i_dir = dir[rng( 0, dir.size() - 1 )]; | |
| if( one_in( 10 ) ) { | |
| i_dir += " " + dirz[rng( 0, dirz.size() - 1 )]; | |
| } | |
| std::string i_desc; | |
| std::pair<std::string, std::string> i_sound; | |
| if( one_in( 5 ) ) { | |
| int r_int = rng( 0, desc_big.size() - 1 ); | |
| i_desc = std::get<0>( desc_big[r_int] ); | |
| i_sound = std::make_pair( std::get<1>( desc_big[r_int] ), std::get<2>( desc_big[r_int] ) ); | |
| } else { | |
| int r_int = rng( 0, desc.size() - 1 ); | |
| i_desc = std::get<0>( desc[r_int] ); | |
| i_sound = std::make_pair( std::get<1>( desc[r_int] ), std::get<2>( desc[r_int] ) ); | |
| } | |
| add_msg( m_warning, _( "From the %1$s you hear %2$s" ), i_dir, i_desc ); | |
| sfx::play_variant_sound( i_sound.first, i_sound.second, rng( 20, 80 ) ); | |
| } | |
| void player::drench( int saturation, const body_part_set &flags, bool ignore_waterproof ) | |
| { | |
| if( saturation < 1 ) { | |
| return; | |
| } | |
| // OK, water gets in your AEP suit or whatever. It wasn't built to keep you dry. | |
| if( has_trait( trait_DEBUG_NOTEMP ) || has_active_mutation( trait_id( "SHELL2" ) ) || | |
| ( !ignore_waterproof && is_waterproof( flags ) ) ) { | |
| return; | |
| } | |
| // Make the body wet | |
| for( const body_part bp : all_body_parts ) { | |
| // Different body parts have different size, they can only store so much water | |
| int bp_wetness_max = 0; | |
| if( flags.test( bp ) ) { | |
| bp_wetness_max = drench_capacity[bp]; | |
| } | |
| if( bp_wetness_max == 0 ) { | |
| continue; | |
| } | |
| // Different sources will only make the bodypart wet to a limit | |
| int source_wet_max = saturation * bp_wetness_max * 2 / 100; | |
| int wetness_increment = ignore_waterproof ? 100 : 2; | |
| // Respect maximums | |
| const int wetness_max = std::min( source_wet_max, bp_wetness_max ); | |
| if( body_wetness[bp] < wetness_max ) { | |
| body_wetness[bp] = std::min( wetness_max, body_wetness[bp] + wetness_increment ); | |
| } | |
| } | |
| // Remove onfire effect | |
| if( saturation > 10 || x_in_y( saturation, 10 ) ) { | |
| remove_effect( effect_onfire ); | |
| } | |
| } | |
| void player::drench_mut_calc() | |
| { | |
| for( const body_part bp : all_body_parts ) { | |
| int ignored = 0; | |
| int neutral = 0; | |
| int good = 0; | |
| for( const auto &iter : my_mutations ) { | |
| const mutation_branch &mdata = iter.first.obj(); | |
| const auto wp_iter = mdata.protection.find( bp ); | |
| if( wp_iter != mdata.protection.end() ) { | |
| ignored += wp_iter->second.x; | |
| neutral += wp_iter->second.y; | |
| good += wp_iter->second.z; | |
| } | |
| } | |
| mut_drench[bp][WT_GOOD] = good; | |
| mut_drench[bp][WT_NEUTRAL] = neutral; | |
| mut_drench[bp][WT_IGNORED] = ignored; | |
| } | |
| } | |
| void player::apply_wetness_morale( int temperature ) | |
| { | |
| // First, a quick check if we have any wetness to calculate morale from | |
| // Faster than checking all worn items for friendliness | |
| if( !std::any_of( body_wetness.begin(), body_wetness.end(), | |
| []( const int w ) { | |
| return w != 0; | |
| } ) ) { | |
| return; | |
| } | |
| // Normalize temperature to [-1.0,1.0] | |
| temperature = std::max( 0, std::min( 100, temperature ) ); | |
| const double global_temperature_mod = -1.0 + ( 2.0 * temperature / 100.0 ); | |
| int total_morale = 0; | |
| const auto wet_friendliness = exclusive_flag_coverage( "WATER_FRIENDLY" ); | |
| for( const body_part bp : all_body_parts ) { | |
| // Sum of body wetness can go up to 103 | |
| const int part_drench = body_wetness[bp]; | |
| if( part_drench == 0 ) { | |
| continue; | |
| } | |
| const auto &part_arr = mut_drench[bp]; | |
| const int part_ignored = part_arr[WT_IGNORED]; | |
| const int part_neutral = part_arr[WT_NEUTRAL]; | |
| const int part_good = part_arr[WT_GOOD]; | |
| if( part_ignored >= part_drench ) { | |
| continue; | |
| } | |
| int bp_morale = 0; | |
| const bool is_friendly = wet_friendliness.test( bp ); | |
| const int effective_drench = part_drench - part_ignored; | |
| if( is_friendly ) { | |
| // Using entire bonus from mutations and then some "human" bonus | |
| bp_morale = std::min( part_good, effective_drench ) + effective_drench / 2; | |
| } else if( effective_drench < part_good ) { | |
| // Positive or 0 | |
| // Won't go higher than part_good / 2 | |
| // Wet slime/scale doesn't feel as good when covered by wet rags/fur/kevlar | |
| bp_morale = std::min( effective_drench, part_good - effective_drench ); | |
| } else if( effective_drench > part_good + part_neutral ) { | |
| // This one will be negative | |
| bp_morale = part_good + part_neutral - effective_drench; | |
| } | |
| // Clamp to [COLD,HOT] and cast to double | |
| const double part_temperature = | |
| std::min( BODYTEMP_HOT, std::max( BODYTEMP_COLD, temp_cur[bp] ) ); | |
| // 0.0 at COLD, 1.0 at HOT | |
| const double part_mod = ( part_temperature - BODYTEMP_COLD ) / | |
| ( BODYTEMP_HOT - BODYTEMP_COLD ); | |
| // Average of global and part temperature modifiers, each in range [-1.0, 1.0] | |
| double scaled_temperature = ( global_temperature_mod + part_mod ) / 2; | |
| if( bp_morale < 0 ) { | |
| // Damp, hot clothing on hot skin feels bad | |
| scaled_temperature = fabs( scaled_temperature ); | |
| } | |
| // For an unmutated human swimming in deep water, this will add up to: | |
| // +51 when hot in 100% water friendly clothing | |
| // -103 when cold/hot in 100% unfriendly clothing | |
| total_morale += static_cast<int>( bp_morale * ( 1.0 + scaled_temperature ) / 2.0 ); | |
| } | |
| if( total_morale == 0 ) { | |
| return; | |
| } | |
| int morale_effect = total_morale / 8; | |
| if( morale_effect == 0 ) { | |
| if( total_morale > 0 ) { | |
| morale_effect = 1; | |
| } else { | |
| morale_effect = -1; | |
| } | |
| } | |
| // 61_seconds because decay is applied in 1_minutes increments | |
| add_morale( MORALE_WET, morale_effect, total_morale, 61_seconds, 61_seconds, true ); | |
| } | |
| void player::update_body_wetness( const w_point &weather ) | |
| { | |
| // Average number of turns to go from completely soaked to fully dry | |
| // assuming average temperature and humidity | |
| constexpr int average_drying = HOURS( 2 ); | |
| // A modifier on drying time | |
| double delay = 1.0; | |
| // Weather slows down drying | |
| delay += ( ( weather.humidity - 66 ) - ( weather.temperature - 65 ) ) / 100; | |
| delay = std::max( 0.1, delay ); | |
| // Fur/slime retains moisture | |
| if( has_trait( trait_LIGHTFUR ) || has_trait( trait_FUR ) || has_trait( trait_FELINE_FUR ) || | |
| has_trait( trait_LUPINE_FUR ) || has_trait( trait_CHITIN_FUR ) || has_trait( trait_CHITIN_FUR2 ) || | |
| has_trait( trait_CHITIN_FUR3 ) ) { | |
| delay = delay * 6 / 5; | |
| } | |
| if( has_trait( trait_URSINE_FUR ) || has_trait( trait_SLIMY ) ) { | |
| delay *= 1.5; | |
| } | |
| if( !x_in_y( 1, average_drying / 100.0 * delay ) ) { | |
| // No drying this turn | |
| return; | |
| } | |
| // Now per-body-part stuff | |
| // To make drying uniform, make just one roll and reuse it | |
| const int drying_roll = rng( 1, 100 ); | |
| if( drying_roll > 40 ) { | |
| // Wouldn't affect anything | |
| return; | |
| } | |
| for( const body_part bp : all_body_parts ) { | |
| if( body_wetness[bp] == 0 ) { | |
| continue; | |
| } | |
| // This is to normalize drying times | |
| int drying_chance = drench_capacity[bp]; | |
| // Body temperature affects duration of wetness | |
| // Note: Using temp_conv rather than temp_cur, to better approximate environment | |
| if( temp_conv[bp] >= BODYTEMP_SCORCHING ) { | |
| drying_chance *= 2; | |
| } else if( temp_conv[bp] >= BODYTEMP_VERY_HOT ) { | |
| drying_chance = drying_chance * 3 / 2; | |
| } else if( temp_conv[bp] >= BODYTEMP_HOT ) { | |
| drying_chance = drying_chance * 4 / 3; | |
| } else if( temp_conv[bp] > BODYTEMP_COLD ) { | |
| // Comfortable, doesn't need any changes | |
| } else { | |
| // Evaporation doesn't change that much at lower temp | |
| drying_chance = drying_chance * 3 / 4; | |
| } | |
| if( drying_chance < 1 ) { | |
| drying_chance = 1; | |
| } | |
| // TODO: Make evaporation reduce body heat | |
| if( drying_chance >= drying_roll ) { | |
| body_wetness[bp] -= 1; | |
| if( body_wetness[bp] < 0 ) { | |
| body_wetness[bp] = 0; | |
| } | |
| } | |
| } | |
| // TODO: Make clothing slow down drying | |
| } | |
| int player::get_morale_level() const | |
| { | |
| return morale->get_level(); | |
| } | |
| void player::add_morale( const morale_type &type, int bonus, int max_bonus, | |
| const time_duration &duration, const time_duration &decay_start, | |
| bool capped, const itype *item_type ) | |
| { | |
| morale->add( type, bonus, max_bonus, duration, decay_start, capped, item_type ); | |
| } | |
| int player::has_morale( const morale_type &type ) const | |
| { | |
| return morale->has( type ); | |
| } | |
| void player::rem_morale( const morale_type &type, const itype *item_type ) | |
| { | |
| morale->remove( type, item_type ); | |
| } | |
| void player::clear_morale() | |
| { | |
| morale->clear(); | |
| } | |
| bool player::has_morale_to_read() const | |
| { | |
| return get_morale_level() >= -40; | |
| } | |
| void player::check_and_recover_morale() | |
| { | |
| player_morale test_morale; | |
| for( const auto &wit : worn ) { | |
| test_morale.on_item_wear( wit ); | |
| } | |
| for( const auto &mut : my_mutations ) { | |
| test_morale.on_mutation_gain( mut.first ); | |
| } | |
| for( auto &elem : *effects ) { | |
| for( auto &_effect_it : elem.second ) { | |
| const effect &e = _effect_it.second; | |
| test_morale.on_effect_int_change( e.get_id(), e.get_intensity(), e.get_bp() ); | |
| } | |
| } | |
| test_morale.on_stat_change( "hunger", get_hunger() ); | |
| test_morale.on_stat_change( "thirst", get_thirst() ); | |
| test_morale.on_stat_change( "fatigue", get_fatigue() ); | |
| test_morale.on_stat_change( "pain", get_pain() ); | |
| test_morale.on_stat_change( "pkill", get_painkiller() ); | |
| test_morale.on_stat_change( "perceived_pain", get_perceived_pain() ); | |
| apply_persistent_morale(); | |
| if( !morale->consistent_with( test_morale ) ) { | |
| *morale = player_morale( test_morale ); // Recover consistency | |
| add_msg( m_debug, "%s morale was recovered.", disp_name( true ) ); | |
| } | |
| } | |
| void player::on_worn_item_transform( const item &old_it, const item &new_it ) | |
| { | |
| morale->on_worn_item_transform( old_it, new_it ); | |
| } | |
| void player::process_active_items() | |
| { | |
| if( weapon.needs_processing() && weapon.process( this, pos(), false ) ) { | |
| weapon = item(); | |
| } | |
| std::vector<item *> inv_active = inv.active_items(); | |
| for( item *tmp_it : inv_active ) { | |
| if( tmp_it->process( this, pos(), false ) ) { | |
| inv.remove_item( tmp_it ); | |
| } | |
| } | |
| // worn items | |
| remove_worn_items_with( [this]( item & itm ) { | |
| return itm.needs_processing() && itm.process( this, pos(), false ); | |
| } ); | |
| // Active item processing done, now we're recharging. | |
| item *cloak = nullptr; | |
| item *power_armor = nullptr; | |
| std::vector<item *> active_worn_items; | |
| bool weapon_active = weapon.has_flag( "USE_UPS" ) && | |
| weapon.charges < weapon.type->maximum_charges(); | |
| // Manual iteration because we only care about *worn* active items. | |
| for( item &w : worn ) { | |
| if( w.has_flag( "USE_UPS" ) && | |
| w.charges < w.type->maximum_charges() ) { | |
| active_worn_items.push_back( &w ); | |
| } | |
| if( !w.active ) { | |
| continue; | |
| } | |
| if( cloak == nullptr && w.has_flag( "ACTIVE_CLOAKING" ) ) { | |
| cloak = &w; | |
| } | |
| // Only the main power armor item can be active, the other ones (hauling frame, helmet) aren't. | |
| if( power_armor == nullptr && w.is_power_armor() ) { | |
| power_armor = &w; | |
| } | |
| } | |
| std::vector<size_t> active_held_items; | |
| int ch_UPS = 0; | |
| for( size_t index = 0; index < inv.size(); index++ ) { | |
| item &it = inv.find_item( index ); | |
| itype_id identifier = it.type->get_id(); | |
| if( identifier == "UPS_off" && it.charges > 0 ) { | |
| ch_UPS += it.ammo_remaining(); | |
| } else if( identifier == "adv_UPS_off" && it.charges > 0 ) { | |
| ch_UPS += it.ammo_remaining() / 0.6; | |
| } | |
| if( !it.has_flag( "USE_UPS" ) && it.charges < it.type->maximum_charges() ) { | |
| active_held_items.push_back( index ); | |
| } | |
| } | |
| int ch_UPS_used = 0; | |
| if( cloak != nullptr ) { | |
| if( ch_UPS >= 20 ) { | |
| use_charges( "UPS", 20 ); | |
| ch_UPS -= 20; | |
| if( ch_UPS < 200 && one_in( 3 ) ) { | |
| add_msg_if_player( m_warning, _( "Your cloaking flickers for a moment!" ) ); | |
| } | |
| } else if( ch_UPS > 0 ) { | |
| use_charges( "UPS", ch_UPS ); | |
| return; | |
| } else { | |
| add_msg_if_player( m_bad, | |
| _( "Your cloaking flickers and becomes opaque." ) ); | |
| // Bypass the "you deactivate the ..." message | |
| cloak->active = false; | |
| return; | |
| } | |
| } | |
| // For powered armor, an armor-powering bionic should always be preferred over UPS usage. | |
| if( power_armor != nullptr ) { | |
| const int power_cost = 4; | |
| bool bio_powered = can_interface_armor() && power_level > 0; | |
| // Bionic power costs are handled elsewhere. | |
| if( !bio_powered ) { | |
| if( ch_UPS >= power_cost ) { | |
| use_charges( "UPS", power_cost ); | |
| ch_UPS -= power_cost; | |
| } else { | |
| // Deactivate armor here, bypassing the usual deactivation message. | |
| add_msg_if_player( m_warning, _( "Your power armor disengages." ) ); | |
| power_armor->active = false; | |
| } | |
| } | |
| } | |
| // Load all items that use the UPS to their minimal functional charge, | |
| // The tool is not really useful if its charges are below charges_to_use | |
| for( size_t index : active_held_items ) { | |
| if( ch_UPS_used >= ch_UPS ) { | |
| break; | |
| } | |
| item &it = inv.find_item( index ); | |
| ch_UPS_used++; | |
| it.charges++; | |
| } | |
| if( weapon_active && ch_UPS_used < ch_UPS ) { | |
| ch_UPS_used++; | |
| weapon.charges++; | |
| } | |
| for( item *worn_item : active_worn_items ) { | |
| if( ch_UPS_used >= ch_UPS ) { | |
| break; | |
| } | |
| ch_UPS_used++; | |
| worn_item->charges++; | |
| } | |
| if( ch_UPS_used > 0 ) { | |
| use_charges( "UPS", ch_UPS_used ); | |
| } | |
| } | |
| item player::reduce_charges( int position, int quantity ) | |
| { | |
| item &it = i_at( position ); | |
| if( it.is_null() ) { | |
| debugmsg( "invalid item position %d for reduce_charges", position ); | |
| return item(); | |
| } | |
| if( it.charges <= quantity ) { | |
| return i_rem( position ); | |
| } | |
| it.mod_charges( -quantity ); | |
| item tmp( it ); | |
| tmp.charges = quantity; | |
| return tmp; | |
| } | |
| item player::reduce_charges( item *it, int quantity ) | |
| { | |
| if( !has_item( *it ) ) { | |
| debugmsg( "invalid item (name %s) for reduce_charges", it->tname() ); | |
| return item(); | |
| } | |
| if( it->charges <= quantity ) { | |
| return i_rem( it ); | |
| } | |
| it->mod_charges( -quantity ); | |
| item result( *it ); | |
| result.charges = quantity; | |
| return result; | |
| } | |
| int player::invlet_to_position( const int linvlet ) const | |
| { | |
| // Invlets may come from curses, which may also return any kind of key codes, those being | |
| // of type int and they can become valid, but different characters when casted to char. | |
| // Example: KEY_NPAGE (returned when the player presses the page-down key) is 0x152, | |
| // casted to char would yield 0x52, which happens to be 'R', a valid invlet. | |
| if( linvlet > std::numeric_limits<char>::max() || linvlet < std::numeric_limits<char>::min() ) { | |
| return INT_MIN; | |
| } | |
| const char invlet = static_cast<char>( linvlet ); | |
| if( is_npc() ) { | |
| DebugLog( D_WARNING, D_GAME ) << "Why do you need to call player::invlet_to_position on npc " << | |
| name; | |
| } | |
| if( weapon.invlet == invlet ) { | |
| return -1; | |
| } | |
| auto iter = worn.begin(); | |
| for( size_t i = 0; i < worn.size(); i++, iter++ ) { | |
| if( iter->invlet == invlet ) { | |
| return worn_position_to_index( i ); | |
| } | |
| } | |
| return inv.invlet_to_position( invlet ); | |
| } | |
| bool player::can_interface_armor() const | |
| { | |
| bool okay = std::any_of( my_bionics->begin(), my_bionics->end(), | |
| []( const bionic & b ) { | |
| return b.powered && b.info().armor_interface; | |
| } ); | |
| return okay; | |
| } | |
| const martialart &player::get_combat_style() const | |
| { | |
| return style_selected.obj(); | |
| } | |
| std::vector<item *> player::inv_dump() | |
| { | |
| std::vector<item *> ret; | |
| if( is_armed() && can_unwield( weapon ).success() ) { | |
| ret.push_back( &weapon ); | |
| } | |
| for( auto &i : worn ) { | |
| ret.push_back( &i ); | |
| } | |
| inv.dump( ret ); | |
| return ret; | |
| } | |
| std::list<item> player::use_amount( itype_id it, int quantity, | |
| const std::function<bool( const item & )> &filter ) | |
| { | |
| std::list<item> ret; | |
| if( weapon.use_amount( it, quantity, ret ) ) { | |
| remove_weapon(); | |
| } | |
| for( auto a = worn.begin(); a != worn.end() && quantity > 0; ) { | |
| if( a->use_amount( it, quantity, ret, filter ) ) { | |
| a->on_takeoff( *this ); | |
| a = worn.erase( a ); | |
| } else { | |
| ++a; | |
| } | |
| } | |
| if( quantity <= 0 ) { | |
| return ret; | |
| } | |
| std::list<item> tmp = inv.use_amount( it, quantity, filter ); | |
| ret.splice( ret.end(), tmp ); | |
| return ret; | |
| } | |
| bool player::use_charges_if_avail( const itype_id &it, int quantity ) | |
| { | |
| if( has_charges( it, quantity ) ) { | |
| use_charges( it, quantity ); | |
| return true; | |
| } | |
| return false; | |
| } | |
| bool player::has_fire( const int quantity ) const | |
| { | |
| // TODO: Replace this with a "tool produces fire" flag. | |
| if( g->m.has_nearby_fire( pos() ) ) { | |
| return true; | |
| } else if( has_item_with_flag( "FIRE" ) ) { | |
| return true; | |
| } else if( has_item_with_flag( "FIRESTARTER" ) ) { | |
| auto firestarters = all_items_with_flag( "FIRESTARTER" ); | |
| for( auto &i : firestarters ) { | |
| if( has_charges( i->typeId(), quantity ) ) { | |
| return true; | |
| } | |
| } | |
| } else if( has_active_bionic( bio_tools ) && power_level > quantity * 5 ) { | |
| return true; | |
| } else if( has_bionic( bio_lighter ) && power_level > quantity * 5 ) { | |
| return true; | |
| } else if( has_bionic( bio_laser ) && power_level > quantity * 5 ) { | |
| return true; | |
| } else if( is_npc() ) { | |
| // A hack to make NPCs use their Molotovs | |
| return true; | |
| } | |
| return false; | |
| } | |
| void player::use_fire( const int quantity ) | |
| { | |
| //Okay, so checks for nearby fires first, | |
| //then held lit torch or candle, bionic tool/lighter/laser | |
| //tries to use 1 charge of lighters, matches, flame throwers | |
| //If there is enough power, will use power of one activation of the bio_lighter, bio_tools and bio_laser | |
| // (home made, military), hotplate, welder in that order. | |
| // bio_lighter, bio_laser, bio_tools, has_active_bionic("bio_tools" | |
| if( g->m.has_nearby_fire( pos() ) ) { | |
| return; | |
| } else if( has_item_with_flag( "FIRE" ) ) { | |
| return; | |
| } else if( has_item_with_flag( "FIRESTARTER" ) ) { | |
| auto firestarters = all_items_with_flag( "FIRESTARTER" ); | |
| for( auto &i : firestarters ) { | |
| if( has_charges( i->typeId(), quantity ) ) { | |
| use_charges( i->typeId(), quantity ); | |
| return; | |
| } | |
| } | |
| } else if( has_active_bionic( bio_tools ) && power_level > quantity * 5 ) { | |
| charge_power( -quantity * 5 ); | |
| return; | |
| } else if( has_bionic( bio_lighter ) && power_level > quantity * 5 ) { | |
| charge_power( -quantity * 5 ); | |
| return; | |
| } else if( has_bionic( bio_laser ) && power_level > quantity * 5 ) { | |
| charge_power( -quantity * 5 ); | |
| return; | |
| } | |
| } | |
| std::list<item> player::use_charges( const itype_id &what, int qty, | |
| const std::function<bool( const item & )> &filter ) | |
| { | |
| std::list<item> res; | |
| if( qty <= 0 ) { | |
| return res; | |
| } else if( what == "toolset" ) { | |
| charge_power( -qty ); | |
| return res; | |
| } else if( what == "fire" ) { | |
| use_fire( qty ); | |
| return res; | |
| } else if( what == "UPS" ) { | |
| if( power_level > 0 && has_active_bionic( bio_ups ) ) { | |
| int bio = std::min( power_level, qty ); | |
| charge_power( -bio ); | |
| qty -= std::min( qty, bio ); | |
| } | |
| int adv = charges_of( "adv_UPS_off", static_cast<int>( ceil( qty * 0.6 ) ) ); | |
| if( adv > 0 ) { | |
| std::list<item> found = use_charges( "adv_UPS_off", adv ); | |
| res.splice( res.end(), found ); | |
| qty -= std::min( qty, static_cast<int>( adv / 0.6 ) ); | |
| } | |
| int ups = charges_of( "UPS_off", qty ); | |
| if( ups > 0 ) { | |
| std::list<item> found = use_charges( "UPS_off", ups ); | |
| res.splice( res.end(), found ); | |
| qty -= std::min( qty, ups ); | |
| } | |
| } | |
| std::vector<item *> del; | |
| bool has_tool_with_UPS = false; | |
| visit_items( [this, &what, &qty, &res, &del, &has_tool_with_UPS, &filter]( item * e ) { | |
| if( e->use_charges( what, qty, res, pos(), filter ) ) { | |
| del.push_back( e ); | |
| } | |
| if( filter( *e ) && e->typeId() == what && e->has_flag( "USE_UPS" ) ) { | |
| has_tool_with_UPS = true; | |
| } | |
| return qty > 0 ? VisitResponse::SKIP : VisitResponse::ABORT; | |
| } ); | |
| for( auto e : del ) { | |
| remove_item( *e ); | |
| } | |
| if( has_tool_with_UPS ) { | |
| use_charges( "UPS", qty ); | |
| } | |
| return res; | |
| } | |
| bool player::covered_with_flag( const std::string &flag, const body_part_set &parts ) const | |
| { | |
| if( parts.none() ) { | |
| return true; | |
| } | |
| body_part_set to_cover( parts ); | |
| for( const auto &elem : worn ) { | |
| if( !elem.has_flag( flag ) ) { | |
| continue; | |
| } | |
| to_cover &= ~elem.get_covered_body_parts(); | |
| if( to_cover.none() ) { | |
| return true; // Allows early exit. | |
| } | |
| } | |
| return to_cover.none(); | |
| } | |
| bool player::is_waterproof( const body_part_set &parts ) const | |
| { | |
| return covered_with_flag( "WATERPROOF", parts ); | |
| } | |
| int player::amount_worn( const itype_id &id ) const | |
| { | |
| int amount = 0; | |
| for( auto &elem : worn ) { | |
| if( elem.typeId() == id ) { | |
| ++amount; | |
| } | |
| } | |
| return amount; | |
| } | |
| bool player::has_charges( const itype_id &it, int quantity, | |
| const std::function<bool( const item & )> &filter ) const | |
| { | |
| if( it == "fire" || it == "apparatus" ) { | |
| return has_fire( quantity ); | |
| } | |
| return charges_of( it, quantity, filter ) == quantity; | |
| } | |
| int player::leak_level( const std::string &flag ) const | |
| { | |
| int leak_level = 0; | |
| leak_level = inv.leak_level( flag ); | |
| return leak_level; | |
| } | |
| bool player::has_mission_item( int mission_id ) const | |
| { | |
| return mission_id != -1 && has_item_with( has_mission_item_filter{ mission_id } ); | |
| } | |
| //Returns the amount of charges that were consumed by the player | |
| int player::drink_from_hands( item &water ) | |
| { | |
| int charges_consumed = 0; | |
| if( query_yn( _( "Drink %s from your hands?" ), | |
| colorize( water.type_name(), water.color_in_inventory() ) ) ) { | |
| // Create a dose of water no greater than the amount of water remaining. | |
| item water_temp( water ); | |
| // If player is slaked water might not get consumed. | |
| consume_item( water_temp ); | |
| charges_consumed = water.charges - water_temp.charges; | |
| if( charges_consumed > 0 ) { | |
| moves -= 350; | |
| } | |
| } | |
| return charges_consumed; | |
| } | |
| // TODO: Properly split medications and food instead of hacking around | |
| bool player::consume_med( item &target ) | |
| { | |
| if( !target.is_medication() ) { | |
| return false; | |
| } | |
| const itype_id tool_type = target.get_comestible()->tool; | |
| const auto req_tool = item::find_type( tool_type ); | |
| bool tool_override = false; | |
| if( tool_type == "syringe" && has_bionic( bio_syringe ) ) { | |
| tool_override = true; | |
| } | |
| if( req_tool->tool ) { | |
| if( !( has_amount( tool_type, 1 ) && has_charges( tool_type, req_tool->tool->charges_per_use ) ) && | |
| !tool_override ) { | |
| add_msg_if_player( m_info, _( "You need a %s to consume that!" ), req_tool->nname( 1 ) ); | |
| return false; | |
| } | |
| use_charges( tool_type, req_tool->tool->charges_per_use ); | |
| } | |
| int amount_used = 1; | |
| if( target.type->has_use() ) { | |
| amount_used = target.type->invoke( *this, target, pos() ); | |
| if( amount_used <= 0 ) { | |
| return false; | |
| } | |
| } | |
| // TODO: Get the target it was used on | |
| // Otherwise injecting someone will give us addictions etc. | |
| if( target.has_flag( "NO_INGEST" ) ) { | |
| const auto &comest = *target.get_comestible(); | |
| // Assume that parenteral meds don't spoil, so don't apply rot | |
| modify_health( comest ); | |
| modify_stimulation( comest ); | |
| modify_addiction( comest ); | |
| modify_morale( target ); | |
| } else { | |
| // Take by mouth | |
| consume_effects( target ); | |
| } | |
| mod_moves( -250 ); | |
| target.charges -= amount_used; | |
| return target.charges <= 0; | |
| } | |
| bool player::consume_item( item &target ) | |
| { | |
| if( target.is_null() ) { | |
| add_msg_if_player( m_info, _( "You do not have that item." ) ); | |
| return false; | |
| } | |
| if( is_underwater() && !has_trait( trait_WATERSLEEP ) ) { | |
| add_msg_if_player( m_info, _( "You can't do that while underwater." ) ); | |
| return false; | |
| } | |
| item &comest = get_consumable_from( target ); | |
| if( comest.is_null() || target.is_craft() ) { | |
| add_msg_if_player( m_info, _( "You can't eat your %s." ), target.tname() ); | |
| if( is_npc() ) { | |
| debugmsg( "%s tried to eat a %s", name, target.tname() ); | |
| } | |
| return false; | |
| } | |
| if( consume_med( comest ) || | |
| eat( comest ) || | |
| feed_battery_with( comest ) || | |
| feed_reactor_with( comest ) || | |
| feed_furnace_with( comest ) ) { | |
| if( target.is_container() ) { | |
| target.on_contents_changed(); | |
| } | |
| return comest.charges <= 0; | |
| } | |
| return false; | |
| } | |
| bool player::consume( int target_position ) | |
| { | |
| auto &target = i_at( target_position ); | |
| if( consume_item( target ) ) { | |
| const bool was_in_container = !can_consume_as_is( target ); | |
| if( was_in_container ) { | |
| i_rem( &target.contents.front() ); | |
| } else { | |
| i_rem( &target ); | |
| } | |
| //Restack and sort so that we don't lie about target's invlet | |
| if( target_position >= 0 ) { | |
| inv.restack( *this ); | |
| } | |
| if( was_in_container && target_position == -1 ) { | |
| add_msg_if_player( _( "You are now wielding an empty %s." ), weapon.tname() ); | |
| } else if( was_in_container && target_position < -1 ) { | |
| add_msg_if_player( _( "You are now wearing an empty %s." ), target.tname() ); | |
| } else if( was_in_container && !is_npc() ) { | |
| bool drop_it = false; | |
| if( get_option<std::string>( "DROP_EMPTY" ) == "no" ) { | |
| drop_it = false; | |
| } else if( get_option<std::string>( "DROP_EMPTY" ) == "watertight" ) { | |
| drop_it = !target.is_watertight_container(); | |
| } else if( get_option<std::string>( "DROP_EMPTY" ) == "all" ) { | |
| drop_it = true; | |
| } | |
| if( drop_it ) { | |
| add_msg( _( "You drop the empty %s." ), target.tname() ); | |
| g->m.add_item_or_charges( pos(), inv.remove_item( &target ) ); | |
| } else { | |
| int quantity = inv.const_stack( inv.position_by_item( &target ) ).size(); | |
| char letter = target.invlet ? target.invlet : ' '; | |
| add_msg( m_info, _( "%c - %d empty %s" ), letter, quantity, target.tname( quantity ) ); | |
| } | |
| } | |
| } else if( target_position >= 0 ) { | |
| if( Pickup::handle_spillable_contents( *this, target, g->m ) ) { | |
| i_rem( &target ); | |
| } | |
| inv.restack( *this ); | |
| inv.unsort(); | |
| } | |
| return true; | |
| } | |
| void player::rooted_message() const | |
| { | |
| bool wearing_shoes = is_wearing_shoes( side::LEFT ) || is_wearing_shoes( side::RIGHT ); | |
| if( ( has_trait( trait_ROOTS2 ) || has_trait( trait_ROOTS3 ) ) && | |
| g->m.has_flag( "PLOWABLE", pos() ) && | |
| !wearing_shoes ) { | |
| add_msg( m_info, _( "You sink your roots into the soil." ) ); | |
| } | |
| } | |
| // TODO: Move this into player::suffer() | |
| void player::rooted() | |
| // Should average a point every two minutes or so; ground isn't uniformly fertile | |
| { | |
| double shoe_factor = footwear_factor(); | |
| if( ( has_trait( trait_ROOTS2 ) || has_trait( trait_ROOTS3 ) ) && | |
| g->m.has_flag( "PLOWABLE", pos() ) && shoe_factor != 1.0 ) { | |
| if( one_in( 96 ) ) { | |
| vitamin_mod( vitamin_id( "iron" ), 1, true ); | |
| vitamin_mod( vitamin_id( "calcium" ), 1, true ); | |
| } | |
| if( get_thirst() <= -2000 && x_in_y( 75, 425 ) ) { | |
| mod_thirst( -1 ); | |
| } | |
| mod_healthy_mod( 5, 50 ); | |
| } | |
| } | |
| bool player::add_faction_warning( const faction_id &id ) | |
| { | |
| const auto it = warning_record.find( id ); | |
| if( it != warning_record.end() ) { | |
| it->second.first += 1; | |
| if( it->second.second - calendar::turn > 5_minutes ) { | |
| it->second.first -= 1; | |
| } | |
| it->second.second = calendar::turn; | |
| if( it->second.first > 3 ) { | |
| return true; | |
| } | |
| } else { | |
| warning_record[id] = std::make_pair( 1, calendar::turn ); | |
| } | |
| faction *fac = g->faction_manager_ptr->get( id ); | |
| if( fac != nullptr && is_player() ) { | |
| fac->likes_u -= 1; | |
| fac->respects_u -= 1; | |
| } | |
| return false; | |
| } | |
| int player::current_warnings_fac( const faction_id &id ) | |
| { | |
| const auto it = warning_record.find( id ); | |
| if( it != warning_record.end() ) { | |
| if( it->second.second - calendar::turn > 5_minutes ) { | |
| it->second.first = std::max( 0, | |
| it->second.first - 1 ); | |
| } | |
| return it->second.first; | |
| } | |
| return 0; | |
| } | |
| bool player::beyond_final_warning( const faction_id &id ) | |
| { | |
| const auto it = warning_record.find( id ); | |
| if( it != warning_record.end() ) { | |
| if( it->second.second - calendar::turn > 5_minutes ) { | |
| it->second.first = std::max( 0, | |
| it->second.first - 1 ); | |
| } | |
| return it->second.first > 3; | |
| } | |
| return false; | |
| } | |
| item::reload_option player::select_ammo( const item &base, | |
| std::vector<item::reload_option> opts ) const | |
| { | |
| if( opts.empty() ) { | |
| add_msg_if_player( m_info, _( "Never mind." ) ); | |
| return item::reload_option(); | |
| } | |
| uilist menu; | |
| menu.text = string_format( base.is_watertight_container() ? _( "Refill %s" ) : | |
| base.has_flag( "RELOAD_AND_SHOOT" ) ? _( "Select ammo for %s" ) : _( "Reload %s" ), | |
| base.tname() ); | |
| menu.w_width = -1; | |
| menu.w_height = -1; | |
| // Construct item names | |
| std::vector<std::string> names; | |
| std::transform( opts.begin(), opts.end(), | |
| std::back_inserter( names ), [&]( const item::reload_option & e ) { | |
| if( e.ammo->is_magazine() && e.ammo->ammo_data() ) { | |
| if( e.ammo->ammo_current() == "battery" ) { | |
| // This battery ammo is not a real object that can be recovered but pseudo-object that represents charge | |
| //~ magazine with ammo count | |
| return string_format( _( "%s (%d)" ), e.ammo->type_name(), e.ammo->ammo_remaining() ); | |
| } else { | |
| //~ magazine with ammo (count) | |
| return string_format( _( "%s with %s (%d)" ), e.ammo->type_name(), | |
| e.ammo->ammo_data()->nname( e.ammo->ammo_remaining() ), e.ammo->ammo_remaining() ); | |
| } | |
| } else if( e.ammo->is_watertight_container() || | |
| ( e.ammo->is_ammo_container() && g->u.is_worn( *e.ammo ) ) ) { | |
| // worn ammo containers should be named by their contents with their location also updated below | |
| return e.ammo->contents.front().display_name(); | |
| } else { | |
| return ( ammo_location && ammo_location == e.ammo ? "* " : "" ) + e.ammo->display_name(); | |
| } | |
| } ); | |
| // Get location descriptions | |
| std::vector<std::string> where; | |
| std::transform( opts.begin(), opts.end(), | |
| std::back_inserter( where ), []( const item::reload_option & e ) { | |
| bool is_ammo_container = e.ammo->is_ammo_container(); | |
| if( is_ammo_container || e.ammo->is_container() ) { | |
| if( is_ammo_container && g->u.is_worn( *e.ammo ) ) { | |
| return e.ammo->type_name(); | |
| } | |
| return string_format( _( "%s, %s" ), e.ammo->type_name(), e.ammo.describe( &g->u ) ); | |
| } | |
| return e.ammo.describe( &g->u ); | |
| } ); | |
| // Pads elements to match longest member and return length | |
| auto pad = []( std::vector<std::string> &vec, int n, int t ) -> int { | |
| for( const auto &e : vec ) | |
| { | |
| n = std::max( n, utf8_width( e, true ) + t ); | |
| } | |
| for( auto &e : vec ) | |
| { | |
| e += std::string( n - utf8_width( e, true ), ' ' ); | |
| } | |
| return n; | |
| }; | |
| // Pad the first column including 4 trailing spaces | |
| int w = pad( names, utf8_width( menu.text, true ), 6 ); | |
| menu.text.insert( 0, 2, ' ' ); // add space for UI hotkeys | |
| menu.text += std::string( w + 2 - utf8_width( menu.text, true ), ' ' ); | |
| // Pad the location similarly (excludes leading "| " and trailing " ") | |
| w = pad( where, utf8_width( _( "| Location " ) ) - 3, 6 ); | |
| menu.text += _( "| Location " ); | |
| menu.text += std::string( w + 3 - utf8_width( _( "| Location " ) ), ' ' ); | |
| menu.text += _( "| Amount " ); | |
| menu.text += _( "| Moves " ); | |
| // We only show ammo statistics for guns and magazines | |
| if( base.is_gun() || base.is_magazine() ) { | |
| menu.text += _( "| Damage | Pierce " ); | |
| } | |
| auto draw_row = [&]( int idx ) { | |
| const auto &sel = opts[ idx ]; | |
| std::string row = string_format( "%s| %s |", names[ idx ], where[ idx ] ); | |
| row += string_format( ( sel.ammo->is_ammo() || | |
| sel.ammo->is_ammo_container() ) ? " %-7d |" : " |", sel.qty() ); | |
| row += string_format( " %-7d ", sel.moves() ); | |
| if( base.is_gun() || base.is_magazine() ) { | |
| const itype *ammo = sel.ammo->is_ammo_container() ? sel.ammo->contents.front().ammo_data() : | |
| sel.ammo->ammo_data(); | |
| if( ammo ) { | |
| if( ammo->ammo->prop_damage ) { | |
| row += string_format( "| *%-6.2f | %-7d", static_cast<float>( *ammo->ammo->prop_damage ), | |
| ammo->ammo->legacy_pierce ); | |
| } else { | |
| const damage_instance &dam = ammo->ammo->damage; | |
| row += string_format( "| %-7d | %-7d", static_cast<int>( dam.total_damage() ), | |
| static_cast<int>( dam.empty() ? 0.0f : ( *dam.begin() ).res_pen ) ); | |
| } | |
| } else { | |
| row += "| | "; | |
| } | |
| } | |
| return row; | |
| }; | |
| itype_id last = uistate.lastreload[ ammotype( base.ammo_default() ) ]; | |
| // We keep the last key so that pressing the key twice (for example, r-r for reload) | |
| // will always pick the first option on the list. | |
| int last_key = inp_mngr.get_previously_pressed_key(); | |
| bool last_key_bound = false; | |
| // This is the entry that has out default | |
| int default_to = 0; | |
| // If last_key is RETURN, don't use that to override hotkey | |
| if( last_key == '\n' ) { | |
| last_key_bound = true; | |
| default_to = -1; | |
| } | |
| for( auto i = 0; i < static_cast<int>( opts.size() ); ++i ) { | |
| const item &ammo = opts[ i ].ammo->is_ammo_container() ? opts[ i ].ammo->contents.front() : | |
| *opts[ i ].ammo; | |
| char hotkey = -1; | |
| if( g->u.has_item( ammo ) ) { | |
| // if ammo in player possession and either it or any container has a valid invlet use this | |
| if( ammo.invlet ) { | |
| hotkey = ammo.invlet; | |
| } else { | |
| for( const auto obj : g->u.parents( ammo ) ) { | |
| if( obj->invlet ) { | |
| hotkey = obj->invlet; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| if( last == ammo.typeId() ) { | |
| if( !last_key_bound && hotkey == -1 ) { | |
| // If this is the first occurrence of the most recently used type of ammo and the hotkey | |
| // was not already set above then set it to the keypress that opened this prompt | |
| hotkey = last_key; | |
| last_key_bound = true; | |
| } | |
| if( !last_key_bound ) { | |
| // Pressing the last key defaults to the first entry of compatible type | |
| default_to = i; | |
| last_key_bound = true; | |
| } | |
| } | |
| if( hotkey == last_key ) { | |
| last_key_bound = true; | |
| // Prevent the default from being used: key is bound to something already | |
| default_to = -1; | |
| } | |
| menu.addentry( i, true, hotkey, draw_row( i ) ); | |
| } | |
| struct reload_callback : public uilist_callback { | |
| public: | |
| std::vector<item::reload_option> &opts; | |
| const std::function<std::string( int )> draw_row; | |
| int last_key; | |
| const int default_to; | |
| const bool can_partial_reload; | |
| reload_callback( std::vector<item::reload_option> &_opts, | |
| std::function<std::string( int )> _draw_row, | |
| int _last_key, int _default_to, bool _can_partial_reload ) : | |
| opts( _opts ), draw_row( _draw_row ), | |
| last_key( _last_key ), default_to( _default_to ), | |
| can_partial_reload( _can_partial_reload ) | |
| {} | |
| bool key( const input_context &, const input_event &event, int idx, uilist *menu ) override { | |
| auto cur_key = event.get_first_input(); | |
| if( default_to != -1 && cur_key == last_key ) { | |
| // Select the first entry on the list | |
| menu->ret = default_to; | |
| return true; | |
| } | |
| if( idx < 0 || idx >= static_cast<int>( opts.size() ) ) { | |
| return false; | |
| } | |
| auto &sel = opts[ idx ]; | |
| switch( cur_key ) { | |
| case KEY_LEFT: | |
| if( can_partial_reload ) { | |
| sel.qty( sel.qty() - 1 ); | |
| menu->entries[ idx ].txt = draw_row( idx ); | |
| } | |
| return true; | |
| case KEY_RIGHT: | |
| if( can_partial_reload ) { | |
| sel.qty( sel.qty() + 1 ); | |
| menu->entries[ idx ].txt = draw_row( idx ); | |
| } | |
| return true; | |
| } | |
| return false; | |
| } | |
| } cb( opts, draw_row, last_key, default_to, !base.has_flag( "RELOAD_ONE" ) ); | |
| menu.callback = &cb; | |
| menu.query(); | |
| if( menu.ret < 0 || static_cast<size_t>( menu.ret ) >= opts.size() ) { | |
| add_msg_if_player( m_info, _( "Never mind." ) ); | |
| return item::reload_option(); | |
| } | |
| const item_location &sel = opts[ menu.ret ].ammo; | |
| uistate.lastreload[ ammotype( base.ammo_default() ) ] = sel->is_ammo_container() ? | |
| sel->contents.front().typeId() : | |
| sel->typeId(); | |
| return std::move( opts[ menu.ret ] ); | |
| } | |
| bool player::list_ammo( const item &base, std::vector<item::reload_option> &ammo_list, | |
| bool empty ) const | |
| { | |
| auto opts = base.gunmods(); | |
| opts.push_back( &base ); | |
| if( base.magazine_current() ) { | |
| opts.push_back( base.magazine_current() ); | |
| } | |
| for( const auto mod : base.gunmods() ) { | |
| if( mod->magazine_current() ) { | |
| opts.push_back( mod->magazine_current() ); | |
| } | |
| } | |
| bool ammo_match_found = false; | |
| for( const auto e : opts ) { | |
| for( item_location &ammo : find_ammo( *e, empty ) ) { | |
| // don't try to unload frozen liquids | |
| if( ammo->is_watertight_container() && ammo->contents_made_of( SOLID ) ) { | |
| continue; | |
| } | |
| auto id = ( ammo->is_ammo_container() || ammo->is_container() ) | |
| ? ammo->contents.front().typeId() | |
| : ammo->typeId(); | |
| if( e->can_reload_with( id ) ) { | |
| // Speedloaders require an empty target. | |
| if( !ammo->has_flag( "SPEEDLOADER" ) || e->ammo_remaining() < 1 ) { | |
| ammo_match_found = true; | |
| } | |
| } | |
| if( can_reload( *e, id ) || e->has_flag( "RELOAD_AND_SHOOT" ) ) { | |
| ammo_list.emplace_back( this, e, &base, std::move( ammo ) ); | |
| } | |
| } | |
| } | |
| return ammo_match_found; | |
| } | |
| item::reload_option player::select_ammo( const item &base, bool prompt, bool empty ) const | |
| { | |
| std::vector<item::reload_option> ammo_list; | |
| bool ammo_match_found = list_ammo( base, ammo_list, empty ); | |
| if( ammo_list.empty() ) { | |
| if( !base.is_magazine() && !base.magazine_integral() && !base.magazine_current() ) { | |
| add_msg_if_player( m_info, _( "You need a compatible magazine to reload the %s!" ), | |
| base.tname() ); | |
| } else if( ammo_match_found ) { | |
| add_msg_if_player( m_info, _( "Nothing to reload!" ) ); | |
| } else { | |
| std::string name; | |
| if( base.ammo_data() ) { | |
| name = base.ammo_data()->nname( 1 ); | |
| } else if( base.is_watertight_container() ) { | |
| name = base.is_container_empty() ? "liquid" : base.contents.front().tname(); | |
| } else { | |
| name = enumerate_as_string( base.ammo_types().begin(), | |
| base.ammo_types().end(), []( const ammotype & at ) { | |
| return at->name(); | |
| }, enumeration_conjunction::none ); | |
| } | |
| add_msg_if_player( m_info, _( "You don't have any %s to reload your %s!" ), | |
| name, base.tname() ); | |
| } | |
| return item::reload_option(); | |
| } | |
| // sort in order of move cost (ascending), then remaining ammo (descending) with empty magazines always last | |
| std::stable_sort( ammo_list.begin(), ammo_list.end(), []( const item::reload_option & lhs, | |
| const item::reload_option & rhs ) { | |
| return lhs.ammo->ammo_remaining() > rhs.ammo->ammo_remaining(); | |
| } ); | |
| std::stable_sort( ammo_list.begin(), ammo_list.end(), []( const item::reload_option & lhs, | |
| const item::reload_option & rhs ) { | |
| return lhs.moves() < rhs.moves(); | |
| } ); | |
| std::stable_sort( ammo_list.begin(), ammo_list.end(), []( const item::reload_option & lhs, | |
| const item::reload_option & rhs ) { | |
| return ( lhs.ammo->ammo_remaining() != 0 ) > ( rhs.ammo->ammo_remaining() != 0 ); | |
| } ); | |
| if( is_npc() ) { | |
| return std::move( ammo_list[ 0 ] ); | |
| } | |
| if( !prompt && ammo_list.size() == 1 ) { | |
| // unconditionally suppress the prompt if there's only one option | |
| return std::move( ammo_list[ 0 ] ); | |
| } | |
| return select_ammo( base, std::move( ammo_list ) ); | |
| } | |
| ret_val<bool> player::can_wear( const item &it ) const | |
| { | |
| if( !it.is_armor() ) { | |
| return ret_val<bool>::make_failure( _( "Putting on a %s would be tricky." ), it.tname() ); | |
| } | |
| if( it.is_power_armor() ) { | |
| for( auto &elem : worn ) { | |
| if( ( elem.get_covered_body_parts() & it.get_covered_body_parts() ).any() ) { | |
| return ret_val<bool>::make_failure( _( "Can't wear power armor over other gear!" ) ); | |
| } | |
| } | |
| if( !it.covers( bp_torso ) ) { | |
| bool power_armor = false; | |
| if( !worn.empty() ) { | |
| for( auto &elem : worn ) { | |
| if( elem.is_power_armor() ) { | |
| power_armor = true; | |
| break; | |
| } | |
| } | |
| } | |
| if( !power_armor ) { | |
| return ret_val<bool>::make_failure( | |
| _( "You can only wear power armor components with power armor!" ) ); | |
| } | |
| } | |
| for( auto &i : worn ) { | |
| if( i.is_power_armor() && i.typeId() == it.typeId() ) { | |
| return ret_val<bool>::make_failure( _( "Can't wear more than one %s!" ), it.tname() ); | |
| } | |
| } | |
| } else { | |
| // Only headgear can be worn with power armor, except other power armor components. | |
| // You can't wear headgear if power armor helmet is already sitting on your head. | |
| bool has_helmet = false; | |
| if( is_wearing_power_armor( &has_helmet ) && | |
| ( has_helmet || !( it.covers( bp_head ) || it.covers( bp_mouth ) || it.covers( bp_eyes ) ) ) ) { | |
| return ret_val<bool>::make_failure( _( "Can't wear %s with power armor!" ), it.tname() ); | |
| } | |
| } | |
| // Check if we don't have both hands available before wearing a briefcase, shield, etc. Also occurs if we're already wearing one. | |
| if( it.has_flag( "RESTRICT_HANDS" ) && ( !has_two_arms() || worn_with_flag( "RESTRICT_HANDS" ) || | |
| weapon.is_two_handed( *this ) ) ) { | |
| return ret_val<bool>::make_failure( ( is_player() ? _( "You don't have a hand free to wear that." ) | |
| : string_format( _( "%s doesn't have a hand free to wear that." ), name ) ) ); | |
| } | |
| for( auto &i : worn ) { | |
| if( i.has_flag( "ONLY_ONE" ) && i.typeId() == it.typeId() ) { | |
| return ret_val<bool>::make_failure( _( "Can't wear more than one %s!" ), it.tname() ); | |
| } | |
| } | |
| if( amount_worn( it.typeId() ) >= MAX_WORN_PER_TYPE ) { | |
| return ret_val<bool>::make_failure( _( "Can't wear %i or more %s at once." ), | |
| MAX_WORN_PER_TYPE + 1, it.tname( MAX_WORN_PER_TYPE + 1 ) ); | |
| } | |
| if( ( ( it.covers( bp_foot_l ) && is_wearing_shoes( side::LEFT ) ) || | |
| ( it.covers( bp_foot_r ) && is_wearing_shoes( side::RIGHT ) ) ) && | |
| ( !it.has_flag( "OVERSIZE" ) || !it.has_flag( "OUTER" ) ) && | |
| !it.has_flag( "SKINTIGHT" ) && !it.has_flag( "BELTED" ) ) { | |
| // Checks to see if the player is wearing shoes | |
| return ret_val<bool>::make_failure( ( is_player() ? _( "You're already wearing footwear!" ) | |
| : string_format( _( "%s is already wearing footwear!" ), name ) ) ); | |
| } | |
| if( it.covers( bp_head ) && | |
| !it.has_flag( "HELMET_COMPAT" ) && | |
| !it.has_flag( "SKINTIGHT" ) && | |
| !it.has_flag( "OVERSIZE" ) && | |
| is_wearing_helmet() ) { | |
| return ret_val<bool>::make_failure( wearing_something_on( bp_head ), | |
| ( is_player() ? _( "You can't wear that with other headgear!" ) | |
| : string_format( _( "%s can't wear that with other headgear!" ), name ) ) ); | |
| } | |
| if( it.covers( bp_head ) && | |
| ( it.has_flag( "SKINTIGHT" ) || it.has_flag( "HELMET_COMPAT" ) ) && | |
| ( head_cloth_encumbrance() + it.get_encumber( *this ) > 40 ) ) { | |
| return ret_val<bool>::make_failure( ( is_player() ? _( "You can't wear that much on your head!" ) | |
| : string_format( _( "%s can't wear that much on their head!" ), name ) ) ); | |
| } | |
| if( has_trait( trait_WOOLALLERGY ) && ( it.made_of( material_id( "wool" ) ) || | |
| it.item_tags.count( "wooled" ) ) ) { | |
| return ret_val<bool>::make_failure( _( "Can't wear that, it's made of wool!" ) ); | |
| } | |
| if( it.is_filthy() && has_trait( trait_SQUEAMISH ) ) { | |
| return ret_val<bool>::make_failure( _( "Can't wear that, it's filthy!" ) ); | |
| } | |
| if( !it.has_flag( "OVERSIZE" ) ) { | |
| for( const trait_id &mut : get_mutations() ) { | |
| const auto &branch = mut.obj(); | |
| if( branch.conflicts_with_item( it ) ) { | |
| return ret_val<bool>::make_failure( _( "Your %s mutation prevents you from wearing your %s." ), | |
| branch.name(), it.type_name() ); | |
| } | |
| } | |
| if( it.covers( bp_head ) && | |
| !it.made_of( material_id( "wool" ) ) && !it.made_of( material_id( "cotton" ) ) && | |
| !it.made_of( material_id( "nomex" ) ) && !it.made_of( material_id( "leather" ) ) && | |
| ( has_trait( trait_HORNS_POINTED ) || has_trait( trait_ANTENNAE ) || | |
| has_trait( trait_ANTLERS ) ) ) { | |
| return ret_val<bool>::make_failure( _( "Cannot wear a helmet over %s." ), | |
| ( has_trait( trait_HORNS_POINTED ) ? _( "horns" ) : | |
| ( has_trait( trait_ANTENNAE ) ? _( "antennae" ) : _( "antlers" ) ) ) ); | |
| } | |
| } | |
| return ret_val<bool>::make_success(); | |
| } | |
| ret_val<bool> player::can_wield( const item &it ) const | |
| { | |
| if( it.made_of_from_type( LIQUID ) ) { | |
| return ret_val<bool>::make_failure( _( "Can't wield spilt liquids." ) ); | |
| } | |
| if( it.is_two_handed( *this ) && ( !has_two_arms() || worn_with_flag( "RESTRICT_HANDS" ) ) ) { | |
| if( worn_with_flag( "RESTRICT_HANDS" ) ) { | |
| return ret_val<bool>::make_failure( | |
| _( "Something you are wearing hinders the use of both hands." ) ); | |
| } else if( it.has_flag( "ALWAYS_TWOHAND" ) ) { | |
| return ret_val<bool>::make_failure( _( "The %s can't be wielded with only one arm." ), | |
| it.tname() ); | |
| } else { | |
| return ret_val<bool>::make_failure( _( "You are too weak to wield %s with only one arm." ), | |
| it.tname() ); | |
| } | |
| } | |
| return ret_val<bool>::make_success(); | |
| } | |
| ret_val<bool> player::can_unwield( const item &it ) const | |
| { | |
| if( it.has_flag( "NO_UNWIELD" ) ) { | |
| return ret_val<bool>::make_failure( _( "You cannot unwield your %s." ), it.tname() ); | |
| } | |
| return ret_val<bool>::make_success(); | |
| } | |
| bool player::is_wielding( const item &target ) const | |
| { | |
| return &weapon == ⌖ | |
| } | |
| bool player::wield( item &target ) | |
| { | |
| if( is_wielding( target ) ) { | |
| return true; | |
| } | |
| if( !can_wield( target ).success() ) { | |
| return false; | |
| } | |
| if( !unwield() ) { | |
| return false; | |
| } | |
| cached_info.erase( "weapon_value" ); | |
| if( target.is_null() ) { | |
| return true; | |
| } | |
| // Query whether to draw an item from a holster when attempting to wield the holster | |
| if( target.get_use( "holster" ) && !target.contents.empty() ) { | |
| if( query_yn( _( "Draw %s from %s?" ), target.get_contained().tname(), target.tname() ) ) { | |
| invoke_item( &target ); | |
| return false; | |
| } | |
| } | |
| // Wielding from inventory is relatively slow and does not improve with increasing weapon skill. | |
| // Worn items (including guns with shoulder straps) are faster but still slower | |
| // than a skilled player with a holster. | |
| // There is an additional penalty when wielding items from the inventory whilst currently grabbed. | |
| bool worn = is_worn( target ); | |
| int mv = item_handling_cost( target, true, | |
| worn ? INVENTORY_HANDLING_PENALTY / 2 : INVENTORY_HANDLING_PENALTY ); | |
| if( worn ) { | |
| target.on_takeoff( *this ); | |
| } | |
| add_msg( m_debug, "wielding took %d moves", mv ); | |
| moves -= mv; | |
| if( has_item( target ) ) { | |
| weapon = i_rem( &target ); | |
| } else { | |
| weapon = target; | |
| } | |
| last_item = weapon.typeId(); | |
| recoil = MAX_RECOIL; | |
| weapon.on_wield( *this, mv ); | |
| inv.update_invlet( weapon ); | |
| inv.update_cache_with_item( weapon ); | |
| return true; | |
| } | |
| bool player::unwield() | |
| { | |
| if( weapon.is_null() ) { | |
| return true; | |
| } | |
| if( !can_unwield( weapon ).success() ) { | |
| return false; | |
| } | |
| const std::string query = string_format( _( "Stop wielding %s?" ), weapon.tname() ); | |
| if( !dispose_item( item_location( *this, &weapon ), query ) ) { | |
| return false; | |
| } | |
| inv.unsort(); | |
| return true; | |
| } | |
| // ids of martial art styles that are available with the bio_cqb bionic. | |
| static const std::vector<matype_id> bio_cqb_styles {{ | |
| matype_id{ "style_karate" }, matype_id{ "style_judo" }, matype_id{ "style_muay_thai" }, matype_id{ "style_biojutsu" } | |
| }}; | |
| bool player::pick_style() // Style selection menu | |
| { | |
| enum style_selection { | |
| KEEP_HANDS_FREE = 0, | |
| STYLE_OFFSET | |
| }; | |
| // If there are style already, cursor starts there | |
| // if no selected styles, cursor starts from no-style | |
| // Any other keys quit the menu | |
| const std::vector<matype_id> &selectable_styles = has_active_bionic( bio_cqb ) ? bio_cqb_styles : | |
| ma_styles; | |
| input_context ctxt( "MELEE_STYLE_PICKER" ); | |
| ctxt.register_action( "SHOW_DESCRIPTION" ); | |
| uilist kmenu; | |
| kmenu.text = string_format( _( "Select a style. (press %s for more info)" ), | |
| ctxt.get_desc( "SHOW_DESCRIPTION" ) ); | |
| ma_style_callback callback( static_cast<size_t>( STYLE_OFFSET ), selectable_styles ); | |
| kmenu.callback = &callback; | |
| kmenu.input_category = "MELEE_STYLE_PICKER"; | |
| kmenu.additional_actions.emplace_back( "SHOW_DESCRIPTION", "" ); | |
| kmenu.desc_enabled = true; | |
| kmenu.addentry_desc( KEEP_HANDS_FREE, true, 'h', | |
| keep_hands_free ? _( "Keep hands free (on)" ) : _( "Keep hands free (off)" ), | |
| _( "When this is enabled, player won't wield things unless explicitly told to." ) ); | |
| kmenu.selected = STYLE_OFFSET; | |
| for( size_t i = 0; i < selectable_styles.size(); i++ ) { | |
| auto &style = selectable_styles[i].obj(); | |
| //Check if this style is currently selected | |
| const bool selected = selectable_styles[i] == style_selected; | |
| std::string entry_text = _( style.name ); | |
| if( selected ) { | |
| kmenu.selected = i + STYLE_OFFSET; | |
| entry_text = colorize( entry_text, c_pink ); | |
| } | |
| kmenu.addentry_desc( i + STYLE_OFFSET, true, -1, entry_text, _( style.description ) ); | |
| } | |
| kmenu.query(); | |
| int selection = kmenu.ret; | |
| if( selection >= STYLE_OFFSET ) { | |
| style_selected = selectable_styles[selection - STYLE_OFFSET]; | |
| martialart_use_message(); | |
| } else if( selection == KEEP_HANDS_FREE ) { | |
| keep_hands_free = !keep_hands_free; | |
| } else { | |
| return false; | |
| } | |
| return true; | |
| } | |
| hint_rating player::rate_action_wear( const item &it ) const | |
| { | |
| // TODO: flag already-worn items as HINT_IFFY | |
| if( !it.is_armor() ) { | |
| return HINT_CANT; | |
| } | |
| return can_wear( it ).success() ? HINT_GOOD : HINT_IFFY; | |
| } | |
| hint_rating player::rate_action_change_side( const item &it ) const | |
| { | |
| if( !is_worn( it ) ) { | |
| return HINT_IFFY; | |
| } | |
| if( !it.is_sided() ) { | |
| return HINT_CANT; | |
| } | |
| return HINT_GOOD; | |
| } | |
| bool player::can_reload( const item &it, const itype_id &ammo ) const | |
| { | |
| if( !it.is_reloadable_with( ammo ) ) { | |
| return false; | |
| } | |
| if( it.is_ammo_belt() ) { | |
| const auto &linkage = it.type->magazine->linkage; | |
| if( linkage && !has_charges( *linkage, 1 ) ) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| bool player::dispose_item( item_location &&obj, const std::string &prompt ) | |
| { | |
| uilist menu; | |
| menu.text = prompt.empty() ? string_format( _( "Dispose of %s" ), obj->tname() ) : prompt; | |
| using dispose_option = struct { | |
| std::string prompt; | |
| bool enabled; | |
| char invlet; | |
| int moves; | |
| std::function<bool()> action; | |
| }; | |
| std::vector<dispose_option> opts; | |
| const bool bucket = obj->is_bucket_nonempty(); | |
| opts.emplace_back( dispose_option { | |
| bucket ? _( "Spill contents and store in inventory" ) : _( "Store in inventory" ), | |
| volume_carried() + obj->volume() <= volume_capacity(), '1', | |
| item_handling_cost( *obj ), | |
| [this, bucket, &obj] { | |
| if( bucket && !obj->spill_contents( *this ) ) | |
| { | |
| return false; | |
| } | |
| moves -= item_handling_cost( *obj ); | |
| inv.add_item_keep_invlet( *obj ); | |
| inv.unsort(); | |
| obj.remove_item(); | |
| return true; | |
| } | |
| } ); | |
| opts.emplace_back( dispose_option { | |
| _( "Drop item" ), true, '2', 0, [this, &obj] { | |
| g->m.add_item_or_charges( pos(), *obj ); | |
| obj.remove_item(); | |
| return true; | |
| } | |
| } ); | |
| opts.emplace_back( dispose_option { | |
| bucket ? _( "Spill contents and wear item" ) : _( "Wear item" ), | |
| can_wear( *obj ).success(), '3', item_wear_cost( *obj ), | |
| [this, bucket, &obj] { | |
| if( bucket && !obj->spill_contents( *this ) ) | |
| { | |
| return false; | |
| } | |
| item it = *obj; | |
| obj.remove_item(); | |
| return !!wear_item( it ); | |
| } | |
| } ); | |
| for( auto &e : worn ) { | |
| if( e.can_holster( *obj ) ) { | |
| auto ptr = dynamic_cast<const holster_actor *>( e.type->get_use( "holster" )->get_actor_ptr() ); | |
| opts.emplace_back( dispose_option { | |
| string_format( _( "Store in %s" ), e.tname() ), true, e.invlet, | |
| item_store_cost( *obj, e, false, ptr->draw_cost ), | |
| [this, ptr, &e, &obj]{ | |
| return ptr->store( *this, e, *obj ); | |
| } | |
| } ); | |
| } | |
| } | |
| int w = utf8_width( menu.text, true ) + 4; | |
| for( const auto &e : opts ) { | |
| w = std::max( w, utf8_width( e.prompt, true ) + 4 ); | |
| } | |
| for( auto &e : opts ) { | |
| e.prompt += std::string( w - utf8_width( e.prompt, true ), ' ' ); | |
| } | |
| menu.text.insert( 0, 2, ' ' ); // add space for UI hotkeys | |
| menu.text += std::string( w + 2 - utf8_width( menu.text, true ), ' ' ); | |
| menu.text += _( " | Moves " ); | |
| for( const auto &e : opts ) { | |
| menu.addentry( -1, e.enabled, e.invlet, string_format( e.enabled ? "%s | %-7d" : "%s |", | |
| e.prompt, e.moves ) ); | |
| } | |
| menu.query(); | |
| if( menu.ret >= 0 ) { | |
| return opts[ menu.ret ].action(); | |
| } | |
| return false; | |
| } | |
| void player::mend_item( item_location &&obj, bool interactive ) | |
| { | |
| if( g->u.has_trait( trait_DEBUG_HS ) ) { | |
| uilist menu; | |
| menu.text = _( "Toggle which fault?" ); | |
| std::vector<std::pair<fault_id, bool>> opts; | |
| for( const auto &f : obj->faults_potential() ) { | |
| opts.emplace_back( f, !!obj->faults.count( f ) ); | |
| menu.addentry( -1, true, -1, string_format( "%s %s", | |
| opts.back().second ? _( "Mend" ) : _( "Break" ), | |
| f.obj().name() ) ); | |
| } | |
| if( opts.empty() ) { | |
| add_msg( m_info, _( "The %s doesn't have any faults to toggle." ), obj->tname() ); | |
| return; | |
| } | |
| menu.query(); | |
| if( menu.ret >= 0 ) { | |
| if( opts[ menu.ret ].second ) { | |
| obj->faults.erase( opts[ menu.ret ].first ); | |
| } else { | |
| obj->faults.insert( opts[ menu.ret ].first ); | |
| } | |
| } | |
| return; | |
| } | |
| std::vector<std::pair<const fault *, bool>> faults; | |
| std::transform( obj->faults.begin(), obj->faults.end(), | |
| std::back_inserter( faults ), []( const fault_id & e ) { | |
| return std::make_pair<const fault *, bool>( &e.obj(), false ); | |
| } ); | |
| if( faults.empty() ) { | |
| if( interactive ) { | |
| add_msg( m_info, _( "The %s doesn't have any faults to mend." ), obj->tname() ); | |
| } | |
| return; | |
| } | |
| auto inv = crafting_inventory(); | |
| for( auto &f : faults ) { | |
| f.second = f.first->requirements().can_make_with_inventory( inv, is_crafting_component ); | |
| } | |
| int sel = 0; | |
| if( interactive ) { | |
| uilist menu; | |
| menu.text = _( "Mend which fault?" ); | |
| menu.desc_enabled = true; | |
| menu.desc_lines = 0; // Let uilist handle description height | |
| int w = 80; | |
| for( auto &f : faults ) { | |
| auto reqs = f.first->requirements(); | |
| auto tools = reqs.get_folded_tools_list( w, c_white, inv ); | |
| auto comps = reqs.get_folded_components_list( w, c_white, inv, is_crafting_component ); | |
| std::ostringstream descr; | |
| descr << _( "<color_white>Time required:</color>\n" ); | |
| // TODO: better have a from_moves function | |
| descr << "> " << to_string_approx( time_duration::from_turns( f.first->time() / 100 ) ) << "\n"; | |
| descr << _( "<color_white>Skills:</color>\n" ); | |
| for( const auto &e : f.first->skills() ) { | |
| bool hasSkill = get_skill_level( e.first ) >= e.second; | |
| if( !hasSkill && f.second ) { | |
| f.second = false; | |
| } | |
| //~ %1$s represents the internal color name which shouldn't be translated, %2$s is skill name, and %3$i is skill level | |
| descr << string_format( _( "> <color_%1$s>%2$s %3$i</color>\n" ), hasSkill ? "c_green" : "c_red", | |
| e.first.obj().name(), e.second ); | |
| } | |
| std::copy( tools.begin(), tools.end(), std::ostream_iterator<std::string>( descr, "\n" ) ); | |
| std::copy( comps.begin(), comps.end(), std::ostream_iterator<std::string>( descr, "\n" ) ); | |
| menu.addentry_desc( -1, true, -1, f.first->name(), descr.str() ); | |
| } | |
| menu.query(); | |
| if( menu.ret < 0 ) { | |
| add_msg( _( "Never mind." ) ); | |
| return; | |
| } | |
| sel = menu.ret; | |
| } | |
| if( sel >= 0 ) { | |
| if( !faults[ sel ].second ) { | |
| if( interactive ) { | |
| add_msg( m_info, _( "You are currently unable to mend the %s." ), obj->tname() ); | |
| } | |
| return; | |
| } | |
| assign_activity( activity_id( "ACT_MEND_ITEM" ), faults[ sel ].first->time() ); | |
| activity.name = faults[ sel ].first->id().str(); | |
| activity.targets.push_back( std::move( obj ) ); | |
| } | |
| } | |
| int player::item_reload_cost( const item &it, const item &ammo, int qty ) const | |
| { | |
| if( ammo.is_ammo() ) { | |
| qty = std::max( std::min( ammo.charges, qty ), 1 ); | |
| } else if( ammo.is_ammo_container() || ammo.is_container() ) { | |
| qty = std::max( std::min( ammo.contents.front().charges, qty ), 1 ); | |
| } else if( ammo.is_magazine() ) { | |
| qty = 1; | |
| } else { | |
| debugmsg( "cannot determine reload cost as %s is neither ammo or magazine", ammo.tname() ); | |
| return 0; | |
| } | |
| // If necessary create duplicate with appropriate number of charges | |
| item obj = ammo; | |
| obj = obj.split( qty ); | |
| if( obj.is_null() ) { | |
| obj = ammo; | |
| } | |
| // No base cost for handling ammo - that's already included in obtain cost | |
| // We have the ammo in our hands right now | |
| int mv = item_handling_cost( obj, true, 0 ); | |
| if( ammo.has_flag( "MAG_BULKY" ) ) { | |
| mv *= 1.5; // bulky magazines take longer to insert | |
| } | |
| if( !it.is_gun() && !it.is_magazine() ) { | |
| return mv + 100; // reload a tool or sealable container | |
| } | |
| /** @EFFECT_GUN decreases the time taken to reload a magazine */ | |
| /** @EFFECT_PISTOL decreases time taken to reload a pistol */ | |
| /** @EFFECT_SMG decreases time taken to reload an SMG */ | |
| /** @EFFECT_RIFLE decreases time taken to reload a rifle */ | |
| /** @EFFECT_SHOTGUN decreases time taken to reload a shotgun */ | |
| /** @EFFECT_LAUNCHER decreases time taken to reload a launcher */ | |
| int cost = ( it.is_gun() ? it.get_reload_time() : it.type->magazine->reload_time ) * qty; | |
| skill_id sk = it.is_gun() ? it.type->gun->skill_used : skill_gun; | |
| mv += cost / ( 1.0f + std::min( get_skill_level( sk ) * 0.1f, 1.0f ) ); | |
| if( it.has_flag( "STR_RELOAD" ) ) { | |
| /** @EFFECT_STR reduces reload time of some weapons */ | |
| mv -= get_str() * 20; | |
| } | |
| return std::max( mv, 25 ); | |
| } | |
| int player::item_wear_cost( const item &it ) const | |
| { | |
| double mv = item_handling_cost( it ); | |
| switch( it.get_layer() ) { | |
| case UNDERWEAR: | |
| mv *= 1.5; | |
| break; | |
| case REGULAR_LAYER: | |
| break; | |
| case WAIST_LAYER: | |
| case OUTER_LAYER: | |
| mv /= 1.5; | |
| break; | |
| case BELTED_LAYER: | |
| mv /= 2.0; | |
| break; | |
| default: | |
| break; | |
| } | |
| mv *= std::max( it.get_encumber( *this ) / 10.0, 1.0 ); | |
| return mv; | |
| } | |
| cata::optional<std::list<item>::iterator> | |
| player::wear( int pos, bool interactive ) | |
| { | |
| return wear( i_at( pos ), interactive ); | |
| } | |
| cata::optional<std::list<item>::iterator> | |
| player::wear( item &to_wear, bool interactive ) | |
| { | |
| if( is_worn( to_wear ) ) { | |
| if( interactive ) { | |
| add_msg_player_or_npc( m_info, | |
| _( "You are already wearing that." ), | |
| _( "<npcname> is already wearing that." ) | |
| ); | |
| } | |
| return cata::nullopt; | |
| } | |
| if( to_wear.is_null() ) { | |
| if( interactive ) { | |
| add_msg_player_or_npc( m_info, | |
| _( "You don't have that item." ), | |
| _( "<npcname> doesn't have that item." ) ); | |
| } | |
| return cata::nullopt; | |
| } | |
| bool was_weapon; | |
| item to_wear_copy( to_wear ); | |
| if( &to_wear == &weapon ) { | |
| weapon = item(); | |
| was_weapon = true; | |
| } else { | |
| inv.remove_item( &to_wear ); | |
| inv.restack( *this ); | |
| was_weapon = false; | |
| } | |
| auto result = wear_item( to_wear_copy, interactive ); | |
| if( !result ) { | |
| if( was_weapon ) { | |
| weapon = to_wear_copy; | |
| } else { | |
| inv.add_item( to_wear_copy, true ); | |
| } | |
| return cata::nullopt; | |
| } | |
| return result; | |
| } | |
| cata::optional<std::list<item>::iterator> | |
| player::wear_item( const item &to_wear, bool interactive ) | |
| { | |
| const auto ret = can_wear( to_wear ); | |
| if( !ret.success() ) { | |
| if( interactive ) { | |
| add_msg_if_player( m_info, "%s", ret.c_str() ); | |
| } | |
| return cata::nullopt; | |
| } | |
| const bool was_deaf = is_deaf(); | |
| const bool supertinymouse = g->u.has_trait( trait_id( "SMALL2" ) ) || | |
| g->u.has_trait( trait_id( "SMALL_OK" ) ); | |
| last_item = to_wear.typeId(); | |
| std::list<item>::iterator position = position_to_wear_new_item( to_wear ); | |
| std::list<item>::iterator new_item_it = worn.insert( position, to_wear ); | |
| if( interactive ) { | |
| add_msg_player_or_npc( | |
| _( "You put on your %s." ), | |
| _( "<npcname> puts on their %s." ), | |
| to_wear.tname() ); | |
| moves -= item_wear_cost( to_wear ); | |
| for( const body_part bp : all_body_parts ) { | |
| if( to_wear.covers( bp ) && encumb( bp ) >= 40 ) { | |
| add_msg_if_player( m_warning, | |
| bp == bp_eyes ? | |
| _( "Your %s are very encumbered! %s" ) : _( "Your %s is very encumbered! %s" ), | |
| body_part_name( bp ), encumb_text( bp ) ); | |
| } | |
| } | |
| if( !was_deaf && is_deaf() ) { | |
| add_msg_if_player( m_info, _( "You're deafened!" ) ); | |
| } | |
| if( supertinymouse && !to_wear.has_flag( "UNDERSIZE" ) ) { | |
| add_msg_if_player( m_warning, | |
| _( "This %s is too big to wear comfortably! Maybe it could be refitted..." ), | |
| to_wear.tname() ); | |
| } else if( to_wear.has_flag( "UNDERSIZE" ) ) { | |
| add_msg_if_player( m_warning, | |
| _( "This %s is too small to wear comfortably! Maybe it could be refitted..." ), | |
| to_wear.tname() ); | |
| } | |
| } else { | |
| add_msg_if_npc( _( "<npcname> puts on their %s." ), to_wear.tname() ); | |
| } | |
| new_item_it->on_wear( *this ); | |
| inv.update_invlet( *new_item_it ); | |
| inv.update_cache_with_item( *new_item_it ); | |
| recalc_sight_limits(); | |
| reset_encumbrance(); | |
| return new_item_it; | |
| } | |
| bool player::change_side( item &it, bool interactive ) | |
| { | |
| if( !it.swap_side() ) { | |
| if( interactive ) { | |
| add_msg_player_or_npc( m_info, | |
| _( "You cannot swap the side on which your %s is worn." ), | |
| _( "<npcname> cannot swap the side on which their %s is worn." ), | |
| it.tname() ); | |
| } | |
| return false; | |
| } | |
| if( interactive ) { | |
| add_msg_player_or_npc( m_info, _( "You swap the side on which your %s is worn." ), | |
| _( "<npcname> swaps the side on which their %s is worn." ), | |
| it.tname() ); | |
| } | |
| mod_moves( -250 ); | |
| reset_encumbrance(); | |
| return true; | |
| } | |
| bool player::change_side( int pos, bool interactive ) | |
| { | |
| item &it( i_at( pos ) ); | |
| if( !is_worn( it ) ) { | |
| if( interactive ) { | |
| add_msg_player_or_npc( m_info, | |
| _( "You are not wearing that item." ), | |
| _( "<npcname> isn't wearing that item." ) ); | |
| } | |
| return false; | |
| } | |
| return change_side( it, interactive ); | |
| } | |
| hint_rating player::rate_action_takeoff( const item &it ) const | |
| { | |
| if( !it.is_armor() ) { | |
| return HINT_CANT; | |
| } | |
| if( is_worn( it ) ) { | |
| return HINT_GOOD; | |
| } | |
| return HINT_IFFY; | |
| } | |
| std::list<const item *> player::get_dependent_worn_items( const item &it ) const | |
| { | |
| std::list<const item *> dependent; | |
| // Adds dependent worn items recursively | |
| const std::function<void( const item &it )> add_dependent = [ & ]( const item & it ) { | |
| for( const auto &wit : worn ) { | |
| if( &wit == &it || !wit.is_worn_only_with( it ) ) { | |
| continue; | |
| } | |
| const auto iter = std::find_if( dependent.begin(), dependent.end(), | |
| [ &wit ]( const item * dit ) { | |
| return &wit == dit; | |
| } ); | |
| if( iter == dependent.end() ) { // Not in the list yet | |
| add_dependent( wit ); | |
| dependent.push_back( &wit ); | |
| } | |
| } | |
| }; | |
| if( is_worn( it ) ) { | |
| add_dependent( it ); | |
| } | |
| return dependent; | |
| } | |
| ret_val<bool> player::can_takeoff( const item &it, const std::list<item> *res ) const | |
| { | |
| auto iter = std::find_if( worn.begin(), worn.end(), [ &it ]( const item & wit ) { | |
| return &it == &wit; | |
| } ); | |
| if( iter == worn.end() ) { | |
| return ret_val<bool>::make_failure( !is_npc() ? _( "You are not wearing that item." ) : | |
| _( "<npcname> is not wearing that item." ) ); | |
| } | |
| if( res == nullptr && !get_dependent_worn_items( it ).empty() ) { | |
| return ret_val<bool>::make_failure( !is_npc() ? | |
| _( "You can't take off power armor while wearing other power armor components." ) : | |
| _( "<npcname> can't take off power armor while wearing other power armor components." ) ); | |
| } | |
| if( it.has_flag( "NO_TAKEOFF" ) ) { | |
| return ret_val<bool>::make_failure( !is_npc() ? | |
| _( "You can't take that item off." ) : | |
| _( "<npcname> can't take that item off." ) ); | |
| } | |
| return ret_val<bool>::make_success(); | |
| } | |
| bool player::takeoff( const item &it, std::list<item> *res ) | |
| { | |
| const auto ret = can_takeoff( it, res ); | |
| if( !ret.success() ) { | |
| add_msg( m_info, "%s", ret.c_str() ); | |
| return false; | |
| } | |
| auto iter = std::find_if( worn.begin(), worn.end(), [ &it ]( const item & wit ) { | |
| return &it == &wit; | |
| } ); | |
| if( res == nullptr ) { | |
| if( volume_carried() + it.volume() > volume_capacity_reduced_by( it.get_storage() ) ) { | |
| if( is_npc() || query_yn( _( "No room in inventory for your %s. Drop it?" ), | |
| colorize( it.tname(), it.color_in_inventory() ) ) ) { | |
| drop( get_item_position( &it ), pos() ); | |
| return true; // the drop activity ends up taking off the item anyway so shouldn't try to do it again here | |
| } else { | |
| return false; | |
| } | |
| } | |
| iter->on_takeoff( *this ); | |
| inv.add_item_keep_invlet( it ); | |
| } else { | |
| iter->on_takeoff( *this ); | |
| res->push_back( it ); | |
| } | |
| add_msg_player_or_npc( _( "You take off your %s." ), | |
| _( "<npcname> takes off their %s." ), | |
| it.tname() ); | |
| mod_moves( -250 ); // TODO: Make this variable | |
| worn.erase( iter ); | |
| recalc_sight_limits(); | |
| reset_encumbrance(); | |
| return true; | |
| } | |
| bool player::takeoff( int pos ) | |
| { | |
| return takeoff( i_at( pos ) ); | |
| } | |
| void player::drop( int pos, const tripoint &where ) | |
| { | |
| const item &it = i_at( pos ); | |
| const int count = it.count(); | |
| drop( { std::make_pair( pos, count ) }, where ); | |
| } | |
| void player::drop( const std::list<std::pair<int, int>> &what, const tripoint &target, bool stash ) | |
| { | |
| const activity_id type( stash ? "ACT_STASH" : "ACT_DROP" ); | |
| if( what.empty() ) { | |
| return; | |
| } | |
| if( rl_dist( pos(), target ) > 1 || !( stash || g->m.can_put_items( target ) ) ) { | |
| add_msg_player_or_npc( m_info, _( "You can't place items here!" ), | |
| _( "<npcname> can't place items here!" ) ); | |
| return; | |
| } | |
| assign_activity( type ); | |
| activity.placement = target - pos(); | |
| for( auto item_pair : what ) { | |
| if( can_unwield( i_at( item_pair.first ) ).success() ) { | |
| activity.values.push_back( item_pair.first ); | |
| activity.values.push_back( item_pair.second ); | |
| } | |
| } | |
| // TODO: Remove the hack. Its here because npcs don't process activities | |
| if( is_npc() ) { | |
| activity.do_turn( *this ); | |
| } | |
| } | |
| bool player::add_or_drop_with_msg( item &it, const bool unloading ) | |
| { | |
| if( it.made_of( LIQUID ) ) { | |
| liquid_handler::consume_liquid( it, 1 ); | |
| return it.charges <= 0; | |
| } | |
| it.charges = this->i_add_to_container( it, unloading ); | |
| if( it.is_ammo() && it.charges == 0 ) { | |
| return true; | |
| } else if( !this->can_pickVolume( it ) ) { | |
| put_into_vehicle_or_drop( *this, item_drop_reason::too_large, { it } ); | |
| } else if( !this->can_pickWeight( it, !get_option<bool>( "DANGEROUS_PICKUPS" ) ) ) { | |
| put_into_vehicle_or_drop( *this, item_drop_reason::too_heavy, { it } ); | |
| } else { | |
| auto &ni = this->i_add( it ); | |
| add_msg( _( "You put the %s in your inventory." ), ni.tname() ); | |
| add_msg( m_info, "%c - %s", ni.invlet == 0 ? ' ' : ni.invlet, ni.tname() ); | |
| } | |
| return true; | |
| } | |
| bool player::unload( item &it ) | |
| { | |
| // Unload a container consuming moves per item successfully removed | |
| if( it.is_container() || it.is_bandolier() ) { | |
| if( it.contents.empty() ) { | |
| add_msg( m_info, _( "The %s is already empty!" ), it.tname() ); | |
| return false; | |
| } | |
| if( !it.can_unload_liquid() ) { | |
| add_msg( m_info, _( "The liquid can't be unloaded in its current state!" ) ); | |
| return false; | |
| } | |
| bool changed = false; | |
| it.contents.erase( std::remove_if( it.contents.begin(), it.contents.end(), [this, | |
| &changed]( item & e ) { | |
| int old_charges = e.charges; | |
| const bool consumed = this->add_or_drop_with_msg( e, true ); | |
| changed = changed || consumed || e.charges != old_charges; | |
| if( consumed ) { | |
| this->mod_moves( -this->item_handling_cost( e ) ); | |
| } | |
| return consumed; | |
| } ), it.contents.end() ); | |
| if( changed ) { | |
| it.on_contents_changed(); | |
| } | |
| return true; | |
| } | |
| // If item can be unloaded more than once (currently only guns) prompt user to choose | |
| std::vector<std::string> msgs( 1, it.tname() ); | |
| std::vector<item *> opts( 1, &it ); | |
| for( auto e : it.gunmods() ) { | |
| if( e->is_gun() && !e->has_flag( "NO_UNLOAD" ) && | |
| ( e->magazine_current() || e->ammo_remaining() > 0 || e->casings_count() > 0 ) ) { | |
| msgs.emplace_back( e->tname() ); | |
| opts.emplace_back( e ); | |
| } | |
| } | |
| item *target = nullptr; | |
| if( opts.size() > 1 ) { | |
| const int ret = uilist( _( "Unload what?" ), msgs ); | |
| if( ret >= 0 ) { | |
| target = opts[ret]; | |
| } | |
| } else { | |
| target = ⁢ | |
| } | |
| if( target == nullptr ) { | |
| return false; | |
| } | |
| // Next check for any reasons why the item cannot be unloaded | |
| if( target->ammo_types().empty() || target->ammo_capacity() <= 0 ) { | |
| add_msg( m_info, _( "You can't unload a %s!" ), target->tname() ); | |
| return false; | |
| } | |
| if( target->has_flag( "NO_UNLOAD" ) ) { | |
| if( target->has_flag( "RECHARGE" ) || target->has_flag( "USE_UPS" ) ) { | |
| add_msg( m_info, _( "You can't unload a rechargeable %s!" ), target->tname() ); | |
| } else { | |
| add_msg( m_info, _( "You can't unload a %s!" ), target->tname() ); | |
| } | |
| return false; | |
| } | |
| if( !target->magazine_current() && target->ammo_remaining() <= 0 && target->casings_count() <= 0 ) { | |
| if( target->is_tool() ) { | |
| add_msg( m_info, _( "Your %s isn't charged." ), target->tname() ); | |
| } else { | |
| add_msg( m_info, _( "Your %s isn't loaded." ), target->tname() ); | |
| } | |
| return false; | |
| } | |
| target->casings_handle( [&]( item & e ) { | |
| return this->i_add_or_drop( e ); | |
| } ); | |
| if( target->is_magazine() ) { | |
| player_activity unload_mag_act( activity_id( "ACT_UNLOAD_MAG" ) ); | |
| g->u.assign_activity( unload_mag_act ); | |
| g->u.activity.targets.emplace_back( item_location( *this, target ) ); | |
| // Calculate the time to remove the contained ammo (consuming half as much time as required to load the magazine) | |
| int mv = 0; | |
| for( auto &content : target->contents ) { | |
| mv += this->item_reload_cost( it, content, content.charges ) / 2; | |
| } | |
| g->u.activity.moves_left += mv; | |
| // I think this means if unload is not done on ammo-belt, it takes as long as it takes to reload a mag. | |
| if( !it.is_ammo_belt() ) { | |
| g->u.activity.moves_left += mv; | |
| } | |
| g->u.activity.auto_resume = true; | |
| return true; | |
| } else if( target->magazine_current() ) { | |
| if( !this->add_or_drop_with_msg( *target->magazine_current(), true ) ) { | |
| return false; | |
| } | |
| // Eject magazine consuming half as much time as required to insert it | |
| this->moves -= this->item_reload_cost( *target, *target->magazine_current(), -1 ) / 2; | |
| target->contents.remove_if( [&target]( const item & e ) { | |
| return target->magazine_current() == &e; | |
| } ); | |
| } else if( target->ammo_remaining() ) { | |
| int qty = target->ammo_remaining(); | |
| if( target->ammo_current() == "plut_cell" ) { | |
| qty = target->ammo_remaining() / PLUTONIUM_CHARGES; | |
| if( qty > 0 ) { | |
| add_msg( _( "You recover %i unused plutonium." ), qty ); | |
| } else { | |
| add_msg( m_info, _( "You can't remove partially depleted plutonium!" ) ); | |
| return false; | |
| } | |
| } | |
| // Construct a new ammo item and try to drop it | |
| item ammo( target->ammo_current(), calendar::turn, qty ); | |
| if( ammo.made_of_from_type( LIQUID ) ) { | |
| if( !this->add_or_drop_with_msg( ammo ) ) { | |
| qty -= ammo.charges; // only handled part (or none) of the liquid | |
| } | |
| if( qty <= 0 ) { | |
| return false; // no liquid was moved | |
| } | |
| } else if( !this->add_or_drop_with_msg( ammo, qty > 1 ) ) { | |
| return false; | |
| } | |
| // If successful remove appropriate qty of ammo consuming half as much time as required to load it | |
| this->moves -= this->item_reload_cost( *target, ammo, qty ) / 2; | |
| if( target->ammo_current() == "plut_cell" ) { | |
| qty *= PLUTONIUM_CHARGES; | |
| } | |
| target->ammo_set( target->ammo_current(), target->ammo_remaining() - qty ); | |
| } | |
| // Turn off any active tools | |
| if( target->is_tool() && target->active && target->ammo_remaining() == 0 ) { | |
| target->type->invoke( *this, *target, this->pos() ); | |
| } | |
| add_msg( _( "You unload your %s." ), target->tname() ); | |
| return true; | |
| } | |
| void player::use_wielded() | |
| { | |
| use( -1 ); | |
| } | |
| hint_rating player::rate_action_reload( const item &it ) const | |
| { | |
| hint_rating res = HINT_CANT; | |
| // Guns may contain additional reloadable mods so check these first | |
| for( const auto mod : it.gunmods() ) { | |
| switch( rate_action_reload( *mod ) ) { | |
| case HINT_GOOD: | |
| return HINT_GOOD; | |
| case HINT_CANT: | |
| continue; | |
| case HINT_IFFY: | |
| res = HINT_IFFY; | |
| } | |
| } | |
| if( !it.is_reloadable() ) { | |
| return res; | |
| } | |
| return can_reload( it ) ? HINT_GOOD : HINT_IFFY; | |
| } | |
| hint_rating player::rate_action_unload( const item &it ) const | |
| { | |
| if( ( it.is_container() || it.is_bandolier() ) && !it.contents.empty() && | |
| it.can_unload_liquid() ) { | |
| return HINT_GOOD; | |
| } | |
| if( it.has_flag( "NO_UNLOAD" ) ) { | |
| return HINT_CANT; | |
| } | |
| if( it.magazine_current() ) { | |
| return HINT_GOOD; | |
| } | |
| for( auto e : it.gunmods() ) { | |
| if( e->is_gun() && !e->has_flag( "NO_UNLOAD" ) && | |
| ( e->magazine_current() || e->ammo_remaining() > 0 || e->casings_count() > 0 ) ) { | |
| return HINT_GOOD; | |
| } | |
| } | |
| if( it.ammo_types().empty() ) { | |
| return HINT_CANT; | |
| } | |
| if( it.ammo_remaining() > 0 || it.casings_count() > 0 ) { | |
| return HINT_GOOD; | |
| } | |
| if( it.ammo_capacity() > 0 ) { | |
| return HINT_IFFY; | |
| } | |
| return HINT_CANT; | |
| } | |
| hint_rating player::rate_action_mend( const item &it ) const | |
| { | |
| // TODO: check also if item damage could be repaired via a tool | |
| if( !it.faults.empty() ) { | |
| return HINT_GOOD; | |
| } | |
| return it.faults_potential().empty() ? HINT_CANT : HINT_IFFY; | |
| } | |
| hint_rating player::rate_action_disassemble( const item &it ) | |
| { | |
| if( can_disassemble( it, crafting_inventory() ).success() ) { | |
| return HINT_GOOD; // possible | |
| } else if( recipe_dictionary::get_uncraft( it.typeId() ) ) { | |
| return HINT_IFFY; // potentially possible but we currently lack requirements | |
| } else { | |
| return HINT_CANT; // never possible | |
| } | |
| } | |
| hint_rating player::rate_action_use( const item &it ) const | |
| { | |
| if( it.is_tool() ) { | |
| return it.ammo_sufficient() ? HINT_GOOD : HINT_IFFY; | |
| } else if( it.is_gunmod() ) { | |
| /** @EFFECT_GUN >0 allows rating estimates for gun modifications */ | |
| if( get_skill_level( skill_gun ) == 0 ) { | |
| return HINT_IFFY; | |
| } else { | |
| return HINT_GOOD; | |
| } | |
| } else if( it.is_food() || it.is_medication() || it.is_book() || it.is_armor() ) { | |
| return HINT_IFFY; //the rating is subjective, could be argued as HINT_CANT or HINT_GOOD as well | |
| } else if( it.type->has_use() ) { | |
| return HINT_GOOD; | |
| } else if( !it.is_container_empty() ) { | |
| return rate_action_use( it.get_contained() ); | |
| } | |
| return HINT_CANT; | |
| } | |
| bool player::has_enough_charges( const item &it, bool show_msg ) const | |
| { | |
| if( !it.is_tool() || !it.ammo_required() ) { | |
| return true; | |
| } | |
| if( it.has_flag( "USE_UPS" ) ) { | |
| if( has_charges( "UPS", it.ammo_required() ) || it.ammo_sufficient() ) { | |
| return true; | |
| } | |
| if( show_msg ) { | |
| add_msg_if_player( m_info, | |
| ngettext( "Your %s needs %d charge from some UPS.", | |
| "Your %s needs %d charges from some UPS.", | |
| it.ammo_required() ), | |
| it.tname(), it.ammo_required() ); | |
| } | |
| return false; | |
| } else if( !it.ammo_sufficient() ) { | |
| if( show_msg ) { | |
| add_msg_if_player( m_info, | |
| ngettext( "Your %s has %d charge but needs %d.", | |
| "Your %s has %d charges but needs %d.", | |
| it.ammo_remaining() ), | |
| it.tname(), it.ammo_remaining(), it.ammo_required() ); | |
| } | |
| return false; | |
| } | |
| return true; | |
| } | |
| bool player::consume_charges( item &used, int qty ) | |
| { | |
| if( qty < 0 ) { | |
| debugmsg( "Tried to consume negative charges" ); | |
| return false; | |
| } | |
| if( qty == 0 ) { | |
| return false; | |
| } | |
| if( !used.is_tool() && !used.is_food() && !used.is_medication() ) { | |
| debugmsg( "Tried to consume charges for non-tool, non-food, non-med item" ); | |
| return false; | |
| } | |
| // Consume comestibles destroying them if no charges remain | |
| if( used.is_food() || used.is_medication() ) { | |
| used.charges -= qty; | |
| if( used.charges <= 0 ) { | |
| i_rem( &used ); | |
| return true; | |
| } | |
| return false; | |
| } | |
| // Tools which don't require ammo are instead destroyed | |
| if( used.is_tool() && !used.ammo_required() ) { | |
| i_rem( &used ); | |
| return true; | |
| } | |
| // USE_UPS never occurs on base items but is instead added by the UPS tool mod | |
| if( used.has_flag( "USE_UPS" ) ) { | |
| // With the new UPS system, we'll want to use any charges built up in the tool before pulling from the UPS | |
| // The usage of the item was already approved, so drain item if possible, otherwise use UPS | |
| if( used.charges >= qty ) { | |
| used.ammo_consume( qty, pos() ); | |
| } else { | |
| use_charges( "UPS", qty ); | |
| } | |
| } else { | |
| used.ammo_consume( std::min( qty, used.ammo_remaining() ), pos() ); | |
| } | |
| return false; | |
| } | |
| void player::use( int inventory_position ) | |
| { | |
| item &used = i_at( inventory_position ); | |
| auto loc = item_location( *this, &used ); | |
| use( loc ); | |
| } | |
| void player::use( item_location loc ) | |
| { | |
| item &used = *loc.get_item(); | |
| int inventory_position = loc.where() == item_location::type::character ? | |
| this->get_item_position( &used ) : INT_MIN; | |
| if( used.is_null() ) { | |
| add_msg( m_info, _( "You do not have that item." ) ); | |
| return; | |
| } | |
| last_item = used.typeId(); | |
| if( used.is_tool() ) { | |
| if( !used.type->has_use() ) { | |
| add_msg_if_player( _( "You can't do anything interesting with your %s." ), used.tname() ); | |
| return; | |
| } | |
| invoke_item( &used, loc.position() ); | |
| } else if( used.type->can_use( "DOGFOOD" ) || | |
| used.type->can_use( "CATFOOD" ) || | |
| used.type->can_use( "BIRDFOOD" ) || | |
| used.type->can_use( "CATTLEFODDER" ) ) { | |
| invoke_item( &used, loc.position() ); | |
| } else if( !used.is_craft() && ( used.is_food() || | |
| used.is_medication() || | |
| used.get_contained().is_food() || | |
| used.get_contained().is_medication() ) ) { | |
| consume( inventory_position ); | |
| } else if( used.is_book() ) { | |
| // TODO: Remove this nasty cast once this and related functions are migrated to avatar | |
| if( avatar *u = dynamic_cast<avatar *>( this ) ) { | |
| u->read( inventory_position ); | |
| } | |
| } else if( used.type->has_use() ) { | |
| invoke_item( &used, loc.position() ); | |
| } else { | |
| add_msg( m_info, _( "You can't do anything interesting with your %s." ), | |
| used.tname() ); | |
| } | |
| } | |
| bool player::invoke_item( item *used ) | |
| { | |
| return invoke_item( used, pos() ); | |
| } | |
| bool player::invoke_item( item *used, const tripoint &pt ) | |
| { | |
| const auto &use_methods = used->type->use_methods; | |
| if( use_methods.empty() ) { | |
| return false; | |
| } else if( use_methods.size() == 1 ) { | |
| return invoke_item( used, use_methods.begin()->first, pt ); | |
| } | |
| uilist umenu; | |
| umenu.text = string_format( _( "What to do with your %s?" ), used->tname() ); | |
| umenu.hilight_disabled = true; | |
| for( const auto &e : use_methods ) { | |
| const auto res = e.second.can_call( *this, *used, false, pt ); | |
| umenu.addentry_desc( MENU_AUTOASSIGN, res.success(), MENU_AUTOASSIGN, e.second.get_name(), | |
| res.str() ); | |
| } | |
| umenu.desc_enabled = std::any_of( umenu.entries.begin(), | |
| umenu.entries.end(), []( const uilist_entry & elem ) { | |
| return !elem.desc.empty(); | |
| } ); | |
| umenu.query(); | |
| int choice = umenu.ret; | |
| if( choice < 0 || choice >= static_cast<int>( use_methods.size() ) ) { | |
| return false; | |
| } | |
| const std::string &method = std::next( use_methods.begin(), choice )->first; | |
| return invoke_item( used, method, pt ); | |
| } | |
| bool player::invoke_item( item *used, const std::string &method ) | |
| { | |
| return invoke_item( used, method, pos() ); | |
| } | |
| bool player::invoke_item( item *used, const std::string &method, const tripoint &pt ) | |
| { | |
| if( !has_enough_charges( *used, true ) ) { | |
| return false; | |
| } | |
| item *actually_used = used->get_usable_item( method ); | |
| if( actually_used == nullptr ) { | |
| debugmsg( "Tried to invoke a method %s on item %s, which doesn't have this method", | |
| method.c_str(), used->tname() ); | |
| return false; | |
| } | |
| int charges_used = actually_used->type->invoke( *this, *actually_used, pt, method ); | |
| if( used->is_tool() || used->is_medication() || used->get_contained().is_medication() ) { | |
| return consume_charges( *actually_used, charges_used ); | |
| } else if( ( used->is_bionic() || used->is_deployable() ) && charges_used > 0 ) { | |
| i_rem( used ); | |
| return true; | |
| } | |
| return false; | |
| } | |
| void player::reassign_item( item &it, int invlet ) | |
| { | |
| bool remove_old = true; | |
| if( invlet ) { | |
| item &prev = i_at( invlet_to_position( invlet ) ); | |
| if( !prev.is_null() ) { | |
| remove_old = it.typeId() != prev.typeId(); | |
| inv.reassign_item( prev, it.invlet, remove_old ); | |
| } | |
| } | |
| if( !invlet || inv_chars.valid( invlet ) ) { | |
| const auto iter = inv.assigned_invlet.find( it.invlet ); | |
| bool found = iter != inv.assigned_invlet.end(); | |
| if( found ) { | |
| inv.assigned_invlet.erase( iter ); | |
| } | |
| if( invlet && ( !found || it.invlet != invlet ) ) { | |
| inv.assigned_invlet[invlet] = it.typeId(); | |
| } | |
| inv.reassign_item( it, invlet, remove_old ); | |
| } | |
| } | |
| bool player::gunmod_remove( item &gun, item &mod ) | |
| { | |
| auto iter = std::find_if( gun.contents.begin(), gun.contents.end(), [&mod]( const item & e ) { | |
| return &mod == &e; | |
| } ); | |
| if( iter == gun.contents.end() ) { | |
| debugmsg( "Cannot remove non-existent gunmod" ); | |
| return false; | |
| } | |
| if( mod.ammo_remaining() && !g->unload( mod ) ) { | |
| return false; | |
| } | |
| gun.gun_set_mode( gun_mode_id( "DEFAULT" ) ); | |
| moves -= mod.type->gunmod->install_time / 2; | |
| if( mod.typeId() == "brass_catcher" ) { | |
| gun.casings_handle( [&]( item & e ) { | |
| return i_add_or_drop( e ); | |
| } ); | |
| } | |
| const itype *modtype = mod.type; | |
| i_add_or_drop( mod ); | |
| gun.contents.erase( iter ); | |
| //If the removed gunmod added mod locations, check to see if any mods are in invalid locations | |
| if( !modtype->gunmod->add_mod.empty() ) { | |
| std::map<gunmod_location, int> mod_locations = gun.get_mod_locations(); | |
| for( const auto &slot : mod_locations ) { | |
| int free_slots = gun.get_free_mod_locations( slot.first ); | |
| for( auto the_mod : gun.gunmods() ) { | |
| if( the_mod->type->gunmod->location == slot.first && free_slots < 0 ) { | |
| gunmod_remove( gun, *the_mod ); | |
| free_slots++; | |
| } else if( mod_locations.find( the_mod->type->gunmod->location ) == | |
| mod_locations.end() ) { | |
| gunmod_remove( gun, *the_mod ); | |
| } | |
| } | |
| } | |
| } | |
| //~ %1$s - gunmod, %2$s - gun. | |
| add_msg_if_player( _( "You remove your %1$s from your %2$s." ), modtype->nname( 1 ), | |
| gun.tname() ); | |
| return true; | |
| } | |
| std::pair<int, int> player::gunmod_installation_odds( const item &gun, const item &mod ) const | |
| { | |
| // Mods with INSTALL_DIFFICULT have a chance to fail, potentially damaging the gun | |
| if( !mod.has_flag( "INSTALL_DIFFICULT" ) || has_trait( trait_DEBUG_HS ) ) { | |
| return std::make_pair( 100, 0 ); | |
| } | |
| int roll = 100; // chance of success (%) | |
| int risk = 0; // chance of failure (%) | |
| int chances = 1; // start with 1 in 6 (~17% chance) | |
| for( const auto &e : mod.type->min_skills ) { | |
| // gain an additional chance for every level above the minimum requirement | |
| skill_id sk = e.first == "weapon" ? gun.gun_skill() : skill_id( e.first ); | |
| chances += std::max( get_skill_level( sk ) - e.second, 0 ); | |
| } | |
| // cap success from skill alone to 1 in 5 (~83% chance) | |
| roll = std::min( static_cast<double>( chances ), 5.0 ) / 6.0 * 100; | |
| // focus is either a penalty or bonus of at most +/-10% | |
| roll += ( std::min( std::max( focus_pool, 140 ), 60 ) - 100 ) / 4; | |
| // dexterity and intelligence give +/-2% for each point above or below 12 | |
| roll += ( get_dex() - 12 ) * 2; | |
| roll += ( get_int() - 12 ) * 2; | |
| // each level of damage to the base gun reduces success by 10% | |
| roll -= std::max( gun.damage_level( 4 ), 0 ) * 10; | |
| roll = std::min( std::max( roll, 0 ), 100 ); | |
| // risk of causing damage on failure increases with less durable guns | |
| risk = ( 100 - roll ) * ( ( 10.0 - std::min( gun.type->gun->durability, 9 ) ) / 10.0 ); | |
| return std::make_pair( roll, risk ); | |
| } | |
| void player::gunmod_add( item &gun, item &mod ) | |
| { | |
| if( !gun.is_gunmod_compatible( mod ).success() ) { | |
| debugmsg( "Tried to add incompatible gunmod" ); | |
| return; | |
| } | |
| if( !has_item( gun ) && !has_item( mod ) ) { | |
| debugmsg( "Tried gunmod installation but mod/gun not in player possession" ); | |
| return; | |
| } | |
| // first check at least the minimum requirements are met | |
| if( !has_trait( trait_DEBUG_HS ) && !can_use( mod, gun ) ) { | |
| return; | |
| } | |
| // any (optional) tool charges that are used during installation | |
| auto odds = gunmod_installation_odds( gun, mod ); | |
| int roll = odds.first; | |
| int risk = odds.second; | |
| std::string tool; | |
| int qty = 0; | |
| if( mod.is_irremovable() ) { | |
| if( !query_yn( _( "Permanently install your %1$s in your %2$s?" ), | |
| colorize( mod.tname(), mod.color_in_inventory() ), | |
| colorize( gun.tname(), gun.color_in_inventory() ) ) ) { | |
| add_msg_if_player( _( "Never mind." ) ); | |
| return; // player canceled installation | |
| } | |
| } | |
| // if chance of success <100% prompt user to continue | |
| if( roll < 100 ) { | |
| uilist prompt; | |
| prompt.text = string_format( _( "Attach your %1$s to your %2$s?" ), mod.tname(), | |
| gun.tname() ); | |
| std::vector<std::function<void()>> actions; | |
| prompt.addentry( -1, true, 'w', | |
| string_format( _( "Try without tools (%i%%) risking damage (%i%%)" ), roll, risk ) ); | |
| actions.emplace_back( [&] {} ); | |
| prompt.addentry( -1, has_charges( "small_repairkit", 100 ), 'f', | |
| string_format( _( "Use 100 charges of firearm repair kit (%i%%)" ), std::min( roll * 2, 100 ) ) ); | |
| actions.emplace_back( [&] { | |
| tool = "small_repairkit"; | |
| qty = 100; | |
| roll *= 2; // firearm repair kit improves success... | |
| risk /= 2; // ...and reduces the risk of damage upon failure | |
| } ); | |
| prompt.addentry( -1, has_charges( "large_repairkit", 25 ), 'g', | |
| string_format( _( "Use 25 charges of gunsmith repair kit (%i%%)" ), std::min( roll * 3, 100 ) ) ); | |
| actions.emplace_back( [&] { | |
| tool = "large_repairkit"; | |
| qty = 25; | |
| roll *= 3; // gunsmith repair kit improves success markedly... | |
| risk = 0; // ...and entirely prevents damage upon failure | |
| } ); | |
| prompt.query(); | |
| if( prompt.ret < 0 ) { | |
| add_msg_if_player( _( "Never mind." ) ); | |
| return; // player canceled installation | |
| } | |
| actions[ prompt.ret ](); | |
| } | |
| int turns = !has_trait( trait_DEBUG_HS ) ? mod.type->gunmod->install_time : 0; | |
| assign_activity( activity_id( "ACT_GUNMOD_ADD" ), turns, -1, get_item_position( &gun ), tool ); | |
| activity.values.push_back( get_item_position( &mod ) ); | |
| activity.values.push_back( roll ); // chance of success (%) | |
| activity.values.push_back( risk ); // chance of damage (%) | |
| activity.values.push_back( qty ); // tool charges | |
| } | |
| void player::toolmod_add( item_location tool, item_location mod ) | |
| { | |
| if( !tool && !mod ) { | |
| debugmsg( "Tried toolmod installation but mod/tool not available" ); | |
| return; | |
| } | |
| // first check at least the minimum requirements are met | |
| if( !has_trait( trait_DEBUG_HS ) && !can_use( *mod, *tool ) ) { | |
| return; | |
| } | |
| if( !query_yn( _( "Permanently install your %1$s in your %2$s?" ), | |
| colorize( mod->tname(), mod->color_in_inventory() ), | |
| colorize( tool->tname(), tool->color_in_inventory() ) ) ) { | |
| add_msg_if_player( _( "Never mind." ) ); | |
| return; // player canceled installation | |
| } | |
| assign_activity( activity_id( "ACT_TOOLMOD_ADD" ), 1, -1 ); | |
| activity.targets.emplace_back( std::move( tool ) ); | |
| activity.targets.emplace_back( std::move( mod ) ); | |
| } | |
| bool player::fun_to_read( const item &book ) const | |
| { | |
| // If you don't have a problem with eating humans, To Serve Man becomes rewarding | |
| if( ( has_trait( trait_CANNIBAL ) || has_trait( trait_PSYCHOPATH ) || | |
| has_trait( trait_SAPIOVORE ) ) && | |
| book.typeId() == "cookbook_human" ) { | |
| return true; | |
| } else if( has_trait( trait_SPIRITUAL ) && book.has_flag( "INSPIRATIONAL" ) ) { | |
| return true; | |
| } else { | |
| return book_fun_for( book, *this ) > 0; | |
| } | |
| } | |
| int player::book_fun_for( const item &book, const player &p ) const | |
| { | |
| int fun_bonus = book.type->book->fun; | |
| if( !book.is_book() ) { | |
| debugmsg( "called avatar::book_fun_for with non-book" ); | |
| return 0; | |
| } | |
| if( has_trait( trait_LOVES_BOOKS ) ) { | |
| fun_bonus++; | |
| } else if( has_trait( trait_HATES_BOOKS ) ) { | |
| if( book.type->book->fun > 0 ) { | |
| fun_bonus = 0; | |
| } else { | |
| fun_bonus--; | |
| } | |
| } | |
| // If you don't have a problem with eating humans, To Serve Man becomes rewarding | |
| if( ( p.has_trait( trait_CANNIBAL ) || p.has_trait( trait_PSYCHOPATH ) || | |
| p.has_trait( trait_SAPIOVORE ) ) && | |
| book.typeId() == "cookbook_human" ) { | |
| fun_bonus = abs( fun_bonus ); | |
| } else if( p.has_trait( trait_SPIRITUAL ) && book.has_flag( "INSPIRATIONAL" ) ) { | |
| fun_bonus = abs( fun_bonus * 3 ); | |
| } | |
| return fun_bonus; | |
| } | |
| bool player::studied_all_recipes( const itype &book ) const | |
| { | |
| if( !book.book ) { | |
| return true; | |
| } | |
| for( auto &elem : book.book->recipes ) { | |
| if( !knows_recipe( elem.recipe ) ) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| const recipe_subset &player::get_learned_recipes() const | |
| { | |
| // Cache validity check | |
| if( *_skills != *valid_autolearn_skills ) { | |
| for( const auto &r : recipe_dict.all_autolearn() ) { | |
| if( meets_skill_requirements( r->autolearn_requirements ) ) { | |
| learned_recipes->include( r ); | |
| } | |
| } | |
| *valid_autolearn_skills = *_skills; // Reassign the validity stamp | |
| } | |
| return *learned_recipes; | |
| } | |
| const recipe_subset player::get_recipes_from_books( const inventory &crafting_inv ) const | |
| { | |
| recipe_subset res; | |
| for( const auto &stack : crafting_inv.const_slice() ) { | |
| const item &candidate = stack->front(); | |
| for( std::pair<const recipe *, int> recipe_entry : | |
| candidate.get_available_recipes( *this ) ) { | |
| res.include( recipe_entry.first, recipe_entry.second ); | |
| } | |
| } | |
| return res; | |
| } | |
| const std::set<itype_id> player::get_books_for_recipe( const inventory &crafting_inv, | |
| const recipe *r ) const | |
| { | |
| std::set<itype_id> book_ids; | |
| const int skill_level = get_skill_level( r->skill_used ); | |
| for( auto &book_lvl : r->booksets ) { | |
| itype_id book_id = book_lvl.first; | |
| int required_skill_level = book_lvl.second; | |
| // NPCs don't need to identify books | |
| if( !has_identified( book_id ) ) { | |
| continue; | |
| } | |
| if( skill_level >= required_skill_level && crafting_inv.amount_of( book_id ) > 0 ) { | |
| book_ids.insert( book_id ); | |
| } | |
| } | |
| return book_ids; | |
| } | |
| const recipe_subset player::get_available_recipes( const inventory &crafting_inv, | |
| const std::vector<npc *> *helpers ) const | |
| { | |
| recipe_subset res( get_learned_recipes() ); | |
| res.include( get_recipes_from_books( crafting_inv ) ); | |
| if( helpers != nullptr ) { | |
| for( npc *np : *helpers ) { | |
| // Directly form the helper's inventory | |
| res.include( get_recipes_from_books( np->inv ) ); | |
| // Being told what to do | |
| res.include_if( np->get_learned_recipes(), [ this ]( const recipe & r ) { | |
| return get_skill_level( r.skill_used ) >= static_cast<int>( r.difficulty * | |
| 0.8f ); // Skilled enough to understand | |
| } ); | |
| } | |
| } | |
| return res; | |
| } | |
| void player::try_to_sleep( const time_duration &dur ) | |
| { | |
| const optional_vpart_position vp = g->m.veh_at( pos() ); | |
| const trap &trap_at_pos = g->m.tr_at( pos() ); | |
| const ter_id ter_at_pos = g->m.ter( pos() ); | |
| const furn_id furn_at_pos = g->m.furn( pos() ); | |
| bool plantsleep = false; | |
| bool fungaloid_cosplay = false; | |
| bool websleep = false; | |
| bool webforce = false; | |
| bool websleeping = false; | |
| bool in_shell = false; | |
| bool watersleep = false; | |
| if( has_trait( trait_CHLOROMORPH ) ) { | |
| plantsleep = true; | |
| if( ( ter_at_pos == t_dirt || ter_at_pos == t_pit || | |
| ter_at_pos == t_dirtmound || ter_at_pos == t_pit_shallow || | |
| ter_at_pos == t_grass ) && !vp && | |
| furn_at_pos == f_null ) { | |
| add_msg_if_player( m_good, _( "You relax as your roots embrace the soil." ) ); | |
| } else if( vp ) { | |
| add_msg_if_player( m_bad, _( "It's impossible to sleep in this wheeled pot!" ) ); | |
| } else if( furn_at_pos != f_null ) { | |
| add_msg_if_player( m_bad, | |
| _( "The humans' furniture blocks your roots. You can't get comfortable." ) ); | |
| } else { // Floor problems | |
| add_msg_if_player( m_bad, _( "Your roots scrabble ineffectively at the unyielding surface." ) ); | |
| } | |
| } else if( has_trait( trait_M_SKIN3 ) ) { | |
| fungaloid_cosplay = true; | |
| if( g->m.has_flag_ter_or_furn( "FUNGUS", pos() ) ) { | |
| add_msg_if_player( m_good, | |
| _( "Our fibers meld with the ground beneath us. The gills on our neck begin to seed the air with spores as our awareness fades." ) ); | |
| } | |
| } | |
| if( has_trait( trait_WEB_WALKER ) ) { | |
| websleep = true; | |
| } | |
| // Not sure how one would get Arachnid w/o web-making, but Just In Case | |
| if( has_trait( trait_THRESH_SPIDER ) && ( has_trait( trait_WEB_SPINNER ) || | |
| ( has_trait( trait_WEB_WEAVER ) ) ) ) { | |
| webforce = true; | |
| } | |
| if( websleep || webforce ) { | |
| int web = g->m.get_field_intensity( pos(), fd_web ); | |
| if( !webforce ) { | |
| // At this point, it's kinda weird, but surprisingly comfy... | |
| if( web >= 3 ) { | |
| add_msg_if_player( m_good, | |
| _( "These thick webs support your weight, and are strangely comfortable..." ) ); | |
| websleeping = true; | |
| } else if( web > 0 ) { | |
| add_msg_if_player( m_info, | |
| _( "You try to sleep, but the webs get in the way. You brush them aside." ) ); | |
| g->m.remove_field( pos(), fd_web ); | |
| } | |
| } else { | |
| // Here, you're just not comfortable outside a nice thick web. | |
| if( web >= 3 ) { | |
| add_msg_if_player( m_good, _( "You relax into your web." ) ); | |
| websleeping = true; | |
| } else { | |
| add_msg_if_player( m_bad, | |
| _( "You try to sleep, but you feel exposed and your spinnerets keep twitching." ) ); | |
| add_msg_if_player( m_info, _( "Maybe a nice thick web would help you sleep." ) ); | |
| } | |
| } | |
| } | |
| if( has_active_mutation( trait_SHELL2 ) ) { | |
| // Your shell's interior is a comfortable place to sleep. | |
| in_shell = true; | |
| } | |
| if( has_trait( trait_WATERSLEEP ) ) { | |
| if( underwater ) { | |
| add_msg_if_player( m_good, | |
| _( "You lay beneath the waves' embrace, gazing up through the water's surface..." ) ); | |
| watersleep = true; | |
| } else if( g->m.has_flag_ter( "SWIMMABLE", pos() ) ) { | |
| add_msg_if_player( m_good, _( "You settle into the water and begin to drowse..." ) ); | |
| watersleep = true; | |
| } | |
| } | |
| if( !plantsleep && ( furn_at_pos.obj().comfort > static_cast<int>( comfort_level::neutral ) || | |
| ter_at_pos == t_improvised_shelter || | |
| trap_at_pos.comfort > static_cast<int>( comfort_level::neutral ) || | |
| in_shell || websleeping || watersleep || | |
| vp.part_with_feature( "SEAT", true ) || | |
| vp.part_with_feature( "BED", true ) ) ) { | |
| add_msg_if_player( m_good, _( "This is a comfortable place to sleep." ) ); | |
| } else if( ter_at_pos != t_floor && !plantsleep && !fungaloid_cosplay && !watersleep ) { | |
| add_msg_if_player( ter_at_pos.obj().movecost <= 2 ? | |
| _( "It's a little hard to get to sleep on this %s." ) : | |
| _( "It's hard to get to sleep on this %s." ), | |
| ter_at_pos.obj().name() ); | |
| } | |
| add_msg_if_player( _( "You start trying to fall asleep." ) ); | |
| if( has_active_bionic( bio_soporific ) ) { | |
| bio_soporific_powered_at_last_sleep_check = power_level > 0; | |
| if( power_level > 0 ) { | |
| // The actual bonus is applied in sleep_spot( p ). | |
| add_msg_if_player( m_good, _( "Your soporific inducer starts working its magic." ) ); | |
| } else { | |
| add_msg_if_player( m_bad, _( "Your soporific inducer doesn't have enough power to operate." ) ); | |
| } | |
| } | |
| assign_activity( activity_id( "ACT_TRY_SLEEP" ), to_moves<int>( dur ) ); | |
| } | |
| comfort_level player::base_comfort_value( const tripoint &p ) const | |
| { | |
| // Comfort of sleeping spots is "objective", while sleep_spot( p ) is "subjective" | |
| // As in the latter also checks for fatigue and other variables while this function | |
| // only looks at the base comfyness of something. It's still subjective, in a sense, | |
| // as arachnids who sleep in webs will find most places comfortable for instance. | |
| int comfort = 0; | |
| bool plantsleep = has_trait( trait_CHLOROMORPH ); | |
| bool fungaloid_cosplay = has_trait( trait_M_SKIN3 ); | |
| bool websleep = has_trait( trait_WEB_WALKER ); | |
| bool webforce = has_trait( trait_THRESH_SPIDER ) && ( has_trait( trait_WEB_SPINNER ) || | |
| ( has_trait( trait_WEB_WEAVER ) ) ); | |
| bool in_shell = has_active_mutation( trait_SHELL2 ); | |
| bool watersleep = has_trait( trait_WATERSLEEP ); | |
| const optional_vpart_position vp = g->m.veh_at( p ); | |
| const maptile tile = g->m.maptile_at( p ); | |
| const trap &trap_at_pos = tile.get_trap_t(); | |
| const ter_id ter_at_pos = tile.get_ter(); | |
| const furn_id furn_at_pos = tile.get_furn(); | |
| int web = g->m.get_field_intensity( p, fd_web ); | |
| // Some mutants have different comfort needs | |
| if( !plantsleep && !webforce ) { | |
| if( in_shell ) { | |
| comfort += 1 + static_cast<int>( comfort_level::slightly_comfortable ); | |
| // Note: shelled individuals can still use sleeping aids! | |
| } else if( vp ) { | |
| if( vp.part_with_feature( "BED", true ) ) { | |
| comfort += 1 + static_cast<int>( comfort_level::slightly_comfortable ); | |
| } else if( vp.part_with_feature( "SEAT", true ) ) { | |
| comfort += 0 + static_cast<int>( comfort_level::slightly_comfortable ); | |
| } else { | |
| // Sleeping elsewhere is uncomfortable | |
| comfort -= g->m.move_cost( p ); | |
| } | |
| } | |
| // Not in a vehicle, start checking furniture/terrain/traps at this point in decreasing order | |
| else if( furn_at_pos != f_null ) { | |
| comfort += 0 + furn_at_pos.obj().comfort; | |
| } | |
| // Web sleepers can use their webs if better furniture isn't available | |
| else if( websleep && web >= 3 ) { | |
| comfort += 1 + static_cast<int>( comfort_level::slightly_comfortable ); | |
| } else if( ter_at_pos == t_improvised_shelter ) { | |
| comfort += 0 + static_cast<int>( comfort_level::slightly_comfortable ); | |
| } else if( ter_at_pos == t_floor || ter_at_pos == t_floor_waxed || | |
| ter_at_pos == t_carpet_red || ter_at_pos == t_carpet_yellow || | |
| ter_at_pos == t_carpet_green || ter_at_pos == t_carpet_purple ) { | |
| comfort += 1 + static_cast<int>( comfort_level::neutral ); | |
| } else if( !trap_at_pos.is_null() ) { | |
| comfort += 0 + trap_at_pos.comfort; | |
| } else { | |
| // Not a comfortable sleeping spot | |
| comfort -= g->m.move_cost( p ); | |
| } | |
| auto items = g->m.i_at( p ); | |
| for( auto &items_it : items ) { | |
| if( items_it.has_flag( "SLEEP_AID" ) ) { | |
| // Note: BED + SLEEP_AID = 9 pts, or 1 pt below very_comfortable | |
| comfort += 1 + static_cast<int>( comfort_level::slightly_comfortable ); | |
| break; // prevents using more than 1 sleep aid | |
| } | |
| } | |
| if( fungaloid_cosplay && g->m.has_flag_ter_or_furn( "FUNGUS", pos() ) ) { | |
| comfort += static_cast<int>( comfort_level::very_comfortable ); | |
| } else if( watersleep && g->m.has_flag_ter( "SWIMMABLE", pos() ) ) { | |
| comfort += static_cast<int>( comfort_level::very_comfortable ); | |
| } | |
| } else if( plantsleep ) { | |
| if( vp || furn_at_pos != f_null ) { | |
| // Sleep ain't happening in a vehicle or on furniture | |
| comfort = static_cast<int>( comfort_level::uncomfortable ); | |
| } else { | |
| // It's very easy for Chloromorphs to get to sleep on soil! | |
| if( ter_at_pos == t_dirt || ter_at_pos == t_pit || ter_at_pos == t_dirtmound || | |
| ter_at_pos == t_pit_shallow ) { | |
| comfort += static_cast<int>( comfort_level::very_comfortable ); | |
| } | |
| // Not as much if you have to dig through stuff first | |
| else if( ter_at_pos == t_grass ) { | |
| comfort += static_cast<int>( comfort_level::comfortable ); | |
| } | |
| // Sleep ain't happening | |
| else { | |
| comfort = static_cast<int>( comfort_level::uncomfortable ); | |
| } | |
| } | |
| // Has webforce | |
| } else { | |
| if( web >= 3 ) { | |
| // Thick Web and you're good to go | |
| comfort += static_cast<int>( comfort_level::very_comfortable ); | |
| } else { | |
| comfort = static_cast<int>( comfort_level::uncomfortable ); | |
| } | |
| } | |
| if( comfort > static_cast<int>( comfort_level::comfortable ) ) { | |
| return comfort_level::very_comfortable; | |
| } else if( comfort > static_cast<int>( comfort_level::slightly_comfortable ) ) { | |
| return comfort_level::comfortable; | |
| } else if( comfort > static_cast<int>( comfort_level::neutral ) ) { | |
| return comfort_level::slightly_comfortable; | |
| } else if( comfort == static_cast<int>( comfort_level::neutral ) ) { | |
| return comfort_level::neutral; | |
| } else { | |
| return comfort_level::uncomfortable; | |
| } | |
| } | |
| int player::sleep_spot( const tripoint &p ) const | |
| { | |
| comfort_level base_level = base_comfort_value( p ); | |
| int sleepy = static_cast<int>( base_level ); | |
| bool watersleep = has_trait( trait_WATERSLEEP ); | |
| if( has_addiction( ADD_SLEEP ) ) { | |
| sleepy -= 4; | |
| } | |
| if( has_trait( trait_INSOMNIA ) ) { | |
| // 12.5 points is the difference between "tired" and "dead tired" | |
| sleepy -= 12; | |
| } | |
| if( has_trait( trait_EASYSLEEPER ) ) { | |
| // Low fatigue (being rested) has a much stronger effect than high fatigue | |
| // so it's OK for the value to be that much higher | |
| sleepy += 24; | |
| } | |
| if( has_active_bionic( bio_soporific ) ) { | |
| sleepy += 30; | |
| } | |
| if( has_trait( trait_EASYSLEEPER2 ) ) { | |
| // Mousefolk can sleep just about anywhere. | |
| sleepy += 40; | |
| } | |
| if( watersleep && g->m.has_flag_ter( "SWIMMABLE", pos() ) ) { | |
| sleepy += 10; //comfy water! | |
| } | |
| if( get_fatigue() < TIRED + 1 ) { | |
| sleepy -= static_cast<int>( ( TIRED + 1 - get_fatigue() ) / 4 ); | |
| } else { | |
| sleepy += static_cast<int>( ( get_fatigue() - TIRED + 1 ) / 16 ); | |
| } | |
| if( stim > 0 || !has_trait( trait_INSOMNIA ) ) { | |
| sleepy -= 2 * stim; | |
| } else { | |
| // Make it harder for insomniac to get around the trait | |
| sleepy -= stim; | |
| } | |
| return sleepy; | |
| } | |
| bool player::can_sleep() | |
| { | |
| if( has_effect( effect_meth ) ) { | |
| // Sleep ain't happening until that meth wears off completely. | |
| return false; | |
| } | |
| // Since there's a bit of randomness to falling asleep, we want to | |
| // prevent exploiting this if can_sleep() gets called over and over. | |
| // Only actually check if we can fall asleep no more frequently than | |
| // every 30 minutes. We're assuming that if we return true, we'll | |
| // immediately be falling asleep after that. | |
| // | |
| // Also if player debug menu'd time backwards this breaks, just do the | |
| // check anyway, this will reset the timer if 'dur' is negative. | |
| const time_point now = calendar::turn; | |
| const time_duration dur = now - last_sleep_check; | |
| if( dur >= 0_turns && dur < 30_minutes ) { | |
| return false; | |
| } | |
| last_sleep_check = now; | |
| int sleepy = sleep_spot( pos() ); | |
| sleepy += rng( -8, 8 ); | |
| bool result = sleepy > 0; | |
| if( has_active_bionic( bio_soporific ) ) { | |
| if( bio_soporific_powered_at_last_sleep_check && power_level == 0 ) { | |
| add_msg_if_player( m_bad, _( "Your soporific inducer runs out of power!" ) ); | |
| } else if( !bio_soporific_powered_at_last_sleep_check && power_level > 0 ) { | |
| add_msg_if_player( m_good, _( "Your soporific inducer starts back up." ) ); | |
| } | |
| bio_soporific_powered_at_last_sleep_check = power_level > 0; | |
| } | |
| return result; | |
| } | |
| void player::fall_asleep() | |
| { | |
| // Communicate to the player that he is using items on the floor | |
| std::string item_name = is_snuggling(); | |
| if( item_name == "many" ) { | |
| if( one_in( 15 ) ) { | |
| add_msg_if_player( _( "You nestle your pile of clothes for warmth." ) ); | |
| } else { | |
| add_msg_if_player( _( "You use your pile of clothes for warmth." ) ); | |
| } | |
| } else if( item_name != "nothing" ) { | |
| if( one_in( 15 ) ) { | |
| add_msg_if_player( _( "You snuggle your %s to keep warm." ), item_name ); | |
| } else { | |
| add_msg_if_player( _( "You use your %s to keep warm." ), item_name ); | |
| } | |
| } | |
| if( has_active_mutation( trait_id( "HIBERNATE" ) ) && | |
| get_kcal_percent() > 0.8f ) { | |
| add_memorial_log( pgettext( "memorial_male", "Entered hibernation." ), | |
| pgettext( "memorial_female", "Entered hibernation." ) ); | |
| // some days worth of round-the-clock Snooze. Cata seasons default to 91 days. | |
| fall_asleep( 10_days ); | |
| // If you're not fatigued enough for 10 days, you won't sleep the whole thing. | |
| // In practice, the fatigue from filling the tank from (no msg) to Time For Bed | |
| // will last about 8 days. | |
| } | |
| fall_asleep( 10_hours ); // default max sleep time. | |
| } | |
| void player::fall_asleep( const time_duration &duration ) | |
| { | |
| if( activity ) { | |
| if( activity.id() == "ACT_TRY_SLEEP" ) { | |
| activity.set_to_null(); | |
| } else { | |
| cancel_activity(); | |
| } | |
| } | |
| add_effect( effect_sleep, duration ); | |
| } | |
| std::string player::is_snuggling() const | |
| { | |
| auto begin = g->m.i_at( pos() ).begin(); | |
| auto end = g->m.i_at( pos() ).end(); | |
| if( in_vehicle ) { | |
| if( const cata::optional<vpart_reference> vp = g->m.veh_at( pos() ).part_with_feature( VPFLAG_CARGO, | |
| false ) ) { | |
| vehicle *const veh = &vp->vehicle(); | |
| const int cargo = vp->part_index(); | |
| if( !veh->get_items( cargo ).empty() ) { | |
| begin = veh->get_items( cargo ).begin(); | |
| end = veh->get_items( cargo ).end(); | |
| } | |
| } | |
| } | |
| const item *floor_armor = nullptr; | |
| int ticker = 0; | |
| // If there are no items on the floor, return nothing | |
| if( begin == end ) { | |
| return "nothing"; | |
| } | |
| for( auto candidate = begin; candidate != end; ++candidate ) { | |
| if( !candidate->is_armor() ) { | |
| continue; | |
| } else if( candidate->volume() > 250_ml && candidate->get_warmth() > 0 && | |
| ( candidate->covers( bp_torso ) || candidate->covers( bp_leg_l ) || | |
| candidate->covers( bp_leg_r ) ) ) { | |
| floor_armor = &*candidate; | |
| ticker++; | |
| } | |
| } | |
| if( ticker == 0 ) { | |
| return "nothing"; | |
| } else if( ticker == 1 ) { | |
| return floor_armor->type_name(); | |
| } else if( ticker > 1 ) { | |
| return "many"; | |
| } | |
| return "nothing"; | |
| } | |
| // Returned values range from 1.0 (unimpeded vision) to 11.0 (totally blind). | |
| // 1.0 is LIGHT_AMBIENT_LIT or brighter | |
| // 4.0 is a dark clear night, barely bright enough for reading and crafting | |
| // 6.0 is LIGHT_AMBIENT_DIM | |
| // 7.3 is LIGHT_AMBIENT_MINIMAL, a dark cloudy night, unlit indoors | |
| // 11.0 is zero light or blindness | |
| float player::fine_detail_vision_mod() const | |
| { | |
| // PER_SLIME_OK implies you can get enough eyes around the bile | |
| // that you can generally see. There still will be the haze, but | |
| // it's annoying rather than limiting. | |
| if( is_blind() || | |
| ( ( has_effect( effect_boomered ) || has_effect( effect_darkness ) ) && | |
| !has_trait( trait_PER_SLIME_OK ) ) ) { | |
| return 11.0; | |
| } | |
| // Scale linearly as light level approaches LIGHT_AMBIENT_LIT. | |
| // If we're actually a source of light, assume we can direct it where we need it. | |
| // Therefore give a hefty bonus relative to ambient light. | |
| float own_light = std::max( 1.0, LIGHT_AMBIENT_LIT - active_light() - 2 ); | |
| // Same calculation as above, but with a result 3 lower. | |
| float ambient_light = std::max( 1.0, LIGHT_AMBIENT_LIT - g->m.ambient_light_at( pos() ) + 1.0 ); | |
| return std::min( own_light, ambient_light ); | |
| } | |
| int player::get_wind_resistance( body_part bp ) const | |
| { | |
| int coverage = 0; | |
| float totalExposed = 1.0; | |
| int totalCoverage = 0; | |
| int penalty = 100; | |
| for( auto &i : worn ) { | |
| if( i.covers( bp ) ) { | |
| if( i.made_of( material_id( "leather" ) ) || i.made_of( material_id( "plastic" ) ) || | |
| i.made_of( material_id( "bone" ) ) || | |
| i.made_of( material_id( "chitin" ) ) || i.made_of( material_id( "nomex" ) ) ) { | |
| penalty = 10; // 90% effective | |
| } else if( i.made_of( material_id( "cotton" ) ) ) { | |
| penalty = 30; | |
| } else if( i.made_of( material_id( "wool" ) ) ) { | |
| penalty = 40; | |
| } else { | |
| penalty = 1; // 99% effective | |
| } | |
| coverage = std::max( 0, i.get_coverage() - penalty ); | |
| totalExposed *= ( 1.0 - coverage / 100.0 ); // Coverage is between 0 and 1? | |
| } | |
| } | |
| // Your shell provides complete wind protection if you're inside it | |
| if( has_active_mutation( trait_SHELL2 ) ) { | |
| totalCoverage = 100; | |
| return totalCoverage; | |
| } | |
| totalCoverage = 100 - totalExposed * 100; | |
| return totalCoverage; | |
| } | |
| int player::warmth( body_part bp ) const | |
| { | |
| int ret = 0; | |
| int warmth = 0; | |
| for( auto &i : worn ) { | |
| if( i.covers( bp ) ) { | |
| warmth = i.get_warmth(); | |
| // Wool items do not lose their warmth due to being wet. | |
| // Warmth is reduced by 0 - 66% based on wetness. | |
| if( !i.made_of( material_id( "wool" ) ) ) { | |
| warmth *= 1.0 - 0.66 * body_wetness[bp] / drench_capacity[bp]; | |
| } | |
| ret += warmth; | |
| } | |
| } | |
| return ret; | |
| } | |
| static int bestwarmth( const std::list< item > &its, const std::string &flag ) | |
| { | |
| int best = 0; | |
| for( auto &w : its ) { | |
| if( w.has_flag( flag ) && w.get_warmth() > best ) { | |
| best = w.get_warmth(); | |
| } | |
| } | |
| return best; | |
| } | |
| int player::bonus_item_warmth( body_part bp ) const | |
| { | |
| int ret = 0; | |
| // If the player is not wielding anything big, check if hands can be put in pockets | |
| if( ( bp == bp_hand_l || bp == bp_hand_r ) && weapon.volume() < 500_ml ) { | |
| ret += bestwarmth( worn, "POCKETS" ); | |
| } | |
| // If the player's head is not encumbered, check if hood can be put up | |
| if( bp == bp_head && encumb( bp_head ) < 10 ) { | |
| ret += bestwarmth( worn, "HOOD" ); | |
| } | |
| // If the player's mouth is not encumbered, check if collar can be put up | |
| if( bp == bp_mouth && encumb( bp_mouth ) < 10 ) { | |
| ret += bestwarmth( worn, "COLLAR" ); | |
| } | |
| return ret; | |
| } | |
| int player::get_armor_bash( body_part bp ) const | |
| { | |
| return get_armor_bash_base( bp ) + armor_bash_bonus; | |
| } | |
| int player::get_armor_cut( body_part bp ) const | |
| { | |
| return get_armor_cut_base( bp ) + armor_cut_bonus; | |
| } | |
| int player::get_armor_type( damage_type dt, body_part bp ) const | |
| { | |
| switch( dt ) { | |
| case DT_TRUE: | |
| case DT_BIOLOGICAL: | |
| return 0; | |
| case DT_BASH: | |
| return get_armor_bash( bp ); | |
| case DT_CUT: | |
| return get_armor_cut( bp ); | |
| case DT_STAB: | |
| return get_armor_cut( bp ) * 0.8f; | |
| case DT_ACID: | |
| case DT_HEAT: | |
| case DT_COLD: | |
| case DT_ELECTRIC: { | |
| int ret = 0; | |
| for( auto &i : worn ) { | |
| if( i.covers( bp ) ) { | |
| ret += i.damage_resist( dt ); | |
| } | |
| } | |
| ret += mutation_armor( bp, dt ); | |
| return ret; | |
| } | |
| case DT_NULL: | |
| case NUM_DT: | |
| // Let it error below | |
| break; | |
| } | |
| debugmsg( "Invalid damage type: %d", dt ); | |
| return 0; | |
| } | |
| int player::get_armor_bash_base( body_part bp ) const | |
| { | |
| int ret = 0; | |
| for( auto &i : worn ) { | |
| if( i.covers( bp ) ) { | |
| ret += i.bash_resist(); | |
| } | |
| } | |
| if( has_bionic( bio_carbon ) ) { | |
| ret += 2; | |
| } | |
| if( bp == bp_head && has_bionic( bio_armor_head ) ) { | |
| ret += 3; | |
| } | |
| if( ( bp == bp_arm_l || bp == bp_arm_r ) && has_bionic( bio_armor_arms ) ) { | |
| ret += 3; | |
| } | |
| if( bp == bp_torso && has_bionic( bio_armor_torso ) ) { | |
| ret += 3; | |
| } | |
| if( ( bp == bp_leg_l || bp == bp_leg_r ) && has_bionic( bio_armor_legs ) ) { | |
| ret += 3; | |
| } | |
| if( bp == bp_eyes && has_bionic( bio_armor_eyes ) ) { | |
| ret += 3; | |
| } | |
| ret += mutation_armor( bp, DT_BASH ); | |
| return ret; | |
| } | |
| int player::get_armor_cut_base( body_part bp ) const | |
| { | |
| int ret = 0; | |
| for( auto &i : worn ) { | |
| if( i.covers( bp ) ) { | |
| ret += i.cut_resist(); | |
| } | |
| } | |
| if( has_bionic( bio_carbon ) ) { | |
| ret += 4; | |
| } | |
| if( bp == bp_head && has_bionic( bio_armor_head ) ) { | |
| ret += 3; | |
| } else if( ( bp == bp_arm_l || bp == bp_arm_r ) && has_bionic( bio_armor_arms ) ) { | |
| ret += 3; | |
| } else if( bp == bp_torso && has_bionic( bio_armor_torso ) ) { | |
| ret += 3; | |
| } else if( ( bp == bp_leg_l || bp == bp_leg_r ) && has_bionic( bio_armor_legs ) ) { | |
| ret += 3; | |
| } else if( bp == bp_eyes && has_bionic( bio_armor_eyes ) ) { | |
| ret += 3; | |
| } | |
| ret += mutation_armor( bp, DT_CUT ); | |
| return ret; | |
| } | |
| int player::get_armor_acid( body_part bp ) const | |
| { | |
| return get_armor_type( DT_ACID, bp ); | |
| } | |
| int player::get_armor_fire( body_part bp ) const | |
| { | |
| return get_armor_type( DT_HEAT, bp ); | |
| } | |
| static void destroyed_armor_msg( Character &who, const std::string &pre_damage_name ) | |
| { | |
| //~ %s is armor name | |
| who.add_memorial_log( pgettext( "memorial_male", "Worn %s was completely destroyed." ), | |
| pgettext( "memorial_female", "Worn %s was completely destroyed." ), | |
| pre_damage_name ); | |
| who.add_msg_player_or_npc( m_bad, _( "Your %s is completely destroyed!" ), | |
| _( "<npcname>'s %s is completely destroyed!" ), | |
| pre_damage_name ); | |
| } | |
| bool player::armor_absorb( damage_unit &du, item &armor ) | |
| { | |
| if( rng( 1, 100 ) > armor.get_coverage() ) { | |
| return false; | |
| } | |
| // TODO: add some check for power armor | |
| armor.mitigate_damage( du ); | |
| // We want armor's own resistance to this type, not the resistance it grants | |
| const int armors_own_resist = armor.damage_resist( du.type, true ); | |
| if( armors_own_resist > 1000 ) { | |
| // This is some weird type that doesn't damage armors | |
| return false; | |
| } | |
| // Scale chance of article taking damage based on the number of parts it covers. | |
| // This represents large articles being able to take more punishment | |
| // before becoming ineffective or being destroyed. | |
| const int num_parts_covered = armor.get_covered_body_parts().count(); | |
| if( !one_in( num_parts_covered ) ) { | |
| return false; | |
| } | |
| // Don't damage armor as much when bypassed by armor piercing | |
| // Most armor piercing damage comes from bypassing armor, not forcing through | |
| const int raw_dmg = du.amount; | |
| if( raw_dmg > armors_own_resist ) { | |
| // If damage is above armor value, the chance to avoid armor damage is | |
| // 50% + 50% * 1/dmg | |
| if( one_in( raw_dmg ) || one_in( 2 ) ) { | |
| return false; | |
| } | |
| } else { | |
| // Sturdy items and power armors never take chip damage. | |
| // Other armors have 0.5% of getting damaged from hits below their armor value. | |
| if( armor.has_flag( "STURDY" ) || armor.is_power_armor() || !one_in( 200 ) ) { | |
| return false; | |
| } | |
| } | |
| auto &material = armor.get_random_material(); | |
| std::string damage_verb = ( du.type == DT_BASH ) ? material.bash_dmg_verb() : | |
| material.cut_dmg_verb(); | |
| const std::string pre_damage_name = armor.tname(); | |
| const std::string pre_damage_adj = armor.get_base_material().dmg_adj( armor.damage_level( 4 ) ); | |
| // add "further" if the damage adjective and verb are the same | |
| std::string format_string = ( pre_damage_adj == damage_verb ) ? | |
| _( "Your %1$s is %2$s further!" ) : _( "Your %1$s is %2$s!" ); | |
| add_msg_if_player( m_bad, format_string, pre_damage_name, damage_verb ); | |
| //item is damaged | |
| if( is_player() ) { | |
| SCT.add( posx(), posy(), NORTH, remove_color_tags( pre_damage_name ), m_neutral, damage_verb, | |
| m_info ); | |
| } | |
| return armor.mod_damage( armor.has_flag( "FRAGILE" ) ? | |
| rng( 2 * itype::damage_scale, 3 * itype::damage_scale ) : itype::damage_scale, du.type ); | |
| } | |
| float player::bionic_armor_bonus( body_part bp, damage_type dt ) const | |
| { | |
| float result = 0.0f; | |
| // We only check the passive bionics | |
| if( has_bionic( bio_carbon ) ) { | |
| if( dt == DT_BASH ) { | |
| result += 2; | |
| } else if( dt == DT_CUT || dt == DT_STAB ) { | |
| result += 4; | |
| } | |
| } | |
| // All the other bionic armors reduce bash/cut/stab by 3 | |
| // Map body parts to a set of bionics that protect it | |
| // TODO: JSONize passive bionic armor instead of hardcoding it | |
| static const std::map< body_part, bionic_id > armor_bionics = { | |
| { bp_head, { bio_armor_head } }, | |
| { bp_arm_l, { bio_armor_arms } }, | |
| { bp_arm_r, { bio_armor_arms } }, | |
| { bp_torso, { bio_armor_torso } }, | |
| { bp_leg_l, { bio_armor_legs } }, | |
| { bp_leg_r, { bio_armor_legs } }, | |
| { bp_eyes, { bio_armor_eyes } } | |
| }; | |
| auto iter = armor_bionics.find( bp ); | |
| if( iter != armor_bionics.end() && has_bionic( iter->second ) && | |
| ( dt == DT_BASH || dt == DT_CUT || dt == DT_STAB ) ) { | |
| result += 3; | |
| } | |
| return result; | |
| } | |
| void player::passive_absorb_hit( body_part bp, damage_unit &du ) const | |
| { | |
| // >0 check because some mutations provide negative armor | |
| // Thin skin check goes before subdermal armor plates because SUBdermal | |
| if( du.amount > 0.0f ) { | |
| // Horrible hack warning! | |
| // Get rid of this as soon as CUT and STAB are split | |
| if( du.type == DT_STAB ) { | |
| damage_unit du_copy = du; | |
| du_copy.type = DT_CUT; | |
| du.amount -= mutation_armor( bp, du_copy ); | |
| } else { | |
| du.amount -= mutation_armor( bp, du ); | |
| } | |
| } | |
| du.amount -= bionic_armor_bonus( bp, du.type ); //Check for passive armor bionics | |
| du.amount -= mabuff_armor_bonus( du.type ); | |
| du.amount = std::max( 0.0f, du.amount ); | |
| } | |
| void player::absorb_hit( body_part bp, damage_instance &dam ) | |
| { | |
| std::list<item> worn_remains; | |
| bool armor_destroyed = false; | |
| for( auto &elem : dam.damage_units ) { | |
| if( elem.amount < 0 ) { | |
| // Prevents 0 damage hits (like from hallucinations) from ripping armor | |
| elem.amount = 0; | |
| continue; | |
| } | |
| // The bio_ads CBM absorbs damage before hitting armor | |
| if( has_active_bionic( bio_ads ) ) { | |
| if( elem.amount > 0 && power_level > 24 ) { | |
| if( elem.type == DT_BASH ) { | |
| elem.amount -= rng( 1, 8 ); | |
| } else if( elem.type == DT_CUT ) { | |
| elem.amount -= rng( 1, 4 ); | |
| } else if( elem.type == DT_STAB ) { | |
| elem.amount -= rng( 1, 2 ); | |
| } | |
| charge_power( -25 ); | |
| } | |
| if( elem.amount < 0 ) { | |
| elem.amount = 0; | |
| } | |
| } | |
| // Only the outermost armor can be set on fire | |
| bool outermost = true; | |
| // The worn vector has the innermost item first, so | |
| // iterate reverse to damage the outermost (last in worn vector) first. | |
| for( auto iter = worn.rbegin(); iter != worn.rend(); ) { | |
| item &armor = *iter; | |
| if( !armor.covers( bp ) ) { | |
| ++iter; | |
| continue; | |
| } | |
| const std::string pre_damage_name = armor.tname(); | |
| bool destroy = false; | |
| // Heat damage can set armor on fire | |
| // Even though it doesn't cause direct physical damage to it | |
| if( outermost && elem.type == DT_HEAT && elem.amount >= 1.0f ) { | |
| // TODO: Different fire intensity values based on damage | |
| fire_data frd{ 2 }; | |
| destroy = armor.burn( frd ); | |
| int fuel = roll_remainder( frd.fuel_produced ); | |
| if( fuel > 0 ) { | |
| add_effect( effect_onfire, time_duration::from_turns( fuel + 1 ), bp, false, 0, false, true ); | |
| } | |
| } | |
| if( !destroy ) { | |
| destroy = armor_absorb( elem, armor ); | |
| } | |
| if( destroy ) { | |
| if( g->u.sees( *this ) ) { | |
| SCT.add( posx(), posy(), NORTH, remove_color_tags( pre_damage_name ), | |
| m_neutral, _( "destroyed" ), m_info ); | |
| } | |
| destroyed_armor_msg( *this, pre_damage_name ); | |
| armor_destroyed = true; | |
| armor.on_takeoff( *this ); | |
| worn_remains.insert( worn_remains.end(), armor.contents.begin(), armor.contents.end() ); | |
| // decltype is the type name of the iterator, note that reverse_iterator::base returns the | |
| // iterator to the next element, not the one the revers_iterator points to. | |
| // http://stackoverflow.com/questions/1830158/how-to-call-erase-with-a-reverse-iterator | |
| iter = decltype( iter )( worn.erase( --( iter.base() ) ) ); | |
| } else { | |
| ++iter; | |
| outermost = false; | |
| } | |
| } | |
| passive_absorb_hit( bp, elem ); | |
| if( elem.type == DT_BASH ) { | |
| if( has_trait( trait_LIGHT_BONES ) ) { | |
| elem.amount *= 1.4; | |
| } | |
| if( has_trait( trait_HOLLOW_BONES ) ) { | |
| elem.amount *= 1.8; | |
| } | |
| } | |
| elem.amount = std::max( elem.amount, 0.0f ); | |
| } | |
| for( item &remain : worn_remains ) { | |
| g->m.add_item_or_charges( pos(), remain ); | |
| } | |
| if( armor_destroyed ) { | |
| drop_invalid_inventory(); | |
| } | |
| } | |
| int player::get_env_resist( body_part bp ) const | |
| { | |
| int ret = 0; | |
| for( auto &i : worn ) { | |
| // Head protection works on eyes too (e.g. baseball cap) | |
| if( i.covers( bp ) || ( bp == bp_eyes && i.covers( bp_head ) ) ) { | |
| ret += i.get_env_resist(); | |
| } | |
| } | |
| if( bp == bp_mouth && has_bionic( bio_purifier ) && ret < 5 ) { | |
| ret += 2; | |
| if( ret > 5 ) { | |
| ret = 5; | |
| } | |
| } | |
| if( bp == bp_eyes && has_bionic( bio_armor_eyes ) && ret < 5 ) { | |
| ret += 2; | |
| if( ret > 5 ) { | |
| ret = 5; | |
| } | |
| } | |
| if( bp == bp_eyes && has_trait( trait_SEESLEEP ) ) { | |
| ret += 8; | |
| } | |
| return ret; | |
| } | |
| bool player::wearing_something_on( body_part bp ) const | |
| { | |
| for( auto &i : worn ) { | |
| if( i.covers( bp ) ) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool player::natural_attack_restricted_on( body_part bp ) const | |
| { | |
| for( auto &i : worn ) { | |
| if( i.covers( bp ) && !i.has_flag( "ALLOWS_NATURAL_ATTACKS" ) ) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool player::is_wearing_shoes( const side &which_side ) const | |
| { | |
| bool left = true; | |
| bool right = true; | |
| if( which_side == side::LEFT || which_side == side::BOTH ) { | |
| left = false; | |
| for( const item &worn_item : worn ) { | |
| if( worn_item.covers( bp_foot_l ) && | |
| !worn_item.has_flag( "BELTED" ) && | |
| !worn_item.has_flag( "SKINTIGHT" ) ) { | |
| left = true; | |
| break; | |
| } | |
| } | |
| } | |
| if( which_side == side::RIGHT || which_side == side::BOTH ) { | |
| right = false; | |
| for( const item &worn_item : worn ) { | |
| if( worn_item.covers( bp_foot_r ) && | |
| !worn_item.has_flag( "BELTED" ) && | |
| !worn_item.has_flag( "SKINTIGHT" ) ) { | |
| right = true; | |
| break; | |
| } | |
| } | |
| } | |
| return ( left && right ); | |
| } | |
| bool player::is_wearing_helmet() const | |
| { | |
| for( const item &i : worn ) { | |
| if( i.covers( bp_head ) && | |
| !i.has_flag( "HELMET_COMPAT" ) && | |
| !i.has_flag( "SKINTIGHT" ) && | |
| !i.has_flag( "OVERSIZE" ) ) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| int player::head_cloth_encumbrance() const | |
| { | |
| int ret = 0; | |
| for( auto &i : worn ) { | |
| const item *worn_item = &i; | |
| if( i.covers( bp_head ) && ( worn_item->has_flag( "HELMET_COMPAT" ) || | |
| worn_item->has_flag( "SKINTIGHT" ) ) ) { | |
| ret += worn_item->get_encumber( *this ); | |
| } | |
| } | |
| return ret; | |
| } | |
| double player::footwear_factor() const | |
| { | |
| double ret = 0; | |
| if( wearing_something_on( bp_foot_l ) ) { | |
| ret += .5; | |
| } | |
| if( wearing_something_on( bp_foot_r ) ) { | |
| ret += .5; | |
| } | |
| return ret; | |
| } | |
| double player::armwear_factor() const | |
| { | |
| double ret = 0; | |
| if( wearing_something_on( bp_arm_l ) ) { | |
| ret += .5; | |
| } | |
| if( wearing_something_on( bp_arm_r ) ) { | |
| ret += .5; | |
| } | |
| return ret; | |
| } | |
| int player::shoe_type_count( const itype_id &it ) const | |
| { | |
| int ret = 0; | |
| if( is_wearing_on_bp( it, bp_foot_l ) ) { | |
| ret++; | |
| } | |
| if( is_wearing_on_bp( it, bp_foot_r ) ) { | |
| ret++; | |
| } | |
| return ret; | |
| } | |
| bool player::is_wearing_power_armor( bool *hasHelmet ) const | |
| { | |
| bool result = false; | |
| for( auto &elem : worn ) { | |
| if( !elem.is_power_armor() ) { | |
| continue; | |
| } | |
| if( hasHelmet == nullptr ) { | |
| // found power armor, helmet not requested, cancel loop | |
| return true; | |
| } | |
| // found power armor, continue search for helmet | |
| result = true; | |
| if( elem.covers( bp_head ) ) { | |
| *hasHelmet = true; | |
| return true; | |
| } | |
| } | |
| return result; | |
| } | |
| int player::adjust_for_focus( int amount ) const | |
| { | |
| int effective_focus = focus_pool; | |
| if( has_trait( trait_FASTLEARNER ) ) { | |
| effective_focus += 15; | |
| } | |
| if( has_active_bionic( bio_memory ) ) { | |
| effective_focus += 10; | |
| } | |
| if( has_trait( trait_SLOWLEARNER ) ) { | |
| effective_focus -= 15; | |
| } | |
| double tmp = amount * ( effective_focus / 100.0 ); | |
| return roll_remainder( tmp ); | |
| } | |
| void player::practice( const skill_id &id, int amount, int cap ) | |
| { | |
| SkillLevel &level = get_skill_level_object( id ); | |
| const Skill &skill = id.obj(); | |
| std::string skill_name = skill.name(); | |
| if( !level.can_train() ) { | |
| // If leveling is disabled, don't train, don't drain focus, don't print anything | |
| // Leaving as a skill method rather than global for possible future skill cap setting | |
| return; | |
| } | |
| const auto highest_skill = [&]() { | |
| std::pair<skill_id, int> result( skill_id::NULL_ID(), -1 ); | |
| for( const auto &pair : *_skills ) { | |
| const SkillLevel &lobj = pair.second; | |
| if( lobj.level() > result.second ) { | |
| result = std::make_pair( pair.first, lobj.level() ); | |
| } | |
| } | |
| return result.first; | |
| }; | |
| const bool isSavant = has_trait( trait_SAVANT ); | |
| const skill_id savantSkill = isSavant ? highest_skill() : skill_id::NULL_ID(); | |
| amount = adjust_for_focus( amount ); | |
| if( has_trait( trait_PACIFIST ) && skill.is_combat_skill() ) { | |
| if( !one_in( 3 ) ) { | |
| amount = 0; | |
| } | |
| } | |
| if( has_trait( trait_PRED2 ) && skill.is_combat_skill() ) { | |
| if( one_in( 3 ) ) { | |
| amount *= 2; | |
| } | |
| } | |
| if( has_trait( trait_PRED3 ) && skill.is_combat_skill() ) { | |
| amount *= 2; | |
| } | |
| if( has_trait( trait_PRED4 ) && skill.is_combat_skill() ) { | |
| amount *= 3; | |
| } | |
| if( isSavant && id != savantSkill ) { | |
| amount /= 2; | |
| } | |
| if( amount > 0 && get_skill_level( id ) > cap ) { //blunt grinding cap implementation for crafting | |
| amount = 0; | |
| if( is_player() && one_in( 5 ) ) { //remind the player intermittently that no skill gain takes place | |
| int curLevel = get_skill_level( id ); | |
| add_msg( m_info, _( "This task is too simple to train your %s beyond %d." ), | |
| skill_name, curLevel ); | |
| } | |
| } | |
| if( amount > 0 && level.isTraining() ) { | |
| int oldLevel = get_skill_level( id ); | |
| get_skill_level_object( id ).train( amount ); | |
| int newLevel = get_skill_level( id ); | |
| if( is_player() && newLevel > oldLevel ) { | |
| add_msg( m_good, _( "Your skill in %s has increased to %d!" ), skill_name, newLevel ); | |
| } | |
| if( is_player() && newLevel > cap ) { | |
| //inform player immediately that the current recipe can't be used to train further | |
| add_msg( m_info, _( "You feel that %s tasks of this level are becoming trivial." ), | |
| skill_name ); | |
| } | |
| int chance_to_drop = focus_pool; | |
| focus_pool -= chance_to_drop / 100; | |
| // Apex Predators don't think about much other than killing. | |
| // They don't lose Focus when practicing combat skills. | |
| if( ( rng( 1, 100 ) <= ( chance_to_drop % 100 ) ) && ( !( has_trait( trait_PRED4 ) && | |
| skill.is_combat_skill() ) ) ) { | |
| focus_pool--; | |
| } | |
| } | |
| get_skill_level_object( id ).practice(); | |
| } | |
| int player::exceeds_recipe_requirements( const recipe &rec ) const | |
| { | |
| return get_all_skills().exceeds_recipe_requirements( rec ); | |
| } | |
| bool player::has_recipe_requirements( const recipe &rec ) const | |
| { | |
| return get_all_skills().has_recipe_requirements( rec ); | |
| } | |
| bool player::can_decomp_learn( const recipe &rec ) const | |
| { | |
| return !rec.learn_by_disassembly.empty() && | |
| meets_skill_requirements( rec.learn_by_disassembly ); | |
| } | |
| bool player::knows_recipe( const recipe *rec ) const | |
| { | |
| return get_learned_recipes().contains( rec ); | |
| } | |
| int player::has_recipe( const recipe *r, const inventory &crafting_inv, | |
| const std::vector<npc *> &helpers ) const | |
| { | |
| if( !r->skill_used ) { | |
| return 0; | |
| } | |
| if( knows_recipe( r ) ) { | |
| return r->difficulty; | |
| } | |
| const auto available = get_available_recipes( crafting_inv, &helpers ); | |
| return available.contains( r ) ? available.get_custom_difficulty( r ) : -1; | |
| } | |
| void player::learn_recipe( const recipe *const rec ) | |
| { | |
| if( rec->never_learn ) { | |
| return; | |
| } | |
| learned_recipes->include( rec ); | |
| } | |
| void player::assign_activity( const activity_id &type, int moves, int index, int pos, | |
| const std::string &name ) | |
| { | |
| assign_activity( player_activity( type, moves, index, pos, name ) ); | |
| } | |
| void player::assign_activity( const player_activity &act, bool allow_resume ) | |
| { | |
| if( allow_resume && !backlog.empty() && backlog.front().can_resume_with( act, *this ) ) { | |
| add_msg_if_player( _( "You resume your task." ) ); | |
| activity = backlog.front(); | |
| backlog.pop_front(); | |
| } else { | |
| if( activity ) { | |
| backlog.push_front( activity ); | |
| } | |
| activity = act; | |
| } | |
| if( activity.rooted() ) { | |
| rooted_message(); | |
| } | |
| if( is_npc() ) { | |
| npc *guy = dynamic_cast<npc *>( this ); | |
| guy->current_activity = activity.get_verb(); | |
| } | |
| } | |
| bool player::has_activity( const activity_id &type ) const | |
| { | |
| return activity.id() == type; | |
| } | |
| bool player::has_activity( const std::vector<activity_id> &types ) const | |
| { | |
| return std::find( types.begin(), types.end(), activity.id() ) != types.end() ; | |
| } | |
| void player::cancel_activity() | |
| { | |
| if( has_activity( activity_id( "ACT_MOVE_ITEMS" ) ) && is_hauling() ) { | |
| stop_hauling(); | |
| } | |
| // Clear any backlog items that aren't auto-resume. | |
| for( auto backlog_item = backlog.begin(); backlog_item != backlog.end(); ) { | |
| if( backlog_item->auto_resume ) { | |
| backlog_item++; | |
| } else { | |
| backlog_item = backlog.erase( backlog_item ); | |
| } | |
| } | |
| if( activity && activity.is_suspendable() ) { | |
| backlog.push_front( activity ); | |
| } | |
| sfx::end_activity_sounds(); // kill activity sounds when canceled | |
| activity = player_activity(); | |
| } | |
| void player::resume_backlog_activity() | |
| { | |
| if( !backlog.empty() && backlog.front().auto_resume ) { | |
| activity = backlog.front(); | |
| backlog.pop_front(); | |
| } | |
| } | |
| bool player::has_gun_for_ammo( const ammotype &at ) const | |
| { | |
| return has_item_with( [at]( const item & it ) { | |
| // item::ammo_type considers the active gunmod. | |
| return it.is_gun() && it.ammo_types().count( at ); | |
| } ); | |
| } | |
| bool player::has_magazine_for_ammo( const ammotype &at ) const | |
| { | |
| return has_item_with( [&at]( const item & it ) { | |
| return !it.has_flag( "NO_RELOAD" ) && | |
| ( ( it.is_magazine() && it.ammo_types().count( at ) ) || | |
| ( it.is_gun() && it.magazine_integral() && it.ammo_types().count( at ) ) || | |
| ( it.is_gun() && it.magazine_current() != nullptr && | |
| it.magazine_current()->ammo_types().count( at ) ) ); | |
| } ); | |
| } | |
| // mytest return weapon name to display in sidebar | |
| std::string player::weapname( unsigned int truncate ) const | |
| { | |
| if( weapon.is_gun() ) { | |
| std::string str = string_format( "(%s) %s", weapon.gun_current_mode().tname(), weapon.type_name() ); | |
| // Is either the base item or at least one auxiliary gunmod loaded (includes empty magazines) | |
| bool base = weapon.ammo_capacity() > 0 && !weapon.has_flag( "RELOAD_AND_SHOOT" ); | |
| const auto mods = weapon.gunmods(); | |
| bool aux = std::any_of( mods.begin(), mods.end(), [&]( const item * e ) { | |
| return e->is_gun() && e->ammo_capacity() > 0 && !e->has_flag( "RELOAD_AND_SHOOT" ); | |
| } ); | |
| if( base || aux ) { | |
| str += " ("; | |
| if( base ) { | |
| str += std::to_string( weapon.ammo_remaining() ); | |
| if( weapon.magazine_integral() ) { | |
| str += "/" + std::to_string( weapon.ammo_capacity() ); | |
| } | |
| } else { | |
| str += "---"; | |
| } | |
| str += ")"; | |
| for( auto e : mods ) { | |
| if( e->is_gun() && e->ammo_capacity() > 0 && !e->has_flag( "RELOAD_AND_SHOOT" ) ) { | |
| str += " (" + std::to_string( e->ammo_remaining() ); | |
| if( e->magazine_integral() ) { | |
| str += "/" + std::to_string( e->ammo_capacity() ); | |
| } | |
| str += ")"; | |
| } | |
| } | |
| } | |
| return str; | |
| } else if( weapon.is_container() && weapon.contents.size() == 1 ) { | |
| return string_format( "%s (%d)", weapon.tname(), | |
| weapon.contents.front().charges ); | |
| } else if( !is_armed() ) { | |
| return _( "fists" ); | |
| } else { | |
| return weapon.tname( 1, true, truncate ); | |
| } | |
| } | |
| bool player::wield_contents( item &container, int pos, bool penalties, int base_cost ) | |
| { | |
| // if index not specified and container has multiple items then ask the player to choose one | |
| if( pos < 0 ) { | |
| std::vector<std::string> opts; | |
| std::transform( container.contents.begin(), container.contents.end(), | |
| std::back_inserter( opts ), []( const item & elem ) { | |
| return elem.display_name(); | |
| } ); | |
| if( opts.size() > 1 ) { | |
| pos = uilist( _( "Wield what?" ), opts ); | |
| if( pos < 0 ) { | |
| return false; | |
| } | |
| } else { | |
| pos = 0; | |
| } | |
| } | |
| if( pos >= static_cast<int>( container.contents.size() ) ) { | |
| debugmsg( "Tried to wield non-existent item from container (player::wield_contents)" ); | |
| return false; | |
| } | |
| auto target = std::next( container.contents.begin(), pos ); | |
| const auto ret = can_wield( *target ); | |
| if( !ret.success() ) { | |
| add_msg_if_player( m_info, "%s", ret.c_str() ); | |
| return false; | |
| } | |
| int mv = 0; | |
| if( is_armed() ) { | |
| if( !unwield() ) { | |
| return false; | |
| } | |
| inv.unsort(); | |
| } | |
| weapon = std::move( *target ); | |
| container.contents.erase( target ); | |
| container.on_contents_changed(); | |
| inv.update_invlet( weapon ); | |
| inv.update_cache_with_item( weapon ); | |
| last_item = weapon.typeId(); | |
| /** | |
| * @EFFECT_PISTOL decreases time taken to draw pistols from holsters | |
| * @EFFECT_SMG decreases time taken to draw smgs from holsters | |
| * @EFFECT_RIFLE decreases time taken to draw rifles from holsters | |
| * @EFFECT_SHOTGUN decreases time taken to draw shotguns from holsters | |
| * @EFFECT_LAUNCHER decreases time taken to draw launchers from holsters | |
| * @EFFECT_STABBING decreases time taken to draw stabbing weapons from sheathes | |
| * @EFFECT_CUTTING decreases time taken to draw cutting weapons from scabbards | |
| * @EFFECT_BASHING decreases time taken to draw bashing weapons from holsters | |
| */ | |
| int lvl = get_skill_level( weapon.is_gun() ? weapon.gun_skill() : weapon.melee_skill() ); | |
| mv += item_handling_cost( weapon, penalties, base_cost ) / ( ( lvl + 10.0f ) / 10.0f ); | |
| moves -= mv; | |
| weapon.on_wield( *this, mv ); | |
| return true; | |
| } | |
| void player::store( item &container, item &put, bool penalties, int base_cost ) | |
| { | |
| moves -= item_store_cost( put, container, penalties, base_cost ); | |
| container.put_in( i_rem( &put ) ); | |
| } | |
| nc_color encumb_color( int level ) | |
| { | |
| if( level < 0 ) { | |
| return c_green; | |
| } | |
| if( level < 10 ) { | |
| return c_light_gray; | |
| } | |
| if( level < 40 ) { | |
| return c_yellow; | |
| } | |
| if( level < 70 ) { | |
| return c_light_red; | |
| } | |
| return c_red; | |
| } | |
| float player::get_melee() const | |
| { | |
| return get_skill_level( skill_id( "melee" ) ); | |
| } | |
| void player::setID( int i ) | |
| { | |
| if( id >= 0 ) { | |
| debugmsg( "tried to set id of a npc/player, but has already a id: %d", id ); | |
| } else if( i < -1 ) { | |
| debugmsg( "tried to set invalid id of a npc/player: %d", i ); | |
| } else { | |
| id = i; | |
| } | |
| } | |
| int player::getID() const | |
| { | |
| return this->id; | |
| } | |
| bool player::uncanny_dodge() | |
| { | |
| bool is_u = this == &g->u; | |
| bool seen = g->u.sees( *this ); | |
| if( this->power_level < 74 || !this->has_active_bionic( bio_uncanny_dodge ) ) { | |
| return false; | |
| } | |
| tripoint adjacent = adjacent_tile(); | |
| charge_power( -75 ); | |
| if( adjacent.x != posx() || adjacent.y != posy() ) { | |
| position.x = adjacent.x; | |
| position.y = adjacent.y; | |
| if( is_u ) { | |
| add_msg( _( "Time seems to slow down and you instinctively dodge!" ) ); | |
| } else if( seen ) { | |
| add_msg( _( "%s dodges... so fast!" ), this->disp_name() ); | |
| } | |
| return true; | |
| } | |
| if( is_u ) { | |
| add_msg( _( "You try to dodge but there's no room!" ) ); | |
| } else if( seen ) { | |
| add_msg( _( "%s tries to dodge but there's no room!" ), this->disp_name() ); | |
| } | |
| return false; | |
| } | |
| int player::climbing_cost( const tripoint &from, const tripoint &to ) const | |
| { | |
| if( !g->m.valid_move( from, to, false, true ) ) { | |
| return 0; | |
| } | |
| const int diff = g->m.climb_difficulty( from ); | |
| if( diff > 5 ) { | |
| return 0; | |
| } | |
| return 50 + diff * 100; | |
| // TODO: All sorts of mutations, equipment weight etc. | |
| } | |
| void player::environmental_revert_effect() | |
| { | |
| addictions.clear(); | |
| morale->clear(); | |
| for( int part = 0; part < num_hp_parts; part++ ) { | |
| hp_cur[part] = hp_max[part]; | |
| } | |
| set_hunger( 0 ); | |
| set_thirst( 0 ); | |
| set_fatigue( 0 ); | |
| set_healthy( 0 ); | |
| set_healthy_mod( 0 ); | |
| stim = 0; | |
| set_pain( 0 ); | |
| set_painkiller( 0 ); | |
| radiation = 0; | |
| recalc_sight_limits(); | |
| reset_encumbrance(); | |
| } | |
| bool player::is_invisible() const | |
| { | |
| static const bionic_id str_bio_cloak( "bio_cloak" ); // This function used in monster::plan_moves | |
| static const bionic_id str_bio_night( "bio_night" ); | |
| return ( | |
| has_active_bionic( str_bio_cloak ) || | |
| has_active_bionic( str_bio_night ) || | |
| has_active_optcloak() || | |
| has_trait( trait_DEBUG_CLOAK ) || | |
| has_artifact_with( AEP_INVISIBLE ) | |
| ); | |
| } | |
| int player::visibility( bool, int ) const | |
| { | |
| // 0-100 % | |
| if( is_invisible() ) { | |
| return 0; | |
| } | |
| // TODO: | |
| // if ( dark_clothing() && light check ... | |
| int stealth_modifier = std::floor( mutation_value( "stealth_modifier" ) ); | |
| return clamp( 100 - stealth_modifier, 40, 160 ); | |
| } | |
| void player::set_destination( const std::vector<tripoint> &route, | |
| const player_activity &destination_activity ) | |
| { | |
| auto_move_route = route; | |
| this->destination_activity = destination_activity; | |
| destination_point.emplace( g->m.getabs( route.back() ) ); | |
| } | |
| void player::clear_destination() | |
| { | |
| auto_move_route.clear(); | |
| destination_activity = player_activity(); | |
| destination_point = cata::nullopt; | |
| next_expected_position = cata::nullopt; | |
| } | |
| bool player::has_distant_destination() const | |
| { | |
| return has_destination() && !destination_activity.is_null() && | |
| destination_activity.id() == "ACT_TRAVELLING" && !omt_path.empty(); | |
| } | |
| bool player::has_destination() const | |
| { | |
| return !auto_move_route.empty(); | |
| } | |
| bool player::has_destination_activity() const | |
| { | |
| return !destination_activity.is_null() && destination_point && | |
| position == g->m.getlocal( *destination_point ); | |
| } | |
| void player::start_destination_activity() | |
| { | |
| if( !has_destination_activity() ) { | |
| debugmsg( "Tried to start invalid destination activity" ); | |
| return; | |
| } | |
| assign_activity( destination_activity ); | |
| clear_destination(); | |
| } | |
| std::vector<tripoint> &player::get_auto_move_route() | |
| { | |
| return auto_move_route; | |
| } | |
| action_id player::get_next_auto_move_direction() | |
| { | |
| if( !has_destination() ) { | |
| return ACTION_NULL; | |
| } | |
| if( next_expected_position ) { | |
| if( pos() != *next_expected_position ) { | |
| // We're off course, possibly stumbling or stuck, cancel auto move | |
| return ACTION_NULL; | |
| } | |
| } | |
| next_expected_position.emplace( auto_move_route.front() ); | |
| auto_move_route.erase( auto_move_route.begin() ); | |
| tripoint dp = *next_expected_position - pos(); | |
| // Make sure the direction is just one step and that | |
| // all diagonal moves have 0 z component | |
| if( abs( dp.x ) > 1 || abs( dp.y ) > 1 || abs( dp.z ) > 1 || | |
| ( abs( dp.z ) != 0 && ( abs( dp.x ) != 0 || abs( dp.y ) != 0 ) ) ) { | |
| // Should never happen, but check just in case | |
| return ACTION_NULL; | |
| } | |
| return get_movement_direction_from_delta( dp.x, dp.y, dp.z ); | |
| } | |
| bool player::defer_move( const tripoint &next ) | |
| { | |
| // next must be adjacent to current pos | |
| if( square_dist( next, pos() ) != 1 ) { | |
| return false; | |
| } | |
| // next must be adjacent to subsequent move in any preexisting automove route | |
| if( has_destination() && square_dist( auto_move_route.front(), next ) != 1 ) { | |
| return false; | |
| } | |
| auto_move_route.insert( auto_move_route.begin(), next ); | |
| next_expected_position = pos(); | |
| return true; | |
| } | |
| void player::shift_destination( int shiftx, int shifty ) | |
| { | |
| if( next_expected_position ) { | |
| next_expected_position->x += shiftx; | |
| next_expected_position->y += shifty; | |
| } | |
| for( auto &elem : auto_move_route ) { | |
| elem.x += shiftx; | |
| elem.y += shifty; | |
| } | |
| } | |
| void player::start_hauling() | |
| { | |
| add_msg( _( "You start hauling items along the ground." ) ); | |
| if( is_armed() ) { | |
| add_msg( m_warning, _( "Your hands are not free, which makes hauling slower." ) ); | |
| } | |
| hauling = true; | |
| } | |
| void player::stop_hauling() | |
| { | |
| add_msg( _( "You stop hauling items." ) ); | |
| hauling = false; | |
| if( has_activity( activity_id( "ACT_MOVE_ITEMS" ) ) ) { | |
| cancel_activity(); | |
| } | |
| } | |
| bool player::is_hauling() const | |
| { | |
| return hauling; | |
| } | |
| bool player::has_weapon() const | |
| { | |
| return !unarmed_attack(); | |
| } | |
| m_size player::get_size() const | |
| { | |
| if( has_trait( trait_id( "SMALL2" ) ) || has_trait( trait_id( "SMALL_OK" ) ) || | |
| has_trait( trait_id( "SMALL" ) ) ) { | |
| return MS_SMALL; | |
| } else if( has_trait( trait_LARGE ) || has_trait( trait_LARGE_OK ) ) { | |
| return MS_LARGE; | |
| } else if( has_trait( trait_HUGE ) || has_trait( trait_HUGE_OK ) ) { | |
| return MS_HUGE; | |
| } | |
| return MS_MEDIUM; | |
| } | |
| int player::get_hp() const | |
| { | |
| return get_hp( num_hp_parts ); | |
| } | |
| int player::get_hp( hp_part bp ) const | |
| { | |
| if( bp < num_hp_parts ) { | |
| return hp_cur[bp]; | |
| } | |
| int hp_total = 0; | |
| for( int i = 0; i < num_hp_parts; ++i ) { | |
| hp_total += hp_cur[i]; | |
| } | |
| return hp_total; | |
| } | |
| int player::get_hp_max() const | |
| { | |
| return get_hp_max( num_hp_parts ); | |
| } | |
| int player::get_hp_max( hp_part bp ) const | |
| { | |
| if( bp < num_hp_parts ) { | |
| return hp_max[bp]; | |
| } | |
| int hp_total = 0; | |
| for( int i = 0; i < num_hp_parts; ++i ) { | |
| hp_total += hp_max[i]; | |
| } | |
| return hp_total; | |
| } | |
| int player::get_stamina_max() const | |
| { | |
| int maxStamina = get_option< int >( "PLAYER_MAX_STAMINA" ); | |
| maxStamina *= Character::mutation_value( "max_stamina_modifier" ); | |
| return maxStamina; | |
| } | |
| void player::burn_move_stamina( int moves ) | |
| { | |
| int overburden_percentage = 0; | |
| units::mass current_weight = weight_carried(); | |
| units::mass max_weight = weight_capacity(); | |
| if( current_weight > max_weight ) { | |
| overburden_percentage = ( current_weight - max_weight ) * 100 / max_weight; | |
| } | |
| int burn_ratio = get_option<int>( "PLAYER_BASE_STAMINA_BURN_RATE" ); | |
| if( g->u.has_active_bionic( bionic_id( "bio_torsionratchet" ) ) ) { | |
| burn_ratio = burn_ratio * 2 - 3; | |
| } | |
| burn_ratio += overburden_percentage; | |
| if( move_mode == PMM_RUN ) { | |
| burn_ratio = burn_ratio * 7; | |
| } | |
| mod_stat( "stamina", -( ( moves * burn_ratio ) / 100 ) ); | |
| add_msg( m_debug, "Stamina burn: %d", -( ( moves * burn_ratio ) / 100 ) ); | |
| // Chance to suffer pain if overburden and stamina runs out or has trait BADBACK | |
| // Starts at 1 in 25, goes down by 5 for every 50% more carried | |
| if( ( current_weight > max_weight ) && ( has_trait( trait_BADBACK ) || stamina == 0 ) && | |
| one_in( 35 - 5 * current_weight / ( max_weight / 2 ) ) ) { | |
| add_msg_if_player( m_bad, _( "Your body strains under the weight!" ) ); | |
| // 1 more pain for every 800 grams more (5 per extra STR needed) | |
| if( ( ( current_weight - max_weight ) / 800_gram > get_pain() && get_pain() < 100 ) ) { | |
| mod_pain( 1 ); | |
| } | |
| } | |
| } | |
| void player::forced_dismount() | |
| { | |
| remove_effect( effect_riding ); | |
| if( mounted_creature ) { | |
| auto mon = mounted_creature.get(); | |
| mon->remove_effect( effect_ridden ); | |
| mounted_creature = nullptr; | |
| } | |
| std::vector<tripoint> valid; | |
| for( const tripoint &jk : g->m.points_in_radius( pos(), 1 ) ) { | |
| if( g->is_empty( jk ) ) { | |
| valid.push_back( jk ); | |
| } | |
| } | |
| if( !valid.empty() ) { | |
| setpos( random_entry( valid ) ); | |
| add_msg( m_bad, _( "You fall off your mount!" ) ); | |
| const int dodge = get_dodge(); | |
| const int damage = std::max( 0, rng( 1, 20 ) - rng( dodge, dodge * 2 ) ); | |
| body_part hit = num_bp; | |
| switch( rng( 1, 10 ) ) { | |
| case 1: | |
| if( one_in( 2 ) ) { | |
| hit = bp_foot_l; | |
| } else { | |
| hit = bp_foot_r; | |
| } | |
| break; | |
| case 2: | |
| case 3: | |
| case 4: | |
| if( one_in( 2 ) ) { | |
| hit = bp_leg_l; | |
| } else { | |
| hit = bp_leg_r; | |
| } | |
| break; | |
| case 5: | |
| case 6: | |
| case 7: | |
| if( one_in( 2 ) ) { | |
| hit = bp_arm_l; | |
| } else { | |
| hit = bp_arm_r; | |
| } | |
| break; | |
| case 8: | |
| case 9: | |
| hit = bp_torso; | |
| break; | |
| case 10: | |
| hit = bp_head; | |
| break; | |
| } | |
| if( damage > 0 ) { | |
| add_msg( m_bad, _( "You hurt yourself!" ) ); | |
| deal_damage( nullptr, hit, damage_instance( DT_BASH, damage ) ); | |
| add_memorial_log( pgettext( "memorial_male", "Fell off a mount." ), | |
| pgettext( "memorial_female", "Fell off a mount." ) ); | |
| check_dead_state(); | |
| } | |
| add_effect( effect_downed, 5_turns, num_bp, true ); | |
| } else { | |
| add_msg( m_debug, "Forced_dismount could not find a square to deposit player" ); | |
| } | |
| moves -= 150; | |
| set_movement_mode( PMM_WALK ); | |
| g->update_map( g->u ); | |
| } | |
| void player::dismount() | |
| { | |
| if( has_effect( effect_riding ) && mounted_creature != nullptr ) { | |
| if( const cata::optional<tripoint> pnt = choose_adjacent( _( "Dismount where?" ) ) ) { | |
| if( g->is_empty( *pnt ) ) { | |
| tripoint temp_pt = *pnt; | |
| int xdiff = pos().x - temp_pt.x; | |
| int ydiff = pos().y - temp_pt.y; | |
| remove_effect( effect_riding ); | |
| monster *critter = mounted_creature.get(); | |
| critter->remove_effect( effect_ridden ); | |
| mounted_creature = nullptr; | |
| setpos( *pnt ); | |
| g->refresh_all(); | |
| critter->setpos( tripoint( pos().x - xdiff, pos().y - ydiff, pos().z ) ); | |
| mod_moves( -100 ); | |
| set_movement_mode( PMM_WALK ); | |
| return; | |
| } else { | |
| add_msg( m_warning, _( "You cannot dismount there!" ) ); | |
| return; | |
| } | |
| } | |
| } else { | |
| add_msg( m_debug, "dismount called when not riding" ); | |
| return; | |
| } | |
| } | |
| Creature::Attitude player::attitude_to( const Creature &other ) const | |
| { | |
| const auto m = dynamic_cast<const monster *>( &other ); | |
| if( m != nullptr ) { | |
| if( m->friendly != 0 ) { | |
| return A_FRIENDLY; | |
| } | |
| switch( m->attitude( const_cast<player *>( this ) ) ) { | |
| // player probably does not want to harm them, but doesn't care much at all. | |
| case MATT_FOLLOW: | |
| case MATT_FPASSIVE: | |
| case MATT_IGNORE: | |
| case MATT_FLEE: | |
| return A_NEUTRAL; | |
| // player does not want to harm those. | |
| case MATT_FRIEND: | |
| case MATT_ZLAVE: // Don't want to harm your zlave! | |
| return A_FRIENDLY; | |
| case MATT_ATTACK: | |
| return A_HOSTILE; | |
| case MATT_NULL: | |
| case NUM_MONSTER_ATTITUDES: | |
| break; | |
| } | |
| return A_NEUTRAL; | |
| } | |
| const auto p = dynamic_cast<const npc *>( &other ); | |
| if( p != nullptr ) { | |
| if( p->is_enemy() ) { | |
| return A_HOSTILE; | |
| } else if( p->is_player_ally() ) { | |
| return A_FRIENDLY; | |
| } else { | |
| return A_NEUTRAL; | |
| } | |
| } else if( &other == this ) { | |
| return A_FRIENDLY; | |
| } | |
| return A_NEUTRAL; | |
| } | |
| bool player::sees( const tripoint &t, bool, int ) const | |
| { | |
| static const bionic_id str_bio_night( "bio_night" ); | |
| const int wanted_range = rl_dist( pos(), t ); | |
| bool can_see = is_player() ? g->m.pl_sees( t, wanted_range ) : | |
| Creature::sees( t ); | |
| // Clairvoyance is now pretty cheap, so we can check it early | |
| if( wanted_range < MAX_CLAIRVOYANCE && wanted_range < clairvoyance() ) { | |
| return true; | |
| } | |
| // Only check if we need to override if we already came to the opposite conclusion. | |
| if( can_see && wanted_range < 15 && wanted_range > sight_range( 1 ) && | |
| has_active_bionic( str_bio_night ) ) { | |
| can_see = false; | |
| } | |
| if( can_see && wanted_range > unimpaired_range() ) { | |
| can_see = false; | |
| } | |
| return can_see; | |
| } | |
| bool player::sees( const Creature &critter ) const | |
| { | |
| // This handles only the player/npc specific stuff (monsters don't have traits or bionics). | |
| const int dist = rl_dist( pos(), critter.pos() ); | |
| if( dist <= 3 && has_active_mutation( trait_ANTENNAE ) ) { | |
| return true; | |
| } | |
| if( critter.digging() && has_active_bionic( bio_ground_sonar ) ) { | |
| // Bypass the check below, the bionic sonar also bypasses the sees(point) check because | |
| // walls don't block sonar which is transmitted in the ground, not the air. | |
| // TODO: this might need checks whether the player is in the air, or otherwise not connected | |
| // to the ground. It also might need a range check. | |
| return true; | |
| } | |
| return Creature::sees( critter ); | |
| } | |
| nc_color player::bodytemp_color( int bp ) const | |
| { | |
| nc_color color = c_light_gray; // default | |
| if( bp == bp_eyes ) { | |
| color = c_light_gray; // Eyes don't count towards warmth | |
| } else if( temp_conv[bp] > BODYTEMP_SCORCHING ) { | |
| color = c_red; | |
| } else if( temp_conv[bp] > BODYTEMP_VERY_HOT ) { | |
| color = c_light_red; | |
| } else if( temp_conv[bp] > BODYTEMP_HOT ) { | |
| color = c_yellow; | |
| } else if( temp_conv[bp] > BODYTEMP_COLD ) { | |
| color = c_green; | |
| } else if( temp_conv[bp] > BODYTEMP_VERY_COLD ) { | |
| color = c_light_blue; | |
| } else if( temp_conv[bp] > BODYTEMP_FREEZING ) { | |
| color = c_cyan; | |
| } else if( temp_conv[bp] <= BODYTEMP_FREEZING ) { | |
| color = c_blue; | |
| } | |
| return color; | |
| } | |
| //message related stuff | |
| void player::add_msg_if_player( const std::string &msg ) const | |
| { | |
| Messages::add_msg( msg ); | |
| } | |
| void player::add_msg_player_or_npc( const std::string &player_msg, | |
| const std::string &/*npc_msg*/ ) const | |
| { | |
| Messages::add_msg( player_msg ); | |
| } | |
| void player::add_msg_if_player( const game_message_type type, const std::string &msg ) const | |
| { | |
| Messages::add_msg( type, msg ); | |
| } | |
| void player::add_msg_player_or_npc( const game_message_type type, const std::string &player_msg, | |
| const std::string &/*npc_msg*/ ) const | |
| { | |
| Messages::add_msg( type, player_msg ); | |
| } | |
| void player::add_msg_player_or_say( const std::string &player_msg, | |
| const std::string &/*npc_speech*/ ) const | |
| { | |
| Messages::add_msg( player_msg ); | |
| } | |
| void player::add_msg_player_or_say( const game_message_type type, const std::string &player_msg, | |
| const std::string &/*npc_speech*/ ) const | |
| { | |
| Messages::add_msg( type, player_msg ); | |
| } | |
| bool player::knows_trap( const tripoint &pos ) const | |
| { | |
| const tripoint p = g->m.getabs( pos ); | |
| return known_traps.count( p ) > 0; | |
| } | |
| void player::add_known_trap( const tripoint &pos, const trap &t ) | |
| { | |
| const tripoint p = g->m.getabs( pos ); | |
| if( t.is_null() ) { | |
| known_traps.erase( p ); | |
| } else { | |
| // TODO: known_traps should map to a trap_str_id | |
| known_traps[p] = t.id.str(); | |
| } | |
| } | |
| bool player::is_deaf() const | |
| { | |
| return get_effect_int( effect_deaf ) > 2 || worn_with_flag( "DEAF" ) || has_trait( trait_DEAF ) || | |
| ( has_active_bionic( bio_earplugs ) && !has_active_bionic( bio_ears ) ) || | |
| ( has_trait( trait_M_SKIN3 ) && g->m.has_flag_ter_or_furn( "FUNGUS", pos() ) && in_sleep_state() ); | |
| } | |
| bool player::can_hear( const tripoint &source, const int volume ) const | |
| { | |
| if( is_deaf() ) { | |
| return false; | |
| } | |
| // source is in-ear and at our square, we can hear it | |
| if( source == pos() && volume == 0 ) { | |
| return true; | |
| } | |
| const int dist = rl_dist( source, pos() ); | |
| const float volume_multiplier = hearing_ability(); | |
| return ( volume - weather::sound_attn( g->weather.weather ) ) * volume_multiplier >= dist; | |
| } | |
| float player::hearing_ability() const | |
| { | |
| float volume_multiplier = 1.0; | |
| // Mutation/Bionic volume modifiers | |
| if( has_active_bionic( bio_ears ) && !has_active_bionic( bio_earplugs ) ) { | |
| volume_multiplier *= 3.5; | |
| } | |
| if( has_trait( trait_PER_SLIME ) ) { | |
| // Random hearing :-/ | |
| // (when it's working at all, see player.cpp) | |
| // changed from 0.5 to fix Mac compiling error | |
| volume_multiplier *= ( rng( 1, 2 ) ); | |
| } | |
| volume_multiplier *= Character::mutation_value( "hearing_modifier" ); | |
| if( has_effect( effect_deaf ) ) { | |
| // Scale linearly up to 30 minutes | |
| volume_multiplier *= ( 30_minutes - get_effect_dur( effect_deaf ) ) / 30_minutes; | |
| } | |
| if( has_effect( effect_earphones ) ) { | |
| volume_multiplier *= .25; | |
| } | |
| return volume_multiplier; | |
| } | |
| std::string player::visible_mutations( const int visibility_cap ) const | |
| { | |
| const std::string trait_str = enumerate_as_string( my_mutations.begin(), my_mutations.end(), | |
| [visibility_cap ]( const std::pair<trait_id, trait_data> &pr ) -> std::string { | |
| const auto &mut_branch = pr.first.obj(); | |
| // Finally some use for visibility trait of mutations | |
| if( mut_branch.visibility > 0 && mut_branch.visibility >= visibility_cap ) | |
| { | |
| return string_format( "<color_%s>%s</color>", string_from_color( mut_branch.get_display_color() ), | |
| mut_branch.name() ); | |
| } | |
| return std::string(); | |
| } ); | |
| return trait_str; | |
| } | |
| std::vector<std::string> player::short_description_parts() const | |
| { | |
| std::vector<std::string> result; | |
| if( is_armed() ) { | |
| result.push_back( _( "Wielding: " ) + weapon.tname() ); | |
| } | |
| const std::string worn_str = enumerate_as_string( worn.begin(), worn.end(), | |
| []( const item & it ) { | |
| return it.tname(); | |
| } ); | |
| if( !worn_str.empty() ) { | |
| result.push_back( _( "Wearing: " ) + worn_str ); | |
| } | |
| const int visibility_cap = 0; // no cap | |
| const auto trait_str = visible_mutations( visibility_cap ); | |
| if( !trait_str.empty() ) { | |
| result.push_back( _( "Traits: " ) + trait_str ); | |
| } | |
| return result; | |
| } | |
| std::string player::short_description() const | |
| { | |
| return join( short_description_parts(), "; " ); | |
| } | |
| int player::print_info( const catacurses::window &w, int vStart, int, int column ) const | |
| { | |
| mvwprintw( w, vStart++, column, _( "You (%s)" ), name ); | |
| return vStart; | |
| } | |
| bool player::is_visible_in_range( const Creature &critter, const int range ) const | |
| { | |
| return sees( critter ) && rl_dist( pos(), critter.pos() ) <= range; | |
| } | |
| std::vector<Creature *> player::get_visible_creatures( const int range ) const | |
| { | |
| return g->get_creatures_if( [this, range]( const Creature & critter ) -> bool { | |
| return this != &critter && pos() != critter.pos() && // TODO: get rid of fake npcs (pos() check) | |
| rl_dist( pos(), critter.pos() ) <= range && sees( critter ); | |
| } ); | |
| } | |
| std::vector<Creature *> player::get_targetable_creatures( const int range ) const | |
| { | |
| return g->get_creatures_if( [this, range]( const Creature & critter ) -> bool { | |
| return this != &critter && pos() != critter.pos() && // TODO: get rid of fake npcs (pos() check) | |
| rl_dist( pos(), critter.pos() ) <= range && | |
| ( sees( critter ) || sees_with_infrared( critter ) ); | |
| } ); | |
| } | |
| std::vector<Creature *> player::get_hostile_creatures( int range ) const | |
| { | |
| return g->get_creatures_if( [this, range]( const Creature & critter ) -> bool { | |
| float dist_to_creature; | |
| // Fixes circular distance range for ranged attacks | |
| if( !trigdist ) | |
| { | |
| dist_to_creature = rl_dist( pos(), critter.pos() ); | |
| } else | |
| { | |
| dist_to_creature = round( trig_dist( pos(), critter.pos() ) ); | |
| } | |
| return this != &critter && pos() != critter.pos() && // TODO: get rid of fake npcs (pos() check) | |
| dist_to_creature <= range && critter.attitude_to( *this ) == A_HOSTILE | |
| && sees( critter ); | |
| } ); | |
| } | |
| void player::place_corpse() | |
| { | |
| //If the character/NPC is on a distant mission, don't drop their their gear when they die since they still have a local pos | |
| if( !death_drops ) { | |
| return; | |
| } | |
| std::vector<item *> tmp = inv_dump(); | |
| item body = item::make_corpse( mtype_id::NULL_ID(), calendar::turn, name ); | |
| body.set_item_temperature( 310.15 ); | |
| for( auto itm : tmp ) { | |
| g->m.add_item_or_charges( pos(), *itm ); | |
| } | |
| for( auto &bio : *my_bionics ) { | |
| if( item::type_is_defined( bio.id.str() ) ) { | |
| body.put_in( item( bio.id.str(), calendar::turn ) ); | |
| } | |
| } | |
| // Restore amount of installed pseudo-modules of Power Storage Units | |
| std::pair<int, int> storage_modules = amount_of_storage_bionics(); | |
| for( int i = 0; i < storage_modules.first; ++i ) { | |
| body.emplace_back( "bio_power_storage" ); | |
| } | |
| for( int i = 0; i < storage_modules.second; ++i ) { | |
| body.emplace_back( "bio_power_storage_mkII" ); | |
| } | |
| g->m.add_item_or_charges( pos(), body ); | |
| } | |
| void player::place_corpse( const tripoint &om_target ) | |
| { | |
| tinymap bay; | |
| bay.load( om_target.x * 2, om_target.y * 2, om_target.z, false ); | |
| int finX = rng( 1, SEEX * 2 - 2 ); | |
| int finY = rng( 1, SEEX * 2 - 2 ); | |
| if( bay.furn( finX, finY ) != furn_str_id( "f_null" ) ) { | |
| for( int x = 0; x < SEEX * 2 - 1; x++ ) { | |
| for( int y = 0; y < SEEY * 2 - 1; y++ ) { | |
| if( bay.furn( x, y ) == furn_str_id( "f_null" ) ) { | |
| finX = x; | |
| finY = y; | |
| } | |
| } | |
| } | |
| } | |
| std::vector<item *> tmp = inv_dump(); | |
| item body = item::make_corpse( mtype_id::NULL_ID(), calendar::turn, name ); | |
| for( auto itm : tmp ) { | |
| bay.add_item_or_charges( finX, finY, *itm ); | |
| } | |
| for( auto &bio : *my_bionics ) { | |
| if( item::type_is_defined( bio.id.str() ) ) { | |
| body.put_in( item( bio.id.str(), calendar::turn ) ); | |
| } | |
| } | |
| // Restore amount of installed pseudo-modules of Power Storage Units | |
| std::pair<int, int> storage_modules = amount_of_storage_bionics(); | |
| for( int i = 0; i < storage_modules.first; ++i ) { | |
| body.emplace_back( "bio_power_storage" ); | |
| } | |
| for( int i = 0; i < storage_modules.second; ++i ) { | |
| body.emplace_back( "bio_power_storage_mkII" ); | |
| } | |
| bay.add_item_or_charges( finX, finY, body ); | |
| } | |
| bool player::sees_with_infrared( const Creature &critter ) const | |
| { | |
| const monster *m = dynamic_cast< const monster * >( &critter ); | |
| // electroreceptors grants vision of robots and electric monsters through walls | |
| if( m != nullptr && has_trait( trait_ELECTRORECEPTORS ) && ( m->type->in_species( ROBOT ) || | |
| critter.has_flag( MF_ELECTRIC ) ) ) { | |
| return true; | |
| } | |
| if( !vision_mode_cache[IR_VISION] || !critter.is_warm() ) { | |
| return false; | |
| } | |
| if( is_player() || critter.is_player() ) { | |
| // Players should not use map::sees | |
| // Likewise, players should not be "looked at" with map::sees, not to break symmetry | |
| return g->m.pl_line_of_sight( critter.pos(), sight_range( DAYLIGHT_LEVEL ) ); | |
| } | |
| return g->m.sees( pos(), critter.pos(), sight_range( DAYLIGHT_LEVEL ) ); | |
| } | |
| std::vector<std::string> player::get_overlay_ids() const | |
| { | |
| std::vector<std::string> rval; | |
| std::multimap<int, std::string> mutation_sorting; | |
| int order; | |
| std::string overlay_id; | |
| // first get effects | |
| for( const auto &eff_pr : *effects ) { | |
| rval.push_back( "effect_" + eff_pr.first.str() ); | |
| } | |
| // then get mutations | |
| for( const auto &mut : my_mutations ) { | |
| overlay_id = ( mut.second.powered ? "active_" : "" ) + mut.first.str(); | |
| order = get_overlay_order_of_mutation( overlay_id ); | |
| mutation_sorting.insert( std::pair<int, std::string>( order, overlay_id ) ); | |
| } | |
| // then get bionics | |
| for( const bionic &bio : *my_bionics ) { | |
| overlay_id = ( bio.powered ? "active_" : "" ) + bio.id.str(); | |
| order = get_overlay_order_of_mutation( overlay_id ); | |
| mutation_sorting.insert( std::pair<int, std::string>( order, overlay_id ) ); | |
| } | |
| for( auto &mutorder : mutation_sorting ) { | |
| rval.push_back( "mutation_" + mutorder.second ); | |
| } | |
| // next clothing | |
| // TODO: worry about correct order of clothing overlays | |
| for( const item &worn_item : worn ) { | |
| rval.push_back( "worn_" + worn_item.typeId() ); | |
| } | |
| // last weapon | |
| // TODO: might there be clothing that covers the weapon? | |
| if( is_armed() ) { | |
| rval.push_back( "wielded_" + weapon.typeId() ); | |
| } | |
| if( move_mode != PMM_WALK ) { | |
| rval.push_back( player_movemode_str[ move_mode ] ); | |
| } | |
| return rval; | |
| } | |
| void player::spores() | |
| { | |
| fungal_effects fe( *g, g->m ); | |
| //~spore-release sound | |
| sounds::sound( pos(), 10, sounds::sound_t::combat, _( "Pouf!" ), false, "misc", "puff" ); | |
| for( const tripoint &sporep : g->m.points_in_radius( pos(), 1 ) ) { | |
| if( sporep == pos() ) { | |
| continue; | |
| } | |
| fe.fungalize( sporep, this, 0.25 ); | |
| } | |
| } | |
| void player::blossoms() | |
| { | |
| // Player blossoms are shorter-ranged, but you can fire much more frequently if you like. | |
| sounds::sound( pos(), 10, sounds::sound_t::combat, _( "Pouf!" ), false, "misc", "puff" ); | |
| for( const tripoint &tmp : g->m.points_in_radius( pos(), 2 ) ) { | |
| g->m.add_field( tmp, fd_fungal_haze, rng( 1, 2 ) ); | |
| } | |
| } | |
| float player::power_rating() const | |
| { | |
| int dmg = std::max( { weapon.damage_melee( DT_BASH ), | |
| weapon.damage_melee( DT_CUT ), | |
| weapon.damage_melee( DT_STAB ) | |
| } ); | |
| int ret = 2; | |
| // Small guns can be easily hidden from view | |
| if( weapon.volume() <= 250_ml ) { | |
| ret = 2; | |
| } else if( weapon.is_gun() ) { | |
| ret = 4; | |
| } else if( dmg > 12 ) { | |
| ret = 3; // Melee weapon or weapon-y tool | |
| } | |
| if( has_trait( trait_HUGE ) || has_trait( trait_HUGE_OK ) ) { | |
| ret += 1; | |
| } | |
| if( is_wearing_power_armor( nullptr ) ) { | |
| ret = 5; // No mercy! | |
| } | |
| return ret; | |
| } | |
| float player::speed_rating() const | |
| { | |
| float ret = get_speed() / 100.0f; | |
| ret *= 100.0f / run_cost( 100, false ); | |
| // Adjustment for player being able to run, but not doing so at the moment | |
| if( move_mode != PMM_RUN ) { | |
| ret *= 1.0f + ( static_cast<float>( stamina ) / static_cast<float>( get_stamina_max() ) ); | |
| } | |
| return ret; | |
| } | |
| std::vector<const item *> player::all_items_with_flag( const std::string &flag ) const | |
| { | |
| return items_with( [&flag]( const item & it ) { | |
| return it.has_flag( flag ); | |
| } ); | |
| } | |
| bool player::has_item_with_flag( const std::string &flag, bool need_charges ) const | |
| { | |
| return has_item_with( [&flag, &need_charges]( const item & it ) { | |
| if( it.is_tool() && need_charges ) { | |
| return it.has_flag( flag ) && it.type->tool->max_charges ? it.charges > 0 : it.has_flag( flag ); | |
| } | |
| return it.has_flag( flag ); | |
| } ); | |
| } | |
| void player::on_mutation_gain( const trait_id &mid ) | |
| { | |
| morale->on_mutation_gain( mid ); | |
| magic.on_mutation_gain( mid, *this ); | |
| } | |
| void player::on_mutation_loss( const trait_id &mid ) | |
| { | |
| morale->on_mutation_loss( mid ); | |
| magic.on_mutation_loss( mid ); | |
| } | |
| void player::on_stat_change( const std::string &stat, int value ) | |
| { | |
| morale->on_stat_change( stat, value ); | |
| } | |
| void player::on_item_wear( const item &it ) | |
| { | |
| morale->on_item_wear( it ); | |
| } | |
| void player::on_item_takeoff( const item &it ) | |
| { | |
| morale->on_item_takeoff( it ); | |
| } | |
| void player::on_worn_item_washed( const item &it ) | |
| { | |
| if( is_worn( it ) ) { | |
| morale->on_worn_item_washed( it ); | |
| } | |
| } | |
| void player::on_effect_int_change( const efftype_id &eid, int intensity, body_part bp ) | |
| { | |
| // Adrenaline can reduce perceived pain (or increase it when you enter comedown). | |
| // See @ref get_perceived_pain() | |
| if( eid == effect_adrenaline ) { | |
| // Note that calling this does no harm if it wasn't changed. | |
| on_stat_change( "perceived_pain", get_perceived_pain() ); | |
| } | |
| morale->on_effect_int_change( eid, intensity, bp ); | |
| } | |
| const targeting_data &player::get_targeting_data() | |
| { | |
| if( tdata == nullptr ) { | |
| debugmsg( "Tried to get targeting data before setting it" ); | |
| tdata.reset( new targeting_data() ); | |
| tdata->relevant = nullptr; | |
| g->u.cancel_activity(); | |
| } | |
| return *tdata; | |
| } | |
| void player::set_targeting_data( const targeting_data &td ) | |
| { | |
| tdata.reset( new targeting_data( td ) ); | |
| } | |
| bool player::query_yn( const std::string &mes ) const | |
| { | |
| return ::query_yn( mes ); | |
| } | |
| const pathfinding_settings &player::get_pathfinding_settings() const | |
| { | |
| return *path_settings; | |
| } | |
| std::set<tripoint> player::get_path_avoid() const | |
| { | |
| std::set<tripoint> ret; | |
| for( npc &guy : g->all_npcs() ) { | |
| if( sees( guy ) ) { | |
| ret.insert( guy.pos() ); | |
| } | |
| } | |
| // TODO: Add known traps in a way that doesn't destroy performance | |
| return ret; | |
| } | |
| bool player::is_rad_immune() const | |
| { | |
| bool has_helmet = false; | |
| return ( is_wearing_power_armor( &has_helmet ) && has_helmet ) || worn_with_flag( "RAD_PROOF" ); | |
| } | |
| void player::do_skill_rust() | |
| { | |
| const int rate = rust_rate(); | |
| if( rate <= 0 ) { | |
| return; | |
| } | |
| for( auto &pair : *_skills ) { | |
| if( rate <= rng( 0, 1000 ) ) { | |
| continue; | |
| } | |
| const Skill &aSkill = *pair.first; | |
| SkillLevel &skill_level_obj = pair.second; | |
| if( aSkill.is_combat_skill() && | |
| ( ( has_trait( trait_PRED2 ) && one_in( 4 ) ) || | |
| ( has_trait( trait_PRED3 ) && one_in( 2 ) ) || | |
| ( has_trait( trait_PRED4 ) && x_in_y( 2, 3 ) ) ) ) { | |
| // Their brain is optimized to remember this | |
| if( one_in( 15600 ) ) { | |
| // They've already passed the roll to avoid rust at | |
| // this point, but print a message about it now and | |
| // then. | |
| // | |
| // 13 combat skills, 600 turns/hr, 7800 tests/hr. | |
| // This means PRED2/PRED3/PRED4 think of hunting on | |
| // average every 8/4/3 hours, enough for immersion | |
| // without becoming an annoyance. | |
| // | |
| add_msg_if_player( _( "Your heart races as you recall your most recent hunt." ) ); | |
| stim++; | |
| } | |
| continue; | |
| } | |
| const bool charged_bio_mem = power_level > 25 && has_active_bionic( bio_memory ); | |
| const int oldSkillLevel = skill_level_obj.level(); | |
| if( skill_level_obj.rust( charged_bio_mem ) ) { | |
| add_msg_if_player( m_warning, | |
| _( "Your knowledge of %s begins to fade, but your memory banks retain it!" ), aSkill.name() ); | |
| charge_power( -25 ); | |
| } | |
| const int newSkill = skill_level_obj.level(); | |
| if( newSkill < oldSkillLevel ) { | |
| add_msg_if_player( m_bad, _( "Your skill in %s has reduced to %d!" ), aSkill.name(), newSkill ); | |
| } | |
| } | |
| } | |
| std::pair<std::string, nc_color> player::get_hunger_description() const | |
| { | |
| const bool calorie_deficit = get_bmi() < character_weight_category::normal; | |
| const units::volume contains = stomach.contains(); | |
| const units::volume cap = stomach.capacity(); | |
| std::string hunger_string; | |
| nc_color hunger_color = c_white; | |
| // i ate just now! | |
| const bool just_ate = stomach.time_since_ate() < 15_minutes; | |
| // i ate a meal recently enough that i shouldn't need another meal | |
| const bool recently_ate = stomach.time_since_ate() < 3_hours; | |
| if( calorie_deficit ) { | |
| if( contains >= cap ) { | |
| hunger_string = _( "Engorged" ); | |
| hunger_color = c_green; | |
| } else if( contains > cap * 3 / 4 ) { | |
| hunger_string = _( "Sated" ); | |
| hunger_color = c_green; | |
| } else if( just_ate && contains > cap / 2 ) { | |
| hunger_string = _( "Full" ); | |
| hunger_color = c_green; | |
| } else if( just_ate ) { | |
| hunger_string = _( "Hungry" ); | |
| hunger_color = c_yellow; | |
| } else if( recently_ate ) { | |
| hunger_string = _( "Very Hungry" ); | |
| hunger_color = c_yellow; | |
| } else if( get_bmi() < character_weight_category::emaciated ) { | |
| hunger_string = _( "Starving!" ); | |
| hunger_color = c_red; | |
| } else if( get_bmi() < character_weight_category::underweight ) { | |
| hunger_string = _( "Near starving" ); | |
| hunger_color = c_red; | |
| } else { | |
| hunger_string = _( "Famished" ); | |
| hunger_color = c_light_red; | |
| } | |
| } else { | |
| if( contains >= cap * 5 / 6 ) { | |
| hunger_string = _( "Engorged" ); | |
| hunger_color = c_green; | |
| } else if( contains > cap * 11 / 20 ) { | |
| hunger_string = _( "Sated" ); | |
| hunger_color = c_green; | |
| } else if( recently_ate && contains >= cap * 3 / 8 ) { | |
| hunger_string = _( "Full" ); | |
| hunger_color = c_green; | |
| } else if( ( stomach.time_since_ate() > 90_minutes && contains < cap / 8 && recently_ate ) || | |
| ( just_ate && contains > 0_ml && contains < cap * 3 / 8 ) ) { | |
| hunger_string = _( "Peckish" ); | |
| hunger_color = c_dark_gray; | |
| } else if( !just_ate && ( recently_ate || contains > 0_ml ) ) { | |
| hunger_string.clear(); | |
| } else { | |
| if( get_bmi() > character_weight_category::overweight ) { | |
| hunger_string = _( "Hungry" ); | |
| } else { | |
| hunger_string = _( "Very Hungry" ); | |
| } | |
| hunger_color = c_yellow; | |
| } | |
| } | |
| return std::make_pair( hunger_string, hunger_color ); | |
| } | |
| void player::enforce_minimum_healing() | |
| { | |
| for( int i = 0; i < num_hp_parts; i++ ) { | |
| if( healed_total[i] <= 0 ) { | |
| heal( static_cast<hp_part>( i ), 1 ); | |
| } | |
| healed_total[i] = 0; | |
| } | |
| } |