diff --git a/src/character.cpp b/src/character.cpp index f21a30859f3e9..1c4e2c32df255 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -137,19 +137,16 @@ static const efftype_id effect_beartrap( "beartrap" ); static const efftype_id effect_bite( "bite" ); static const efftype_id effect_bleed( "bleed" ); static const efftype_id effect_blind( "blind" ); -static const efftype_id effect_blisters( "blisters" ); static const efftype_id effect_bloodworms( "bloodworms" ); static const efftype_id effect_boomered( "boomered" ); static const efftype_id effect_brainworms( "brainworms" ); static const efftype_id effect_blood_spiders( "blood_spiders" ); static const efftype_id effect_cig( "cig" ); -static const efftype_id effect_cold( "cold" ); static const efftype_id effect_common_cold( "common_cold" ); static const efftype_id effect_contacts( "contacts" ); static const efftype_id effect_controlled( "controlled" ); static const efftype_id effect_corroding( "corroding" ); static const efftype_id effect_cough_suppress( "cough_suppress" ); -static const efftype_id effect_crushed( "crushed" ); static const efftype_id effect_darkness( "darkness" ); static const efftype_id effect_deaf( "deaf" ); static const efftype_id effect_dermatik( "dermatik" ); @@ -161,8 +158,6 @@ static const efftype_id effect_drunk( "drunk" ); static const efftype_id effect_earphones( "earphones" ); static const efftype_id effect_flu( "flu" ); static const efftype_id effect_foodpoison( "foodpoison" ); -static const efftype_id effect_frostbite( "frostbite" ); -static const efftype_id effect_frostbite_recovery( "frostbite_recovery" ); static const efftype_id effect_fungus( "fungus" ); static const efftype_id effect_glowing( "glowing" ); static const efftype_id effect_glowy_led( "glowy_led" ); @@ -171,18 +166,6 @@ static const efftype_id effect_grabbing( "grabbing" ); static const efftype_id effect_harnessed( "harnessed" ); static const efftype_id effect_heating_bionic( "heating_bionic" ); static const efftype_id effect_heavysnare( "heavysnare" ); -static const efftype_id effect_hot( "hot" ); -static const efftype_id effect_hot_speed( "hot_speed" ); -static const efftype_id effect_hunger_blank( "hunger_blank" ); -static const efftype_id effect_hunger_engorged( "hunger_engorged" ); -static const efftype_id effect_hunger_famished( "hunger_famished" ); -static const efftype_id effect_hunger_full( "hunger_full" ); -static const efftype_id effect_hunger_hungry( "hunger_hungry" ); -static const efftype_id effect_hunger_near_starving( "hunger_near_starving" ); -static const efftype_id effect_hunger_satisfied( "hunger_satisfied" ); -static const efftype_id effect_hunger_starving( "hunger_starving" ); -static const efftype_id effect_hunger_very_hungry( "hunger_very_hungry" ); -static const efftype_id effect_hypovolemia( "hypovolemia" ); static const efftype_id effect_in_pit( "in_pit" ); static const efftype_id effect_incorporeal( "incorporeal" ); static const efftype_id effect_infected( "infected" ); @@ -213,9 +196,6 @@ static const efftype_id effect_slept_through_alarm( "slept_through_alarm" ); static const efftype_id effect_stunned( "stunned" ); static const efftype_id effect_tapeworm( "tapeworm" ); static const efftype_id effect_tied( "tied" ); -static const efftype_id effect_took_prozac( "took_prozac" ); -static const efftype_id effect_took_xanax( "took_xanax" ); -static const efftype_id effect_webbed( "webbed" ); static const efftype_id effect_weed_high( "weed_high" ); static const efftype_id effect_winded( "winded" ); @@ -224,14 +204,10 @@ static const field_type_str_id field_fd_clairvoyant( "fd_clairvoyant" ); static const itype_id fuel_type_animal( "animal" ); static const itype_id itype_apparatus( "apparatus" ); static const itype_id itype_battery( "battery" ); -static const itype_id itype_beartrap( "beartrap" ); static const itype_id itype_e_handcuffs( "e_handcuffs" ); static const itype_id itype_fire( "fire" ); static const itype_id fuel_type_muscle( "muscle" ); static const itype_id itype_rm13_armor_on( "rm13_armor_on" ); -static const itype_id itype_rope_6( "rope_6" ); -static const itype_id itype_snare_trigger( "snare_trigger" ); -static const itype_id itype_string_36( "string_36" ); static const itype_id itype_UPS( "UPS" ); static const skill_id skill_archery( "archery" ); @@ -257,9 +233,6 @@ static const trait_id trait_ANTLERS( "ANTLERS" ); static const trait_id trait_BADBACK( "BADBACK" ); static const trait_id trait_CENOBITE( "CENOBITE" ); static const trait_id trait_CF_HAIR( "CF_HAIR" ); -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_CLUMSY( "CLUMSY" ); static const trait_id trait_DEBUG_BIONIC_POWER( "DEBUG_BIONIC_POWER" ); static const trait_id trait_DEBUG_HS( "DEBUG_HS" ); @@ -267,14 +240,10 @@ static const trait_id trait_DEBUG_NODMG( "DEBUG_NODMG" ); static const trait_id trait_DEFT( "DEFT" ); static const trait_id trait_EATHEALTH( "EATHEALTH" ); static const trait_id trait_FAT( "FAT" ); -static const trait_id trait_FELINE_FUR( "FELINE_FUR" ); -static const trait_id trait_FUR( "FUR" ); static const trait_id trait_ILLITERATE( "ILLITERATE" ); 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_LIGHTFUR( "LIGHTFUR" ); -static const trait_id trait_LUPINE_FUR( "LUPINE_FUR" ); static const trait_id trait_PACIFIST( "PACIFIST" ); static const trait_id trait_PARAIMMUNE( "PARAIMMUNE" ); static const trait_id trait_PROF_SKATER( "PROF_SKATER" ); @@ -284,7 +253,6 @@ static const trait_id trait_SPINES( "SPINES" ); static const trait_id trait_SQUEAMISH( "SQUEAMISH" ); static const trait_id trait_SUNLIGHT_DEPENDENT( "SUNLIGHT_DEPENDENT" ); static const trait_id trait_THORNS( "THORNS" ); -static const trait_id trait_URSINE_FUR( "URSINE_FUR" ); static const trait_id trait_WOOLALLERGY( "WOOLALLERGY" ); static const bionic_id bio_ads( "bio_ads" ); @@ -300,7 +268,6 @@ static const bionic_id bio_ups( "bio_ups" ); static const bionic_id afs_bio_linguistic_coprocessor( "afs_bio_linguistic_coprocessor" ); static const trait_id trait_BADTEMPER( "BADTEMPER" ); -static const trait_id trait_BARK( "BARK" ); static const trait_id trait_BIRD_EYE( "BIRD_EYE" ); static const trait_id trait_CEPH_VISION( "CEPH_VISION" ); static const trait_id trait_CHEMIMBALANCE( "CHEMIMBALANCE" ); @@ -313,7 +280,6 @@ static const trait_id trait_MUTE( "MUTE" ); static const trait_id trait_DEBUG_CLOAK( "DEBUG_CLOAK" ); static const trait_id trait_DEBUG_LS( "DEBUG_LS" ); static const trait_id trait_DEBUG_NIGHTVISION( "DEBUG_NIGHTVISION" ); -static const trait_id trait_DEBUG_NOTEMP( "DEBUG_NOTEMP" ); static const trait_id trait_DISRESISTANT( "DISRESISTANT" ); static const trait_id trait_DOWN( "DOWN" ); static const trait_id trait_ELECTRORECEPTORS( "ELECTRORECEPTORS" ); @@ -325,7 +291,6 @@ static const trait_id trait_GILLS_CEPH( "GILLS_CEPH" ); static const trait_id trait_HEAVYSLEEPER( "HEAVYSLEEPER" ); static const trait_id trait_HEAVYSLEEPER2( "HEAVYSLEEPER2" ); static const trait_id trait_HIBERNATE( "HIBERNATE" ); -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" ); @@ -334,7 +299,6 @@ static const trait_id trait_LEG_TENT_BRACE( "LEG_TENT_BRACE" ); static const trait_id trait_LEG_TENTACLES( "LEG_TENTACLES" ); static const trait_id trait_LIGHT_BONES( "LIGHT_BONES" ); static const trait_id trait_LIGHTSTEP( "LIGHTSTEP" ); -static const trait_id trait_M_DEPENDENT( "M_DEPENDENT" ); static const trait_id trait_M_IMMUNE( "M_IMMUNE" ); static const trait_id trait_M_SKIN3( "M_SKIN3" ); static const trait_id trait_MORE_PAIN( "MORE_PAIN" ); @@ -358,8 +322,6 @@ static const trait_id trait_PER_SLIME_OK( "PER_SLIME_OK" ); static const trait_id trait_PROF_FOODP( "PROF_FOODP" ); static const trait_id trait_QUICK( "QUICK" ); static const trait_id trait_PROF_DICEMASTER( "PROF_DICEMASTER" ); -static const trait_id trait_PYROMANIA( "PYROMANIA" ); -static const trait_id trait_RADIOGENIC( "RADIOGENIC" ); static const trait_id trait_ROOTS2( "ROOTS2" ); static const trait_id trait_ROOTS3( "ROOTS3" ); static const trait_id trait_SEESLEEP( "SEESLEEP" ); @@ -406,7 +368,6 @@ static const json_character_flag json_flag_INFRARED( "INFRARED" ); static const json_character_flag json_flag_INVISIBLE( "INVISIBLE" ); static const json_character_flag json_flag_NIGHT_VISION( "NIGHT_VISION" ); static const json_character_flag json_flag_NO_DISEASE( "NO_DISEASE" ); -static const json_character_flag json_flag_NO_MINIMAL_HEALING( "NO_MINIMAL_HEALING" ); static const json_character_flag json_flag_NO_RADIATION( "NO_RADIATION" ); static const json_character_flag json_flag_NO_THIRST( "NO_THIRST" ); static const json_character_flag json_flag_NON_THRESH( "NON_THRESH" ); @@ -423,7 +384,6 @@ static const json_character_flag json_flag_WATCH( "WATCH" ); static const mtype_id mon_player_blob( "mon_player_blob" ); -static const vitamin_id vitamin_blood( "blood" ); static const morale_type morale_nightmare( "morale_nightmare" ); static const proficiency_id proficiency_prof_traps( "prof_traps" ); @@ -1640,320 +1600,6 @@ bool Character::can_run() const return get_stamina() > 0 && !has_effect( effect_winded ) && get_working_leg_count() >= 2; } -void Character::try_remove_downed() -{ - - /** @EFFECT_DEX increases chance to stand up when knocked down */ - - /** @EFFECT_STR increases chance to stand up when knocked down, slightly */ - if( rng( 0, 40 ) > get_dex() + get_str() / 2 ) { - add_msg_if_player( _( "You struggle to stand." ) ); - } else { - add_msg_player_or_npc( m_good, _( "You stand up." ), - _( " stands up." ) ); - remove_effect( effect_downed ); - } -} - -void Character::try_remove_bear_trap() -{ - /* Real bear traps can't be removed without the proper tools or immense strength; eventually this should - allow normal players two options: removal of the limb or removal of the trap from the ground - (at which point the player could later remove it from the leg with the right tools). - As such we are currently making it a bit easier for players and NPC's to get out of bear traps. - */ - /** @EFFECT_STR increases chance to escape bear trap */ - // If is riding, then despite the character having the effect, it is the mounted creature that escapes. - map &here = get_map(); - if( is_avatar() && is_mounted() ) { - auto *mon = mounted_creature.get(); - if( mon->type->melee_dice * mon->type->melee_sides >= 18 ) { - if( x_in_y( mon->type->melee_dice * mon->type->melee_sides, 200 ) ) { - mon->remove_effect( effect_beartrap ); - remove_effect( effect_beartrap ); - here.spawn_item( pos(), itype_beartrap ); - add_msg( _( "The %s escapes the bear trap!" ), mon->get_name() ); - } else { - add_msg_if_player( m_bad, - _( "Your %s tries to free itself from the bear trap, but can't get loose!" ), mon->get_name() ); - } - } - } else { - if( x_in_y( get_str(), 100 ) ) { - remove_effect( effect_beartrap ); - add_msg_player_or_npc( m_good, _( "You free yourself from the bear trap!" ), - _( " frees themselves from the bear trap!" ) ); - item beartrap( "beartrap", calendar::turn ); - here.add_item_or_charges( pos(), beartrap ); - } else { - add_msg_if_player( m_bad, - _( "You try to free yourself from the bear trap, but can't get loose!" ) ); - } - } -} - -void Character::try_remove_lightsnare() -{ - map &here = get_map(); - if( is_mounted() ) { - auto *mon = mounted_creature.get(); - if( x_in_y( mon->type->melee_dice * mon->type->melee_sides, 12 ) ) { - mon->remove_effect( effect_lightsnare ); - remove_effect( effect_lightsnare ); - here.spawn_item( pos(), itype_string_36 ); - here.spawn_item( pos(), itype_snare_trigger ); - add_msg( _( "The %s escapes the light snare!" ), mon->get_name() ); - } - } else { - /** @EFFECT_STR increases chance to escape light snare */ - - /** @EFFECT_DEX increases chance to escape light snare */ - if( x_in_y( get_str(), 12 ) || x_in_y( get_dex(), 8 ) ) { - remove_effect( effect_lightsnare ); - add_msg_player_or_npc( m_good, _( "You free yourself from the light snare!" ), - _( " frees themselves from the light snare!" ) ); - item string( "string_36", calendar::turn ); - item snare( "snare_trigger", calendar::turn ); - here.add_item_or_charges( pos(), string ); - here.add_item_or_charges( pos(), snare ); - } else { - add_msg_if_player( m_bad, - _( "You try to free yourself from the light snare, but can't get loose!" ) ); - } - } -} - -void Character::try_remove_heavysnare() -{ - map &here = get_map(); - if( is_mounted() ) { - auto *mon = mounted_creature.get(); - if( mon->type->melee_dice * mon->type->melee_sides >= 7 ) { - if( x_in_y( mon->type->melee_dice * mon->type->melee_sides, 32 ) ) { - mon->remove_effect( effect_heavysnare ); - remove_effect( effect_heavysnare ); - here.spawn_item( pos(), itype_rope_6 ); - here.spawn_item( pos(), itype_snare_trigger ); - add_msg( _( "The %s escapes the heavy snare!" ), mon->get_name() ); - } - } - } else { - /** @EFFECT_STR increases chance to escape heavy snare, slightly */ - - /** @EFFECT_DEX increases chance to escape light snare */ - if( x_in_y( get_str(), 32 ) || x_in_y( get_dex(), 16 ) ) { - remove_effect( effect_heavysnare ); - add_msg_player_or_npc( m_good, _( "You free yourself from the heavy snare!" ), - _( " frees themselves from the heavy snare!" ) ); - item rope( "rope_6", calendar::turn ); - item snare( "snare_trigger", calendar::turn ); - here.add_item_or_charges( pos(), rope ); - here.add_item_or_charges( pos(), snare ); - } else { - add_msg_if_player( m_bad, - _( "You try to free yourself from the heavy snare, but can't get loose!" ) ); - } - } -} - -void Character::try_remove_crushed() -{ - /** @EFFECT_STR increases chance to escape crushing rubble */ - - /** @EFFECT_DEX increases chance to escape crushing rubble, slightly */ - if( x_in_y( get_str() + get_dex() / 4.0, 100 ) ) { - remove_effect( effect_crushed ); - add_msg_player_or_npc( m_good, _( "You free yourself from the rubble!" ), - _( " frees themselves from the rubble!" ) ); - } else { - add_msg_if_player( m_bad, _( "You try to free yourself from the rubble, but can't get loose!" ) ); - } -} - -bool Character::try_remove_grab() -{ - int zed_number = 0; - if( is_mounted() ) { - auto *mon = mounted_creature.get(); - if( mon->has_effect( effect_grabbed ) ) { - if( ( dice( mon->type->melee_dice + mon->type->melee_sides, - 3 ) < get_effect_int( effect_grabbed ) ) || - !one_in( 4 ) ) { - add_msg( m_bad, _( "Your %s tries to break free, but fails!" ), mon->get_name() ); - return false; - } else { - add_msg( m_good, _( "Your %s breaks free from the grab!" ), mon->get_name() ); - remove_effect( effect_grabbed ); - mon->remove_effect( effect_grabbed ); - } - } else { - if( one_in( 4 ) ) { - add_msg( m_bad, _( "You are pulled from your %s!" ), mon->get_name() ); - remove_effect( effect_grabbed ); - forced_dismount(); - } - } - } else { - map &here = get_map(); - creature_tracker &creatures = get_creature_tracker(); - for( auto&& dest : here.points_in_radius( pos(), 1, 0 ) ) { // *NOPAD* - const monster *const mon = creatures.creature_at( dest ); - if( mon && mon->has_effect( effect_grabbing ) ) { - zed_number += mon->get_grab_strength(); - } - } - if( zed_number == 0 ) { - add_msg_player_or_npc( m_good, _( "You find yourself no longer grabbed." ), - _( " finds themselves no longer grabbed." ) ); - remove_effect( effect_grabbed ); - - /** @EFFECT_STR increases chance to escape grab */ - } else if( rng( 0, get_str() ) < rng( get_effect_int( effect_grabbed, body_part_torso ), - 8 ) ) { - add_msg_player_or_npc( m_bad, _( "You try break out of the grab, but fail!" ), - _( " tries to break out of the grab, but fails!" ) ); - return false; - } else { - add_msg_player_or_npc( m_good, _( "You break out of the grab!" ), - _( " breaks out of the grab!" ) ); - remove_effect( effect_grabbed ); - for( auto&& dest : here.points_in_radius( pos(), 1, 0 ) ) { // *NOPAD* - monster *mon = creatures.creature_at( dest ); - if( mon && mon->has_effect( effect_grabbing ) ) { - mon->remove_effect( effect_grabbing ); - } - } - } - } - return true; -} - -void Character::try_remove_webs() -{ - if( is_mounted() ) { - auto *mon = mounted_creature.get(); - if( x_in_y( mon->type->melee_dice * mon->type->melee_sides, - 6 * get_effect_int( effect_webbed ) ) ) { - add_msg( _( "The %s breaks free of the webs!" ), mon->get_name() ); - mon->remove_effect( effect_webbed ); - remove_effect( effect_webbed ); - } - /** @EFFECT_STR increases chance to escape webs */ - } else if( x_in_y( get_str(), 6 * get_effect_int( effect_webbed ) ) ) { - add_msg_player_or_npc( m_good, _( "You free yourself from the webs!" ), - _( " frees themselves from the webs!" ) ); - remove_effect( effect_webbed ); - } else { - add_msg_if_player( _( "You try to free yourself from the webs, but can't get loose!" ) ); - } -} - -void Character::try_remove_impeding_effect() -{ - for( const effect &eff : get_effects_with_flag( flag_EFFECT_IMPEDING ) ) { - const efftype_id &eff_id = eff.get_id(); - if( is_mounted() ) { - auto *mon = mounted_creature.get(); - if( x_in_y( mon->type->melee_dice * mon->type->melee_sides, - 6 * get_effect_int( eff_id ) ) ) { - add_msg( _( "The %s breaks free!" ), mon->get_name() ); - mon->remove_effect( eff_id ); - remove_effect( eff_id ); - } - /** @EFFECT_STR increases chance to escape webs */ - } else if( x_in_y( get_str(), 6 * get_effect_int( eff_id ) ) ) { - add_msg_player_or_npc( m_good, _( "You free yourself!" ), - _( " frees themselves!" ) ); - remove_effect( eff_id ); - } else { - add_msg_if_player( _( "You try to free yourself, but can't!" ) ); - } - } -} - -bool Character::move_effects( bool attacking ) -{ - if( has_effect( effect_downed ) ) { - try_remove_downed(); - return false; - } - if( has_effect( effect_webbed ) ) { - try_remove_webs(); - return false; - } - if( has_effect( effect_lightsnare ) ) { - try_remove_lightsnare(); - return false; - - } - if( has_effect( effect_heavysnare ) ) { - try_remove_heavysnare(); - return false; - } - if( has_effect( effect_beartrap ) ) { - try_remove_bear_trap(); - return false; - } - if( has_effect( effect_crushed ) ) { - try_remove_crushed(); - return false; - } - if( has_effect_with_flag( flag_EFFECT_IMPEDING ) ) { - try_remove_impeding_effect(); - return false; - } - - // Below this point are things that allow for movement if they succeed - - // Currently we only have one thing that forces movement if you succeed, should we get more - // than this will need to be reworked to only have success effects if /all/ checks succeed - if( has_effect( effect_in_pit ) ) { - /** @EFFECT_STR increases chance to escape pit */ - - /** @EFFECT_DEX increases chance to escape pit, slightly */ - if( rng( 0, 40 ) > get_str() + get_dex() / 2 ) { - add_msg_if_player( m_bad, _( "You try to escape the pit, but slip back in." ) ); - return false; - } else { - add_msg_player_or_npc( m_good, _( "You escape the pit!" ), - _( " escapes the pit!" ) ); - remove_effect( effect_in_pit ); - } - } - return !has_effect( effect_grabbed ) || attacking || try_remove_grab(); -} - -void Character::wait_effects( bool attacking ) -{ - if( has_effect( effect_downed ) ) { - try_remove_downed(); - return; - } - if( has_effect( effect_beartrap ) ) { - try_remove_bear_trap(); - return; - } - if( has_effect( effect_lightsnare ) ) { - try_remove_lightsnare(); - return; - } - if( has_effect( effect_heavysnare ) ) { - try_remove_heavysnare(); - return; - } - if( has_effect( effect_webbed ) ) { - try_remove_webs(); - return; - } - if( has_effect_with_flag( flag_EFFECT_IMPEDING ) ) { - try_remove_impeding_effect(); - return; - } - if( has_effect( effect_grabbed ) && !attacking && !try_remove_grab() ) { - return; - } -} - move_mode_id Character::current_movement_mode() const { return move_mode; @@ -2215,77 +1861,6 @@ int Character::get_part_hp_max( const bodypart_id &id ) const Creature::get_part_hp_max( id ) ); } -void Character::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 time_duration average_drying = 2_hours; - - // 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, to_turns( average_drying * delay / 100.0 ) ) ) { - // 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, 80 ); - - for( const bodypart_id &bp : get_all_body_parts() ) { - if( get_part_wetness( bp ) == 0 ) { - continue; - } - // This is to normalize drying times - int drying_chance = get_part_drench_capacity( bp ); - const int temp_conv = get_part_temp_conv( bp ); - // Body temperature affects duration of wetness - // Note: Using temp_conv rather than temp_cur, to better approximate environment - if( temp_conv >= BODYTEMP_SCORCHING ) { - drying_chance *= 2; - } else if( temp_conv >= BODYTEMP_VERY_HOT ) { - drying_chance = drying_chance * 3 / 2; - } else if( temp_conv >= BODYTEMP_HOT ) { - drying_chance = drying_chance * 4 / 3; - } else if( temp_conv > 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 ) { - mod_part_wetness( bp, -1 ); - if( get_part_wetness( bp ) < 0 ) { - set_part_wetness( bp, 0 ); - } - } - // Safety measure to keep wetness within bounds - if( get_part_wetness( bp ) > get_part_drench_capacity( bp ) ) { - set_part_wetness( bp, get_part_drench_capacity( bp ) ); - } - } - // TODO: Make clothing slow down drying -} - // This must be called when any of the following change: // - effects // - bionics @@ -5168,91 +4743,6 @@ void Character::update_health( int external_modifiers ) get_healthy_mod() ); } -// 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 -static inline int ticks_between( const time_point &from, const time_point &to, - const time_duration &tick_length ) -{ - return ( to_turn( to ) / to_turns( tick_length ) ) - ( to_turn - ( from ) / to_turns( tick_length ) ); -} - -void Character::update_body() -{ - update_body( calendar::turn - 1_turns, calendar::turn ); - last_updated = calendar::turn; -} - -void Character::update_body( const time_point &from, const time_point &to ) -{ - // Early return if we already did update previously on the same turn (e.g. when loading savegame). - if( to <= last_updated ) { - return; - } - if( !is_npc() ) { - update_stamina( to_turns( to - from ) ); - } - update_stomach( from, to ); - recalculate_enchantment_cache(); - if( ticks_between( from, to, 3_minutes ) > 0 ) { - magic->update_mana( *this, to_turns( 3_minutes ) ); - } - const int five_mins = ticks_between( from, to, 5_minutes ); - if( five_mins > 0 ) { - activity_history.try_reduce_weariness( base_bmr(), in_sleep_state() ); - 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 - // TODO: change @ref med to take time_duration - mend( five_mins * to_turns( 5_minutes ) ); - activity_history.reset_activity_level(); - } - - activity_history.new_turn(); - if( ticks_between( from, to, 24_hours ) > 0 && !has_flag( json_flag_NO_MINIMAL_HEALING ) ) { - enforce_minimum_healing(); - } - - const int thirty_mins = ticks_between( from, to, 30_minutes ); - if( thirty_mins > 0 ) { - // Radiation kills health even at low doses - update_health( has_trait( trait_RADIOGENIC ) ? 0 : -get_rad() ); - get_sick(); - } - - for( const auto &v : vitamin::all() ) { - const time_duration rate = vitamin_rate( v.first ); - - // No blood volume regeneration if body lacks fluids - if( v.first == vitamin_blood && has_effect( effect_hypovolemia ) && get_thirst() > 240 ) { - continue; - } - - 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 ); - } - } - } - - if( is_avatar() && ticks_between( from, to, 24_hours ) > 0 ) { - as_avatar()->advance_daily_calories(); - } - - if( calendar::once_every( 24_hours ) ) { - do_skill_rust(); - } -} - item *Character::best_quality_item( const quality_id &qual ) { std::vector qual_inv = items_with( [qual]( const item & itm ) { @@ -5367,165 +4857,6 @@ float Character::activity_level() const return std::min( max, attempted_level ); } -void Character::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( "NO_NPC_FOOD" ); - const bool foodless = debug_ls || npc_no_food; - const bool no_thirst = has_flag( json_flag_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 ); - const int half_hours = ticks_between( from, to, 30_minutes ); - const units::volume stomach_capacity = stomach.capacity( *this ); - - if( five_mins > 0 ) { - // Digest nutrients in stomach, they are destined for the guts (except water) - food_summary digested_to_guts = stomach.digest( *this, rates, five_mins, half_hours ); - // Digest nutrients in guts, they will be distributed to needs levels - food_summary digested_to_body = guts.digest( *this, rates, five_mins, half_hours ); - // Water from stomach skips guts and gets absorbed by body - mod_thirst( -units::to_milliliter( digested_to_guts.water ) / 5 ); - guts.ingest( digested_to_guts ); - - mod_stored_kcal( digested_to_body.nutr.kcal() ); - vitamins_mod( effect_vitamin_mod( digested_to_body.nutr.vitamins ), false ); - log_activity_level( activity_history.average_activity() ); - - 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 - // Explicitly floor it here, the int cast will do so anyways - mod_stored_calories( -std::floor( five_mins * kcal_per_time * 1000 ) ); - } - } - // if npc_no_food no need to calc hunger, and set hunger_effect - if( npc_no_food ) { - return; - } - 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 ) { - // full - 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 && 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 && - get_stored_kcal() >= get_healthy_kcal() ) || get_stored_kcal() < get_healthy_kcal() ) ) { - set_hunger( 100 ); - } else if( get_hunger() < 0 ) { - set_hunger( 0 ); - } - } - } 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 ) { - // full - 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 || no_thirst ) { - set_thirst( 0 ); - } - - const bool calorie_deficit = get_bmi() < character_weight_category::normal; - const units::volume contains = stomach.contains(); - const units::volume cap = stomach.capacity( *this ); - - efftype_id hunger_effect; - // 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; - // Hunger effect should intensify whenever stomach contents decreases, last eaten time increases, or calorie deficit intensifies. - if( calorie_deficit ) { - // just_ate recently_ate - // <15 min <3 hrs >=3 hrs - // >= cap engorged engorged engorged - // > 3/4 cap full full full - // > 1/2 cap satisfied v. hungry famished/(near)starving - // <= 1/2 cap hungry v. hungry famished/(near)starving - if( contains >= cap ) { - hunger_effect = effect_hunger_engorged; - } else if( contains > cap * 3 / 4 ) { - hunger_effect = effect_hunger_full; - } else if( just_ate && contains > cap / 2 ) { - hunger_effect = effect_hunger_satisfied; - } else if( just_ate ) { - hunger_effect = effect_hunger_hungry; - } else if( recently_ate ) { - hunger_effect = effect_hunger_very_hungry; - } else if( get_bmi() < character_weight_category::underweight ) { - hunger_effect = effect_hunger_near_starving; - } else if( get_bmi() < character_weight_category::emaciated ) { - hunger_effect = effect_hunger_starving; - } else { - hunger_effect = effect_hunger_famished; - } - } else { - // just_ate recently_ate - // <15 min <3 hrs >=3 hrs - // >= 5/6 cap engorged engorged engorged - // > 11/20 cap full full full - // >= 3/8 cap satisfied satisfied blank - // > 0 blank blank blank - // 0 blank blank (v.) hungry - if( contains >= cap * 5 / 6 ) { - hunger_effect = effect_hunger_engorged; - } else if( contains > cap * 11 / 20 ) { - hunger_effect = effect_hunger_full; - } else if( recently_ate && contains >= cap * 3 / 8 ) { - hunger_effect = effect_hunger_satisfied; - } else if( recently_ate || contains > 0_ml ) { - hunger_effect = effect_hunger_blank; - } else if( get_bmi() > character_weight_category::overweight ) { - hunger_effect = effect_hunger_hungry; - } else { - hunger_effect = effect_hunger_very_hungry; - } - } - if( !has_effect( hunger_effect ) ) { - remove_effect( effect_hunger_engorged ); - remove_effect( effect_hunger_full ); - remove_effect( effect_hunger_satisfied ); - remove_effect( effect_hunger_hungry ); - remove_effect( effect_hunger_very_hungry ); - remove_effect( effect_hunger_near_starving ); - remove_effect( effect_hunger_starving ); - remove_effect( effect_hunger_famished ); - remove_effect( effect_hunger_blank ); - add_effect( hunger_effect, 24_hours, true ); - } -} - void Character::update_needs( int rate_multiplier ) { const int current_stim = get_stim(); @@ -6016,414 +5347,6 @@ bool Character::is_hibernating() const get_thirst() <= 80 && has_active_mutation( trait_HIBERNATE ); } -/* 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 Character::update_bodytemp() -{ - if( has_trait( trait_DEBUG_NOTEMP ) ) { - set_all_parts_temp_conv( BODYTEMP_NORM ); - set_all_parts_temp_cur( BODYTEMP_NORM ); - return; - } - weather_manager &weather_man = get_weather(); - /* Cache calls to g->get_temperature( player position ), used in several places in function */ - const int player_local_temp = weather_man.get_temperature( pos() ); - // NOTE : visit weather.h for some details on the numbers used - // Converts temperature to Celsius/10 - int Ctemperature = static_cast( 100 * temp_to_celsius( player_local_temp ) ); - const w_point weather = *weather_man.weather_precise; - int vehwindspeed = 0; - map &here = get_map(); - const optional_vpart_position vp = here.veh_at( pos() ); - if( vp ) { - vehwindspeed = std::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( weather_man.windspeed + vehwindspeed, cur_om_ter, - pos(), weather_man.winddirection, sheltered ); - // Let's cache this not to check it for every bodyparts - 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 heat_immune = has_flag( json_flag_HEATPROOF ); - const bool has_heatsink = has_flag( json_flag_HEATSINK ) || is_wearing( itype_rm13_armor_on ) || - heat_immune; - 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 = here.furn( pos() ); - const cata::optional boardable = vp.part_with_feature( "BOARDABLE", true ); - // 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( 2000 * std::min( met_rate, 1.0f ) - 2000 ); - // Give SOME bonus to those living furnaces with extreme metabolism - const int metabolism_warmth = static_cast( std::max( 0.0f, met_rate - 1.0f ) * 1000 ); - // Fatigue - // ~-900 when exhausted - const int fatigue_warmth = has_sleep ? 0 : static_cast( clamp( -1.5f * get_fatigue(), -1350.0f, - 0.0f ) ); - - // Sunlight - const int sunlight_warmth = g->is_in_sunlight( pos() ) ? - ( get_weather().weather_id->sun_intensity == - sun_intensity_type::high ? - 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( get_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 ); - - std::map> clothing_map; - for( const bodypart_id &bp : get_all_body_parts() ) { - clothing_map.emplace( bp, std::vector() ); - } - for( const item &it : worn ) { - for( const bodypart_str_id &covered : it.get_covered_body_parts() ) { - clothing_map[covered.id()].emplace_back( &it ); - } - } - - std::map warmth_per_bp = warmth( clothing_map ); - std::map bonus_warmth_per_bp = bonus_item_warmth(); - std::map wind_res_per_bp = get_wind_resistance( clothing_map ); - // We might not use this at all, so leave it empty - // If we do need to use it, we'll initialize it (once) there - std::map fire_armor_per_bp; - // Current temperature and converging temperature calculations - for( const bodypart_id &bp : get_all_body_parts() ) { - - if( bp->has_flag( "IGNORE_TEMP" ) ) { - 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, - get_part_temp_cur( bp ) ); - // Produces a smooth curve between 30.0 and 60.0. - double homeostasis_adjustment = 30.0 * ( 1.0 + scaled_temperature ); - int clothing_warmth_adjustment = static_cast( homeostasis_adjustment * warmth_per_bp[bp] ); - int clothing_warmth_adjusted_bonus = static_cast( homeostasis_adjustment * - bonus_warmth_per_bp[bp] ); - // WINDCHILL - - bp_windpower = static_cast( static_cast( bp_windpower ) * - ( 1 - wind_res_per_bp[bp] / 100.0 ) ); - // Calculate windchill - int windchill = get_local_windchill( player_local_temp, - get_local_humidity( weather.humidity, get_weather().weather_id, sheltered ), - bp_windpower ); - - static const auto is_lower = []( const bodypart_id & bp ) { - return bp == body_part_foot_l || - bp == body_part_foot_r || - bp == body_part_leg_l || - bp == body_part_leg_r ; - }; - - // If you're standing in water, air temperature is replaced by water temperature. No wind. - // Convert to 0.01C - if( here.has_flag_ter( ter_furn_flag::TFLAG_DEEP_WATER, pos() ) || - ( here.has_flag_ter( ter_furn_flag::TFLAG_SHALLOW_WATER, pos() ) && is_lower( bp ) ) ) { - 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. - set_part_temp_conv( bp, BODYTEMP_NORM + adjusted_temp + windchill * 100 + - clothing_warmth_adjustment ); - // HUNGER / STARVATION - mod_part_temp_conv( bp, hunger_warmth ); - // FATIGUE - mod_part_temp_conv( bp, fatigue_warmth ); - // Mutations - mod_part_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( get_part_frostbite_timer( bp ) > 0 ) { - mod_part_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( std::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( has_heatsink ) { - blister_count -= 20; - } - if( fire_armor_per_bp.empty() && blister_count > 0 ) { - fire_armor_per_bp = get_armor_fire( clothing_map ); - } - if( blister_count - fire_armor_per_bp[bp] > 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 ); - } - - mod_part_temp_conv( bp, sunlight_warmth ); - // DISEASES - if( bp == body_part_head && has_effect( effect_flu ) ) { - mod_part_temp_conv( bp, 1500 ); - } - if( has_common_cold ) { - mod_part_temp_conv( bp, -750 ); - } - // Loss of blood results in loss of body heat, 1% bodyheat lost per 2% hp lost - mod_part_temp_conv( bp, - blood_loss( bp ) * get_part_temp_conv( bp ) / 200 ); - - temp_equalizer( bp, bp->connected_to ); - temp_equalizer( bp, bp->main_part ); - - // Climate Control eases the effects of high and low ambient temps - if( has_climate_control ) { - set_part_temp_conv( bp, temp_corrected_by_climate_control( get_part_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 - if( bp == body_part_foot_l || bp == body_part_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 if( boardable ) { - bonus_fire_warmth = best_fire * boardable->info().bonus_fire_warmth_feet; - } else { - // Has to stand - bonus_fire_warmth = best_fire * bp->fire_warmth_bonus; - } - } else { - bonus_fire_warmth = best_fire * bp->fire_warmth_bonus; - } - - } - - 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 * get_part_temp_conv( 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 < get_part_temp_conv( bp ) ) { - // Too hot, can't help here - } else if( desired < get_part_temp_conv( bp ) + bonus_warmth ) { - // Use some heat, but not all of it - set_part_temp_conv( bp, desired ); - } else { - // Use all the heat - mod_part_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::once_every( 1_minutes ) && get_effect_int( effect_cold ) == 0 && - get_effect_int( effect_hot ) == 0 && - get_part_temp_conv( bp ) > BODYTEMP_COLD && get_part_temp_conv( bp ) <= BODYTEMP_NORM ) { - add_morale( MORALE_COMFY, 1, 10, 2_minutes, 1_minutes, true ); - } - } - - const int temp_before = get_part_temp_cur( bp ); - const int cur_temp_conv = get_part_temp_conv( bp ); - int temp_difference = temp_before - cur_temp_conv; // 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_before != cur_temp_conv ) { - set_part_temp_cur( bp, static_cast( temp_difference * std::exp( -0.002 ) + cur_temp_conv + - 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 && - ( ( cur_temp_conv + clothing_warmth_adjusted_bonus ) < BODYTEMP_HOT || - get_part_temp_cur( bp ) < BODYTEMP_COLD ) ) { - mod_part_temp_conv( bp, clothing_warmth_adjusted_bonus ); - rounding_error = 0; - if( temp_difference < 0 && temp_difference > -600 ) { - rounding_error = 1; - } - const int new_temp_conv = get_part_temp_conv( bp ); - if( temp_before != new_temp_conv ) { - temp_difference = get_part_temp_cur( bp ) - new_temp_conv; - set_part_temp_cur( bp, static_cast( temp_difference * std::exp( -0.002 ) + new_temp_conv + - rounding_error ) ); - } - } - const int temp_after = get_part_temp_cur( bp ); - // PENALTIES - if( temp_after < BODYTEMP_FREEZING ) { - add_effect( effect_cold, 1_turns, bp, true, 3 ); - } else if( temp_after < BODYTEMP_VERY_COLD ) { - add_effect( effect_cold, 1_turns, bp, true, 2 ); - } else if( temp_after < BODYTEMP_COLD ) { - add_effect( effect_cold, 1_turns, bp, true, 1 ); - } else if( temp_after > BODYTEMP_SCORCHING && !heat_immune ) { - add_effect( effect_hot, 1_turns, bp, true, 3 ); - if( bp->main_part == bp.id() ) { - add_effect( effect_hot_speed, 1_turns, bp, true, 3 ); - } - } else if( temp_after > BODYTEMP_VERY_HOT && !heat_immune ) { - add_effect( effect_hot, 1_turns, bp, true, 2 ); - if( bp->main_part == bp.id() ) { - add_effect( effect_hot_speed, 1_turns, bp, true, 2 ); - } - } else if( temp_after > BODYTEMP_HOT && !heat_immune ) { - add_effect( effect_hot, 1_turns, bp, true, 1 ); - if( bp->main_part == bp.id() ) { - add_effect( effect_hot_speed, 1_turns, bp, true, 1 ); - } - } else { - remove_effect( effect_cold, bp ); - remove_effect( effect_hot, bp ); - remove_effect( effect_hot_speed, bp ); - } - - update_frostbite( bp, bp_windpower, warmth_per_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 ) ); - } - - // Note: Numbers are based off of BODYTEMP at the top of weather.h - // If torso is BODYTEMP_COLD which is 34C, the early stages of hypothermia begin - // constant shivering will prevent the player from falling asleep. - // Otherwise, if any other body part is BODYTEMP_VERY_COLD, or 31C - // AND you have frostbite, then that also prevents you from sleeping - if( in_sleep_state() && !has_effect( effect_narcosis ) ) { - if( bp == body_part_torso && temp_after <= BODYTEMP_COLD ) { - add_msg( m_warning, _( "Your shivering prevents you from sleeping." ) ); - wake_up(); - } else if( bp != body_part_torso && temp_after <= BODYTEMP_VERY_COLD && - has_effect( effect_frostbite ) ) { - add_msg( m_warning, _( "You are too cold. Your frostbite prevents you from sleeping." ) ); - wake_up(); - } - } - - const int conv_temp = get_part_temp_conv( 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( conv_temp <= 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( conv_temp <= 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( conv_temp <= 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 ) ); - } - } -} - void Character::toolmod_add( item_location tool, item_location mod ) { if( !tool && !mod ) { @@ -6447,118 +5370,6 @@ void Character::toolmod_add( item_location tool, item_location mod ) activity.targets.emplace_back( std::move( mod ) ); } -void Character::update_frostbite( const bodypart_id &bp, const int FBwindPower, - const std::map &warmth_per_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 - **/ - - const int player_local_temp = get_weather().get_temperature( pos() ); - const int temp_after = get_part_temp_cur( bp ); - - if( bp == body_part_mouth || bp == body_part_foot_r || - bp == body_part_foot_l || bp == body_part_hand_r || bp == body_part_hand_l ) { - // Handle the frostbite timer - // Need temps in F, windPower already in mph - int wetness_percentage = 100 * get_part_wetness_percentage( bp ); // 0 - 100 - // Warmth gives a slight buff to temperature resistance - // Wetness gives a heavy nerf to temperature resistance - double adjusted_warmth = warmth_per_bp.at( bp ) - wetness_percentage; - int Ftemperature = static_cast( player_local_temp + 0.2 * adjusted_warmth ); - - int intense = get_effect_int( effect_frostbite, bp ); - - // This has been broken down into 8 zones - // Low risk zones (stops at frostnip) - if( temp_after < BODYTEMP_COLD && ( ( Ftemperature < 30 && Ftemperature >= 10 ) || - ( Ftemperature < 10 && Ftemperature >= -5 && FBwindPower < 20 && - -4 * Ftemperature + 3 * FBwindPower - 20 >= 0 ) ) ) { - if( get_part_frostbite_timer( bp ) < 2000 ) { - mod_part_frostbite_timer( bp, 3 ); - } - if( one_in( 100 ) && !has_effect( effect_frostbite, bp.id() ) ) { - add_msg( m_warning, _( "Your %s will be frostnipped in the next few hours." ), - body_part_name( bp ) ); - } - // Medium risk zones - } else if( temp_after < 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 ) ) ) { - mod_part_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_after < BODYTEMP_COLD && - ( ( Ftemperature < -5 && FBwindPower >= 10 && - -4 * Ftemperature + 3 * FBwindPower - 170 < 0 ) || - ( Ftemperature < -35 && FBwindPower >= 10 ) ) ) { - mod_part_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 { - mod_part_frostbite_timer( bp, -3 ); - } - - int frostbite_timer = get_part_frostbite_timer( bp ); - // Handle the bestowing of frostbite - if( frostbite_timer < 0 ) { - set_part_frostbite_timer( bp, 0 ); - } else if( frostbite_timer > 4200 ) { - // This ensures that the player will recover in at most 3 hours. - set_part_frostbite_timer( bp, 4200 ); - } - frostbite_timer = get_part_frostbite_timer( bp ); - // Frostbite, no recovery possible - if( frostbite_timer >= 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 >= 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 == 0 ) { - remove_effect( effect_frostbite, bp ); - remove_effect( effect_frostbite_recovery, bp ); - } - } -} - void Character::temp_equalizer( const bodypart_id &bp1, const bodypart_id &bp2 ) { // Body heat is moved around. @@ -6768,220 +5579,6 @@ float Character::get_hit_base() const return get_dex() / 4.0f; } -bodypart_id Character::body_window( const std::string &menu_header, - bool show_all, bool precise, - int normal_bonus, int /* head_bonus */, int /* torso_bonus */, - int bleed, float bite, float infect, float bandage_power, float disinfectant_power ) const -{ - /* This struct establishes some kind of connection between the hp_part (which can be healed and - * have HP) and the body_part. Note that there are more body_parts than hp_parts. For example: - * Damage to bp_head, bp_eyes and bp_mouth is all applied on the HP of hp_head. */ - struct healable_bp { - mutable bool allowed; - bodypart_id bp; - std::string name; // Translated name as it appears in the menu. - int bonus; - }; - - std::vector parts; - for( const bodypart_id &part : this->get_all_body_parts( get_body_part_flags::only_main ) ) { - // TODO: figure out how to do head and torso bonus? - parts.push_back( { false, part, part->name.translated(), normal_bonus } ); - } - - int max_bp_name_len = 0; - for( const healable_bp &e : parts ) { - max_bp_name_len = std::max( max_bp_name_len, utf8_width( e.name ) ); - } - - uilist bmenu; - bmenu.desc_enabled = true; - bmenu.text = menu_header; - bmenu.textwidth = 60; - - bmenu.hilight_disabled = true; - bool is_valid_choice = false; - - // If this is an NPC, the player is the one examining them and so the fact - // that they can't self-diagnose effectively doesn't matter - bool no_feeling = is_avatar() && has_trait( trait_NOPAIN ); - - for( size_t i = 0; i < parts.size(); i++ ) { - const healable_bp &e = parts[i]; - const bodypart_id &bp = e.bp; - const int maximal_hp = get_part_hp_max( bp ); - const int current_hp = get_part_hp_cur( bp ); - // This will c_light_gray if the part does not have any effects cured by the item/effect - // (e.g. it cures only bites, but the part does not have a bite effect) - const nc_color state_col = display::limb_color( *this, bp, bleed > 0, bite > 0.0f, infect > 0.0f ); - const bool has_curable_effect = state_col != c_light_gray; - // The same as in the main UI sidebar. Independent of the capability of the healing item/effect! - const nc_color all_state_col = display::limb_color( *this, bp, true, true, true ); - // Broken means no HP can be restored, it requires surgical attention. - const bool limb_is_broken = is_limb_broken( bp ); - const bool limb_is_mending = worn_with_flag( flag_SPLINT, bp ); - - if( show_all || has_curable_effect ) { // NOLINT(bugprone-branch-clone) - e.allowed = true; - } else if( limb_is_broken ) { // NOLINT(bugprone-branch-clone) - e.allowed = false; - } else if( current_hp < maximal_hp && ( e.bonus != 0 || bandage_power > 0.0f || - disinfectant_power > 0.0f ) ) { - e.allowed = true; - } else { - e.allowed = false; - } - - std::string msg; - std::string desc; - bool bleeding = has_effect( effect_bleed, bp.id() ); - bool bitten = has_effect( effect_bite, bp.id() ); - bool infected = has_effect( effect_infected, bp.id() ); - bool bandaged = has_effect( effect_bandaged, bp.id() ); - const int b_power = get_effect_int( effect_bandaged, bp ); - const int d_power = get_effect_int( effect_disinfected, bp ); - int new_b_power = static_cast( std::floor( bandage_power ) ); - if( bandaged ) { - const effect &eff = get_effect( effect_bandaged, bp ); - if( new_b_power > eff.get_max_intensity() ) { - new_b_power = eff.get_max_intensity(); - } - - } - int new_d_power = static_cast( std::floor( disinfectant_power ) ); - - const auto &aligned_name = std::string( max_bp_name_len - utf8_width( e.name ), ' ' ) + e.name; - std::string hp_str; - if( limb_is_mending ) { - desc += colorize( _( "It is broken, but has been set, and just needs time to heal." ), - c_blue ) + "\n"; - if( no_feeling ) { - hp_str = colorize( "==%==", c_blue ); - } else { - const auto &eff = get_effect( effect_mending, bp ); - const int mend_perc = eff.is_null() ? 0.0 : 100 * eff.get_duration() / eff.get_max_duration(); - - const int num = mend_perc / 20; - hp_str = colorize( std::string( num, '#' ) + std::string( 5 - num, '=' ), c_blue ); - if( precise ) { - hp_str = string_format( "%s %3d%%", hp_str, mend_perc ); - } - } - } else if( limb_is_broken ) { - desc += colorize( _( "It is broken. It needs a splint or surgical attention." ), c_red ) + "\n"; - hp_str = "==%=="; - } else if( no_feeling ) { - if( current_hp < maximal_hp * 0.25 ) { - hp_str = colorize( _( "Very Bad" ), c_red ); - } else if( current_hp < maximal_hp * 0.5 ) { - hp_str = colorize( _( "Bad" ), c_light_red ); - } else if( current_hp < maximal_hp * 0.75 ) { - hp_str = colorize( _( "Okay" ), c_light_green ); - } else { - hp_str = colorize( _( "Good" ), c_green ); - } - } else { - std::pair h_bar = get_hp_bar( current_hp, maximal_hp, false ); - hp_str = colorize( h_bar.first, h_bar.second ) + - colorize( std::string( 5 - utf8_width( h_bar.first ), '.' ), c_white ); - - if( precise ) { - hp_str = string_format( "%s %3d/%d", hp_str, current_hp, maximal_hp ); - } - } - msg += colorize( aligned_name, all_state_col ) + " " + hp_str; - - // BLEEDING block - if( bleeding ) { - desc += string_format( _( "Bleeding: %s" ), - colorize( get_effect( effect_bleed, bp ).get_speed_name(), - colorize_bleeding_intensity( get_effect_int( effect_bleed, bp ) ) ) ); - if( bleed > 0 ) { - int percent = static_cast( bleed * 100 / get_effect_int( effect_bleed, bp ) ); - percent = std::min( percent, 100 ); - desc += " -> " + colorize( string_format( _( "%d %% improvement" ), percent ), c_green ); - } - desc += "\n"; - } - - // BANDAGE block - if( e.allowed && ( new_b_power > 0 || b_power > 0 ) ) { - desc += string_format( _( "Bandaged: %s" ), texitify_healing_power( b_power ) ); - if( new_b_power > 0 ) { - desc += string_format( " -> %s", texitify_healing_power( new_b_power ) ); - if( new_b_power <= b_power ) { - desc += _( " (no improvement)" ); - } - } - desc += "\n"; - } - - // DISINFECTANT block - if( e.allowed && ( d_power > 0 || new_d_power > 0 ) ) { - desc += string_format( _( "Disinfected: %s" ), texitify_healing_power( d_power ) ); - if( new_d_power > 0 ) { - desc += string_format( " -> %s", texitify_healing_power( new_d_power ) ); - if( new_d_power <= d_power ) { - desc += _( " (no improvement)" ); - } - } - desc += "\n"; - } - - // BITTEN block - if( bitten ) { - desc += string_format( "%s: ", get_effect( effect_bite, bp ).get_speed_name() ); - if( bite > 0 ) { - desc += colorize( string_format( _( "Chance to clean and disinfect: %d %%" ), - static_cast( bite * 100 ) ), c_light_green ); - } else { - desc += colorize( _( "It has a deep bite wound that needs cleaning." ), c_red ); - } - desc += "\n"; - } - - // INFECTED block - if( infected ) { - desc += string_format( "%s: ", get_effect( effect_infected, bp ).get_speed_name() ); - if( infect > 0 ) { - desc += colorize( string_format( _( "Chance to cure infection: %d %%" ), - static_cast( infect * 100 ) ), c_light_green ) + "\n"; - } else { - desc += colorize( _( "It has a deep wound that looks infected. Antibiotics might be required." ), - c_red ); - } - desc += "\n"; - } - // END of blocks - - if( ( !e.allowed && !limb_is_broken ) || ( show_all && current_hp == maximal_hp && - !limb_is_broken && !bitten && !infected && !bleeding ) ) { - desc += colorize( _( "Healthy." ), c_green ) + "\n"; - } - if( !e.allowed ) { - desc += colorize( _( "You don't expect any effect from using this." ), c_yellow ); - } else { - is_valid_choice = true; - } - bmenu.addentry_desc( i, e.allowed, MENU_AUTOASSIGN, msg, desc ); - } - - if( !is_valid_choice ) { // no body part can be chosen for this item/effect - bmenu.init(); - bmenu.desc_enabled = false; - bmenu.text = _( "No limb would benefit from it." ); - bmenu.addentry( parts.size(), true, 'q', "%s", _( "Cancel" ) ); - } - - bmenu.query(); - if( bmenu.ret >= 0 && static_cast( bmenu.ret ) < parts.size() && - parts[bmenu.ret].allowed ) { - return parts[bmenu.ret].bp; - } else { - return bodypart_str_id::NULL_ID(); - } -} - std::string Character::get_name() const { return play_name.value_or( name ); @@ -10140,12 +8737,6 @@ bool Character::is_waterproof( const body_part_set &parts ) const return covered_with_flag( flag_WATERPROOF, parts ); } -void Character::update_morale() -{ - morale->decay( 1_minutes ); - apply_persistent_morale(); -} - units::volume Character::free_space() const { units::volume volume_capacity = 0_ml; @@ -10223,158 +8814,6 @@ units::volume Character::volume_carried() const return volume_capacity() - free_space(); } -void Character::hoarder_morale_penalty() -{ - int pen = free_space() / 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 ); - } -} - -void Character::apply_persistent_morale() -{ - // Hoarders get a morale penalty if they're not carrying a full inventory. - if( has_trait( trait_HOARDER ) ) { - hoarder_morale_penalty(); - } - // 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_abs_omt ompos = global_omt_location(); - float total_time = 0.0f; - // Check how long we've stayed in any overmap tile within 5 of us. - const int max_dist = 5; - for( const tripoint_abs_omt &pos : points_in_radius( ompos, max_dist ) ) { - const float dist = rl_dist( ompos, pos ); - if( dist > max_dist ) { - continue; - } - const auto iter = overmap_time.find( pos.xy() ); - if( iter == 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( iter->second ) * ( max_dist - dist ) / max_dist; - } - // Characters with higher tiers of Nomad suffer worse morale penalties, faster. - int max_unhappiness; - float min_time; - float max_time; - if( has_trait( trait_NOMAD ) ) { - max_unhappiness = 20; - min_time = to_moves( 12_hours ); - max_time = to_moves( 1_days ); - } else if( has_trait( trait_NOMAD2 ) ) { - max_unhappiness = 40; - min_time = to_moves( 4_hours ); - max_time = to_moves( 8_hours ); - } else { // traid_NOMAD3 - max_unhappiness = 60; - min_time = to_moves( 1_hours ); - max_time = to_moves( 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 = std::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 ); - } - } -} - -int Character::get_morale_level() const -{ - return morale->get_level(); -} - -void Character::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 Character::has_morale( const morale_type &type ) const -{ - return morale->has( type ); -} - -void Character::rem_morale( const morale_type &type, const itype *item_type ) -{ - morale->remove( type, item_type ); -} - -void Character::clear_morale() -{ - morale->clear(); -} - -bool Character::has_morale_to_read() const -{ - return get_morale_level() >= -40; -} - -void Character::check_and_recover_morale() -{ - player_morale test_morale; - - for( const item &wit : worn ) { - test_morale.on_item_wear( wit ); - } - - for( const trait_id &mut : get_mutations() ) { - test_morale.on_mutation_gain( mut ); - } - - for( auto &elem : *effects ) { - for( std::pair &_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_debug( debugmode::DF_CHARACTER, "%s morale was recovered.", disp_name( true ) ); - } -} - void Character::start_hauling() { add_msg( _( "You start hauling items along the ground." ) ); diff --git a/src/character_body.cpp b/src/character_body.cpp new file mode 100644 index 0000000000000..48bed40d0e97d --- /dev/null +++ b/src/character_body.cpp @@ -0,0 +1,1120 @@ +#include "avatar.h" +#include "character.h" +#include "flag.h" +#include "game.h" +#include "map.h" +#include "messages.h" +#include "morale_types.h" +#include "options.h" +#include "output.h" +#include "overmapbuffer.h" +#include "panels.h" +#include "type_id.h" +#include "vitamin.h" +#include "veh_type.h" +#include "vehicle.h" +#include "vpart_position.h" +#include "weather.h" + +static const efftype_id effect_bandaged( "bandaged" ); +static const efftype_id effect_bite( "bite" ); +static const efftype_id effect_bleed( "bleed" ); +static const efftype_id effect_blisters( "blisters" ); +static const efftype_id effect_cold( "cold" ); +static const efftype_id effect_common_cold( "common_cold" ); +static const efftype_id effect_disinfected( "disinfected" ); +static const efftype_id effect_flu( "flu" ); +static const efftype_id effect_frostbite( "frostbite" ); +static const efftype_id effect_frostbite_recovery( "frostbite_recovery" ); +static const efftype_id effect_hot( "hot" ); +static const efftype_id effect_hot_speed( "hot_speed" ); +static const efftype_id effect_hunger_blank( "hunger_blank" ); +static const efftype_id effect_hunger_engorged( "hunger_engorged" ); +static const efftype_id effect_hunger_famished( "hunger_famished" ); +static const efftype_id effect_hunger_full( "hunger_full" ); +static const efftype_id effect_hunger_hungry( "hunger_hungry" ); +static const efftype_id effect_hunger_near_starving( "hunger_near_starving" ); +static const efftype_id effect_hunger_satisfied( "hunger_satisfied" ); +static const efftype_id effect_hunger_starving( "hunger_starving" ); +static const efftype_id effect_hunger_very_hungry( "hunger_very_hungry" ); +static const efftype_id effect_infected( "infected" ); +static const efftype_id effect_hypovolemia( "hypovolemia" ); +static const efftype_id effect_mending( "mending" ); +static const efftype_id effect_narcosis( "narcosis" ); +static const efftype_id effect_sleep( "sleep" ); + +static const itype_id itype_rm13_armor_on( "rm13_armor_on" ); + +static const json_character_flag json_flag_HEATPROOF( "HEATPROOF" ); +static const json_character_flag json_flag_HEATSINK( "HEATSINK" ); +static const json_character_flag json_flag_NO_MINIMAL_HEALING( "NO_MINIMAL_HEALING" ); +static const json_character_flag json_flag_NO_THIRST( "NO_THIRST" ); + +static const trait_id trait_BARK( "BARK" ); +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_DEBUG_LS( "DEBUG_LS" ); +static const trait_id trait_DEBUG_NOTEMP( "DEBUG_NOTEMP" ); +static const trait_id trait_FELINE_FUR( "FELINE_FUR" ); +static const trait_id trait_FUR( "FUR" ); +static const trait_id trait_LIGHTFUR( "LIGHTFUR" ); +static const trait_id trait_LUPINE_FUR( "LUPINE_FUR" ); +static const trait_id trait_M_DEPENDENT( "M_DEPENDENT" ); +static const trait_id trait_NOPAIN( "NOPAIN" ); +static const trait_id trait_PYROMANIA( "PYROMANIA" ); +static const trait_id trait_RADIOGENIC( "RADIOGENIC" ); +static const trait_id trait_SLIMY( "SLIMY" ); +static const trait_id trait_URSINE_FUR( "URSINE_FUR" ); + +static const vitamin_id vitamin_blood( "blood" ); + +void Character::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 time_duration average_drying = 2_hours; + + // 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, to_turns( average_drying * delay / 100.0 ) ) ) { + // 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, 80 ); + + for( const bodypart_id &bp : get_all_body_parts() ) { + if( get_part_wetness( bp ) == 0 ) { + continue; + } + // This is to normalize drying times + int drying_chance = get_part_drench_capacity( bp ); + const int temp_conv = get_part_temp_conv( bp ); + // Body temperature affects duration of wetness + // Note: Using temp_conv rather than temp_cur, to better approximate environment + if( temp_conv >= BODYTEMP_SCORCHING ) { + drying_chance *= 2; + } else if( temp_conv >= BODYTEMP_VERY_HOT ) { + drying_chance = drying_chance * 3 / 2; + } else if( temp_conv >= BODYTEMP_HOT ) { + drying_chance = drying_chance * 4 / 3; + } else if( temp_conv > 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 ) { + mod_part_wetness( bp, -1 ); + if( get_part_wetness( bp ) < 0 ) { + set_part_wetness( bp, 0 ); + } + } + // Safety measure to keep wetness within bounds + if( get_part_wetness( bp ) > get_part_drench_capacity( bp ) ) { + set_part_wetness( bp, get_part_drench_capacity( bp ) ); + } + } + // TODO: Make clothing slow down drying +} + +void Character::update_body() +{ + update_body( calendar::turn - 1_turns, calendar::turn ); + last_updated = calendar::turn; +} + +// 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 +static inline int ticks_between( const time_point &from, const time_point &to, + const time_duration &tick_length ) +{ + return ( to_turn( to ) / to_turns( tick_length ) ) - ( to_turn + ( from ) / to_turns( tick_length ) ); +} + +void Character::update_body( const time_point &from, const time_point &to ) +{ + // Early return if we already did update previously on the same turn (e.g. when loading savegame). + if( to <= last_updated ) { + return; + } + if( !is_npc() ) { + update_stamina( to_turns( to - from ) ); + } + update_stomach( from, to ); + recalculate_enchantment_cache(); + if( ticks_between( from, to, 3_minutes ) > 0 ) { + magic->update_mana( *this, to_turns( 3_minutes ) ); + } + const int five_mins = ticks_between( from, to, 5_minutes ); + if( five_mins > 0 ) { + activity_history.try_reduce_weariness( base_bmr(), in_sleep_state() ); + 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 + // TODO: change @ref med to take time_duration + mend( five_mins * to_turns( 5_minutes ) ); + activity_history.reset_activity_level(); + } + + activity_history.new_turn(); + if( ticks_between( from, to, 24_hours ) > 0 && !has_flag( json_flag_NO_MINIMAL_HEALING ) ) { + enforce_minimum_healing(); + } + + const int thirty_mins = ticks_between( from, to, 30_minutes ); + if( thirty_mins > 0 ) { + // Radiation kills health even at low doses + update_health( has_trait( trait_RADIOGENIC ) ? 0 : -get_rad() ); + get_sick(); + } + + for( const auto &v : vitamin::all() ) { + const time_duration rate = vitamin_rate( v.first ); + + // No blood volume regeneration if body lacks fluids + if( v.first == vitamin_blood && has_effect( effect_hypovolemia ) && get_thirst() > 240 ) { + continue; + } + + 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 ); + } + } + } + + if( is_avatar() && ticks_between( from, to, 24_hours ) > 0 ) { + as_avatar()->advance_daily_calories(); + } + + if( calendar::once_every( 24_hours ) ) { + do_skill_rust(); + } +} + +/* 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 Character::update_bodytemp() +{ + if( has_trait( trait_DEBUG_NOTEMP ) ) { + set_all_parts_temp_conv( BODYTEMP_NORM ); + set_all_parts_temp_cur( BODYTEMP_NORM ); + return; + } + weather_manager &weather_man = get_weather(); + /* Cache calls to g->get_temperature( player position ), used in several places in function */ + const int player_local_temp = weather_man.get_temperature( pos() ); + // NOTE : visit weather.h for some details on the numbers used + // Converts temperature to Celsius/10 + int Ctemperature = static_cast( 100 * temp_to_celsius( player_local_temp ) ); + const w_point weather = *weather_man.weather_precise; + int vehwindspeed = 0; + map &here = get_map(); + const optional_vpart_position vp = here.veh_at( pos() ); + if( vp ) { + vehwindspeed = std::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( weather_man.windspeed + vehwindspeed, cur_om_ter, + pos(), weather_man.winddirection, sheltered ); + // Let's cache this not to check it for every bodyparts + 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 heat_immune = has_flag( json_flag_HEATPROOF ); + const bool has_heatsink = has_flag( json_flag_HEATSINK ) || is_wearing( itype_rm13_armor_on ) || + heat_immune; + 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 = here.furn( pos() ); + const cata::optional boardable = vp.part_with_feature( "BOARDABLE", true ); + // 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( 2000 * std::min( met_rate, 1.0f ) - 2000 ); + // Give SOME bonus to those living furnaces with extreme metabolism + const int metabolism_warmth = static_cast( std::max( 0.0f, met_rate - 1.0f ) * 1000 ); + // Fatigue + // ~-900 when exhausted + const int fatigue_warmth = has_sleep ? 0 : static_cast( clamp( -1.5f * get_fatigue(), -1350.0f, + 0.0f ) ); + + // Sunlight + const int sunlight_warmth = g->is_in_sunlight( pos() ) ? + ( get_weather().weather_id->sun_intensity == + sun_intensity_type::high ? + 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( get_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 ); + + std::map> clothing_map; + for( const bodypart_id &bp : get_all_body_parts() ) { + clothing_map.emplace( bp, std::vector() ); + } + for( const item &it : worn ) { + for( const bodypart_str_id &covered : it.get_covered_body_parts() ) { + clothing_map[covered.id()].emplace_back( &it ); + } + } + + std::map warmth_per_bp = warmth( clothing_map ); + std::map bonus_warmth_per_bp = bonus_item_warmth(); + std::map wind_res_per_bp = get_wind_resistance( clothing_map ); + // We might not use this at all, so leave it empty + // If we do need to use it, we'll initialize it (once) there + std::map fire_armor_per_bp; + // Current temperature and converging temperature calculations + for( const bodypart_id &bp : get_all_body_parts() ) { + + if( bp->has_flag( "IGNORE_TEMP" ) ) { + 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, + get_part_temp_cur( bp ) ); + // Produces a smooth curve between 30.0 and 60.0. + double homeostasis_adjustment = 30.0 * ( 1.0 + scaled_temperature ); + int clothing_warmth_adjustment = static_cast( homeostasis_adjustment * warmth_per_bp[bp] ); + int clothing_warmth_adjusted_bonus = static_cast( homeostasis_adjustment * + bonus_warmth_per_bp[bp] ); + // WINDCHILL + + bp_windpower = static_cast( static_cast( bp_windpower ) * + ( 1 - wind_res_per_bp[bp] / 100.0 ) ); + // Calculate windchill + int windchill = get_local_windchill( player_local_temp, + get_local_humidity( weather.humidity, get_weather().weather_id, sheltered ), + bp_windpower ); + + static const auto is_lower = []( const bodypart_id & bp ) { + return bp == body_part_foot_l || + bp == body_part_foot_r || + bp == body_part_leg_l || + bp == body_part_leg_r ; + }; + + // If you're standing in water, air temperature is replaced by water temperature. No wind. + // Convert to 0.01C + if( here.has_flag_ter( ter_furn_flag::TFLAG_DEEP_WATER, pos() ) || + ( here.has_flag_ter( ter_furn_flag::TFLAG_SHALLOW_WATER, pos() ) && is_lower( bp ) ) ) { + 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. + set_part_temp_conv( bp, BODYTEMP_NORM + adjusted_temp + windchill * 100 + + clothing_warmth_adjustment ); + // HUNGER / STARVATION + mod_part_temp_conv( bp, hunger_warmth ); + // FATIGUE + mod_part_temp_conv( bp, fatigue_warmth ); + // Mutations + mod_part_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( get_part_frostbite_timer( bp ) > 0 ) { + mod_part_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( std::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( has_heatsink ) { + blister_count -= 20; + } + if( fire_armor_per_bp.empty() && blister_count > 0 ) { + fire_armor_per_bp = get_armor_fire( clothing_map ); + } + if( blister_count - fire_armor_per_bp[bp] > 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 ); + } + + mod_part_temp_conv( bp, sunlight_warmth ); + // DISEASES + if( bp == body_part_head && has_effect( effect_flu ) ) { + mod_part_temp_conv( bp, 1500 ); + } + if( has_common_cold ) { + mod_part_temp_conv( bp, -750 ); + } + // Loss of blood results in loss of body heat, 1% bodyheat lost per 2% hp lost + mod_part_temp_conv( bp, - blood_loss( bp ) * get_part_temp_conv( bp ) / 200 ); + + temp_equalizer( bp, bp->connected_to ); + temp_equalizer( bp, bp->main_part ); + + // Climate Control eases the effects of high and low ambient temps + if( has_climate_control ) { + set_part_temp_conv( bp, temp_corrected_by_climate_control( get_part_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 + if( bp == body_part_foot_l || bp == body_part_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 if( boardable ) { + bonus_fire_warmth = best_fire * boardable->info().bonus_fire_warmth_feet; + } else { + // Has to stand + bonus_fire_warmth = best_fire * bp->fire_warmth_bonus; + } + } else { + bonus_fire_warmth = best_fire * bp->fire_warmth_bonus; + } + + } + + 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 * get_part_temp_conv( 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 < get_part_temp_conv( bp ) ) { + // Too hot, can't help here + } else if( desired < get_part_temp_conv( bp ) + bonus_warmth ) { + // Use some heat, but not all of it + set_part_temp_conv( bp, desired ); + } else { + // Use all the heat + mod_part_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::once_every( 1_minutes ) && get_effect_int( effect_cold ) == 0 && + get_effect_int( effect_hot ) == 0 && + get_part_temp_conv( bp ) > BODYTEMP_COLD && get_part_temp_conv( bp ) <= BODYTEMP_NORM ) { + add_morale( MORALE_COMFY, 1, 10, 2_minutes, 1_minutes, true ); + } + } + + const int temp_before = get_part_temp_cur( bp ); + const int cur_temp_conv = get_part_temp_conv( bp ); + int temp_difference = temp_before - cur_temp_conv; // 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_before != cur_temp_conv ) { + set_part_temp_cur( bp, static_cast( temp_difference * std::exp( -0.002 ) + cur_temp_conv + + 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 && + ( ( cur_temp_conv + clothing_warmth_adjusted_bonus ) < BODYTEMP_HOT || + get_part_temp_cur( bp ) < BODYTEMP_COLD ) ) { + mod_part_temp_conv( bp, clothing_warmth_adjusted_bonus ); + rounding_error = 0; + if( temp_difference < 0 && temp_difference > -600 ) { + rounding_error = 1; + } + const int new_temp_conv = get_part_temp_conv( bp ); + if( temp_before != new_temp_conv ) { + temp_difference = get_part_temp_cur( bp ) - new_temp_conv; + set_part_temp_cur( bp, static_cast( temp_difference * std::exp( -0.002 ) + new_temp_conv + + rounding_error ) ); + } + } + const int temp_after = get_part_temp_cur( bp ); + // PENALTIES + if( temp_after < BODYTEMP_FREEZING ) { + add_effect( effect_cold, 1_turns, bp, true, 3 ); + } else if( temp_after < BODYTEMP_VERY_COLD ) { + add_effect( effect_cold, 1_turns, bp, true, 2 ); + } else if( temp_after < BODYTEMP_COLD ) { + add_effect( effect_cold, 1_turns, bp, true, 1 ); + } else if( temp_after > BODYTEMP_SCORCHING && !heat_immune ) { + add_effect( effect_hot, 1_turns, bp, true, 3 ); + if( bp->main_part == bp.id() ) { + add_effect( effect_hot_speed, 1_turns, bp, true, 3 ); + } + } else if( temp_after > BODYTEMP_VERY_HOT && !heat_immune ) { + add_effect( effect_hot, 1_turns, bp, true, 2 ); + if( bp->main_part == bp.id() ) { + add_effect( effect_hot_speed, 1_turns, bp, true, 2 ); + } + } else if( temp_after > BODYTEMP_HOT && !heat_immune ) { + add_effect( effect_hot, 1_turns, bp, true, 1 ); + if( bp->main_part == bp.id() ) { + add_effect( effect_hot_speed, 1_turns, bp, true, 1 ); + } + } else { + remove_effect( effect_cold, bp ); + remove_effect( effect_hot, bp ); + remove_effect( effect_hot_speed, bp ); + } + + update_frostbite( bp, bp_windpower, warmth_per_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 ) ); + } + + // Note: Numbers are based off of BODYTEMP at the top of weather.h + // If torso is BODYTEMP_COLD which is 34C, the early stages of hypothermia begin + // constant shivering will prevent the player from falling asleep. + // Otherwise, if any other body part is BODYTEMP_VERY_COLD, or 31C + // AND you have frostbite, then that also prevents you from sleeping + if( in_sleep_state() && !has_effect( effect_narcosis ) ) { + if( bp == body_part_torso && temp_after <= BODYTEMP_COLD ) { + add_msg( m_warning, _( "Your shivering prevents you from sleeping." ) ); + wake_up(); + } else if( bp != body_part_torso && temp_after <= BODYTEMP_VERY_COLD && + has_effect( effect_frostbite ) ) { + add_msg( m_warning, _( "You are too cold. Your frostbite prevents you from sleeping." ) ); + wake_up(); + } + } + + const int conv_temp = get_part_temp_conv( 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( conv_temp <= 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( conv_temp <= 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( conv_temp <= 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 ) ); + } + } +} + +void Character::update_frostbite( const bodypart_id &bp, const int FBwindPower, + const std::map &warmth_per_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 + **/ + + const int player_local_temp = get_weather().get_temperature( pos() ); + const int temp_after = get_part_temp_cur( bp ); + + if( bp == body_part_mouth || bp == body_part_foot_r || + bp == body_part_foot_l || bp == body_part_hand_r || bp == body_part_hand_l ) { + // Handle the frostbite timer + // Need temps in F, windPower already in mph + int wetness_percentage = 100 * get_part_wetness_percentage( bp ); // 0 - 100 + // Warmth gives a slight buff to temperature resistance + // Wetness gives a heavy nerf to temperature resistance + double adjusted_warmth = warmth_per_bp.at( bp ) - wetness_percentage; + int Ftemperature = static_cast( player_local_temp + 0.2 * adjusted_warmth ); + + int intense = get_effect_int( effect_frostbite, bp ); + + // This has been broken down into 8 zones + // Low risk zones (stops at frostnip) + if( temp_after < BODYTEMP_COLD && ( ( Ftemperature < 30 && Ftemperature >= 10 ) || + ( Ftemperature < 10 && Ftemperature >= -5 && FBwindPower < 20 && + -4 * Ftemperature + 3 * FBwindPower - 20 >= 0 ) ) ) { + if( get_part_frostbite_timer( bp ) < 2000 ) { + mod_part_frostbite_timer( bp, 3 ); + } + if( one_in( 100 ) && !has_effect( effect_frostbite, bp.id() ) ) { + add_msg( m_warning, _( "Your %s will be frostnipped in the next few hours." ), + body_part_name( bp ) ); + } + // Medium risk zones + } else if( temp_after < 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 ) ) ) { + mod_part_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_after < BODYTEMP_COLD && + ( ( Ftemperature < -5 && FBwindPower >= 10 && + -4 * Ftemperature + 3 * FBwindPower - 170 < 0 ) || + ( Ftemperature < -35 && FBwindPower >= 10 ) ) ) { + mod_part_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 { + mod_part_frostbite_timer( bp, -3 ); + } + + int frostbite_timer = get_part_frostbite_timer( bp ); + // Handle the bestowing of frostbite + if( frostbite_timer < 0 ) { + set_part_frostbite_timer( bp, 0 ); + } else if( frostbite_timer > 4200 ) { + // This ensures that the player will recover in at most 3 hours. + set_part_frostbite_timer( bp, 4200 ); + } + frostbite_timer = get_part_frostbite_timer( bp ); + // Frostbite, no recovery possible + if( frostbite_timer >= 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 >= 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 == 0 ) { + remove_effect( effect_frostbite, bp ); + remove_effect( effect_frostbite_recovery, bp ); + } + } +} + +void Character::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( "NO_NPC_FOOD" ); + const bool foodless = debug_ls || npc_no_food; + const bool no_thirst = has_flag( json_flag_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 ); + const int half_hours = ticks_between( from, to, 30_minutes ); + const units::volume stomach_capacity = stomach.capacity( *this ); + + if( five_mins > 0 ) { + // Digest nutrients in stomach, they are destined for the guts (except water) + food_summary digested_to_guts = stomach.digest( *this, rates, five_mins, half_hours ); + // Digest nutrients in guts, they will be distributed to needs levels + food_summary digested_to_body = guts.digest( *this, rates, five_mins, half_hours ); + // Water from stomach skips guts and gets absorbed by body + mod_thirst( -units::to_milliliter( digested_to_guts.water ) / 5 ); + guts.ingest( digested_to_guts ); + + mod_stored_kcal( digested_to_body.nutr.kcal() ); + vitamins_mod( effect_vitamin_mod( digested_to_body.nutr.vitamins ), false ); + log_activity_level( activity_history.average_activity() ); + + 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 + // Explicitly floor it here, the int cast will do so anyways + mod_stored_calories( -std::floor( five_mins * kcal_per_time * 1000 ) ); + } + } + // if npc_no_food no need to calc hunger, and set hunger_effect + if( npc_no_food ) { + return; + } + 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 ) { + // full + 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 && 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 && + get_stored_kcal() >= get_healthy_kcal() ) || get_stored_kcal() < get_healthy_kcal() ) ) { + set_hunger( 100 ); + } else if( get_hunger() < 0 ) { + set_hunger( 0 ); + } + } + } 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 ) { + // full + 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 || no_thirst ) { + set_thirst( 0 ); + } + + const bool calorie_deficit = get_bmi() < character_weight_category::normal; + const units::volume contains = stomach.contains(); + const units::volume cap = stomach.capacity( *this ); + + efftype_id hunger_effect; + // 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; + // Hunger effect should intensify whenever stomach contents decreases, last eaten time increases, or calorie deficit intensifies. + if( calorie_deficit ) { + // just_ate recently_ate + // <15 min <3 hrs >=3 hrs + // >= cap engorged engorged engorged + // > 3/4 cap full full full + // > 1/2 cap satisfied v. hungry famished/(near)starving + // <= 1/2 cap hungry v. hungry famished/(near)starving + if( contains >= cap ) { + hunger_effect = effect_hunger_engorged; + } else if( contains > cap * 3 / 4 ) { + hunger_effect = effect_hunger_full; + } else if( just_ate && contains > cap / 2 ) { + hunger_effect = effect_hunger_satisfied; + } else if( just_ate ) { + hunger_effect = effect_hunger_hungry; + } else if( recently_ate ) { + hunger_effect = effect_hunger_very_hungry; + } else if( get_bmi() < character_weight_category::underweight ) { + hunger_effect = effect_hunger_near_starving; + } else if( get_bmi() < character_weight_category::emaciated ) { + hunger_effect = effect_hunger_starving; + } else { + hunger_effect = effect_hunger_famished; + } + } else { + // just_ate recently_ate + // <15 min <3 hrs >=3 hrs + // >= 5/6 cap engorged engorged engorged + // > 11/20 cap full full full + // >= 3/8 cap satisfied satisfied blank + // > 0 blank blank blank + // 0 blank blank (v.) hungry + if( contains >= cap * 5 / 6 ) { + hunger_effect = effect_hunger_engorged; + } else if( contains > cap * 11 / 20 ) { + hunger_effect = effect_hunger_full; + } else if( recently_ate && contains >= cap * 3 / 8 ) { + hunger_effect = effect_hunger_satisfied; + } else if( recently_ate || contains > 0_ml ) { + hunger_effect = effect_hunger_blank; + } else if( get_bmi() > character_weight_category::overweight ) { + hunger_effect = effect_hunger_hungry; + } else { + hunger_effect = effect_hunger_very_hungry; + } + } + if( !has_effect( hunger_effect ) ) { + remove_effect( effect_hunger_engorged ); + remove_effect( effect_hunger_full ); + remove_effect( effect_hunger_satisfied ); + remove_effect( effect_hunger_hungry ); + remove_effect( effect_hunger_very_hungry ); + remove_effect( effect_hunger_near_starving ); + remove_effect( effect_hunger_starving ); + remove_effect( effect_hunger_famished ); + remove_effect( effect_hunger_blank ); + add_effect( hunger_effect, 24_hours, true ); + } +} + +bodypart_id Character::body_window( const std::string &menu_header, + bool show_all, bool precise, + int normal_bonus, int /* head_bonus */, int /* torso_bonus */, + int bleed, float bite, float infect, float bandage_power, float disinfectant_power ) const +{ + /* This struct establishes some kind of connection between the hp_part (which can be healed and + * have HP) and the body_part. Note that there are more body_parts than hp_parts. For example: + * Damage to bp_head, bp_eyes and bp_mouth is all applied on the HP of hp_head. */ + struct healable_bp { + mutable bool allowed; + bodypart_id bp; + std::string name; // Translated name as it appears in the menu. + int bonus; + }; + + std::vector parts; + for( const bodypart_id &part : this->get_all_body_parts( get_body_part_flags::only_main ) ) { + // TODO: figure out how to do head and torso bonus? + parts.push_back( { false, part, part->name.translated(), normal_bonus } ); + } + + int max_bp_name_len = 0; + for( const healable_bp &e : parts ) { + max_bp_name_len = std::max( max_bp_name_len, utf8_width( e.name ) ); + } + + uilist bmenu; + bmenu.desc_enabled = true; + bmenu.text = menu_header; + bmenu.textwidth = 60; + + bmenu.hilight_disabled = true; + bool is_valid_choice = false; + + // If this is an NPC, the player is the one examining them and so the fact + // that they can't self-diagnose effectively doesn't matter + bool no_feeling = is_avatar() && has_trait( trait_NOPAIN ); + + for( size_t i = 0; i < parts.size(); i++ ) { + const healable_bp &e = parts[i]; + const bodypart_id &bp = e.bp; + const int maximal_hp = get_part_hp_max( bp ); + const int current_hp = get_part_hp_cur( bp ); + // This will c_light_gray if the part does not have any effects cured by the item/effect + // (e.g. it cures only bites, but the part does not have a bite effect) + const nc_color state_col = display::limb_color( *this, bp, bleed > 0, bite > 0.0f, infect > 0.0f ); + const bool has_curable_effect = state_col != c_light_gray; + // The same as in the main UI sidebar. Independent of the capability of the healing item/effect! + const nc_color all_state_col = display::limb_color( *this, bp, true, true, true ); + // Broken means no HP can be restored, it requires surgical attention. + const bool limb_is_broken = is_limb_broken( bp ); + const bool limb_is_mending = worn_with_flag( flag_SPLINT, bp ); + + if( show_all || has_curable_effect ) { // NOLINT(bugprone-branch-clone) + e.allowed = true; + } else if( limb_is_broken ) { // NOLINT(bugprone-branch-clone) + e.allowed = false; + } else if( current_hp < maximal_hp && ( e.bonus != 0 || bandage_power > 0.0f || + disinfectant_power > 0.0f ) ) { + e.allowed = true; + } else { + e.allowed = false; + } + + std::string msg; + std::string desc; + bool bleeding = has_effect( effect_bleed, bp.id() ); + bool bitten = has_effect( effect_bite, bp.id() ); + bool infected = has_effect( effect_infected, bp.id() ); + bool bandaged = has_effect( effect_bandaged, bp.id() ); + const int b_power = get_effect_int( effect_bandaged, bp ); + const int d_power = get_effect_int( effect_disinfected, bp ); + int new_b_power = static_cast( std::floor( bandage_power ) ); + if( bandaged ) { + const effect &eff = get_effect( effect_bandaged, bp ); + if( new_b_power > eff.get_max_intensity() ) { + new_b_power = eff.get_max_intensity(); + } + + } + int new_d_power = static_cast( std::floor( disinfectant_power ) ); + + const auto &aligned_name = std::string( max_bp_name_len - utf8_width( e.name ), ' ' ) + e.name; + std::string hp_str; + if( limb_is_mending ) { + desc += colorize( _( "It is broken, but has been set, and just needs time to heal." ), + c_blue ) + "\n"; + if( no_feeling ) { + hp_str = colorize( "==%==", c_blue ); + } else { + const auto &eff = get_effect( effect_mending, bp ); + const int mend_perc = eff.is_null() ? 0.0 : 100 * eff.get_duration() / eff.get_max_duration(); + + const int num = mend_perc / 20; + hp_str = colorize( std::string( num, '#' ) + std::string( 5 - num, '=' ), c_blue ); + if( precise ) { + hp_str = string_format( "%s %3d%%", hp_str, mend_perc ); + } + } + } else if( limb_is_broken ) { + desc += colorize( _( "It is broken. It needs a splint or surgical attention." ), c_red ) + "\n"; + hp_str = "==%=="; + } else if( no_feeling ) { + if( current_hp < maximal_hp * 0.25 ) { + hp_str = colorize( _( "Very Bad" ), c_red ); + } else if( current_hp < maximal_hp * 0.5 ) { + hp_str = colorize( _( "Bad" ), c_light_red ); + } else if( current_hp < maximal_hp * 0.75 ) { + hp_str = colorize( _( "Okay" ), c_light_green ); + } else { + hp_str = colorize( _( "Good" ), c_green ); + } + } else { + std::pair h_bar = get_hp_bar( current_hp, maximal_hp, false ); + hp_str = colorize( h_bar.first, h_bar.second ) + + colorize( std::string( 5 - utf8_width( h_bar.first ), '.' ), c_white ); + + if( precise ) { + hp_str = string_format( "%s %3d/%d", hp_str, current_hp, maximal_hp ); + } + } + msg += colorize( aligned_name, all_state_col ) + " " + hp_str; + + // BLEEDING block + if( bleeding ) { + desc += string_format( _( "Bleeding: %s" ), + colorize( get_effect( effect_bleed, bp ).get_speed_name(), + colorize_bleeding_intensity( get_effect_int( effect_bleed, bp ) ) ) ); + if( bleed > 0 ) { + int percent = static_cast( bleed * 100 / get_effect_int( effect_bleed, bp ) ); + percent = std::min( percent, 100 ); + desc += " -> " + colorize( string_format( _( "%d %% improvement" ), percent ), c_green ); + } + desc += "\n"; + } + + // BANDAGE block + if( e.allowed && ( new_b_power > 0 || b_power > 0 ) ) { + desc += string_format( _( "Bandaged: %s" ), texitify_healing_power( b_power ) ); + if( new_b_power > 0 ) { + desc += string_format( " -> %s", texitify_healing_power( new_b_power ) ); + if( new_b_power <= b_power ) { + desc += _( " (no improvement)" ); + } + } + desc += "\n"; + } + + // DISINFECTANT block + if( e.allowed && ( d_power > 0 || new_d_power > 0 ) ) { + desc += string_format( _( "Disinfected: %s" ), texitify_healing_power( d_power ) ); + if( new_d_power > 0 ) { + desc += string_format( " -> %s", texitify_healing_power( new_d_power ) ); + if( new_d_power <= d_power ) { + desc += _( " (no improvement)" ); + } + } + desc += "\n"; + } + + // BITTEN block + if( bitten ) { + desc += string_format( "%s: ", get_effect( effect_bite, bp ).get_speed_name() ); + if( bite > 0 ) { + desc += colorize( string_format( _( "Chance to clean and disinfect: %d %%" ), + static_cast( bite * 100 ) ), c_light_green ); + } else { + desc += colorize( _( "It has a deep bite wound that needs cleaning." ), c_red ); + } + desc += "\n"; + } + + // INFECTED block + if( infected ) { + desc += string_format( "%s: ", get_effect( effect_infected, bp ).get_speed_name() ); + if( infect > 0 ) { + desc += colorize( string_format( _( "Chance to cure infection: %d %%" ), + static_cast( infect * 100 ) ), c_light_green ) + "\n"; + } else { + desc += colorize( _( "It has a deep wound that looks infected. Antibiotics might be required." ), + c_red ); + } + desc += "\n"; + } + // END of blocks + + if( ( !e.allowed && !limb_is_broken ) || ( show_all && current_hp == maximal_hp && + !limb_is_broken && !bitten && !infected && !bleeding ) ) { + desc += colorize( _( "Healthy." ), c_green ) + "\n"; + } + if( !e.allowed ) { + desc += colorize( _( "You don't expect any effect from using this." ), c_yellow ); + } else { + is_valid_choice = true; + } + bmenu.addentry_desc( i, e.allowed, MENU_AUTOASSIGN, msg, desc ); + } + + if( !is_valid_choice ) { // no body part can be chosen for this item/effect + bmenu.init(); + bmenu.desc_enabled = false; + bmenu.text = _( "No limb would benefit from it." ); + bmenu.addentry( parts.size(), true, 'q', "%s", _( "Cancel" ) ); + } + + bmenu.query(); + if( bmenu.ret >= 0 && static_cast( bmenu.ret ) < parts.size() && + parts[bmenu.ret].allowed ) { + return parts[bmenu.ret].bp; + } else { + return bodypart_str_id::NULL_ID(); + } +} + diff --git a/src/character_escape.cpp b/src/character_escape.cpp new file mode 100644 index 0000000000000..43280c0137e56 --- /dev/null +++ b/src/character_escape.cpp @@ -0,0 +1,338 @@ +#include "character.h" +#include "creature_tracker.h" +#include "flag.h" +#include "map.h" +#include "map_iterator.h" +#include "messages.h" +#include "monster.h" +#include "mtype.h" + +static const efftype_id effect_beartrap( "beartrap" ); +static const efftype_id effect_crushed( "crushed" ); +static const efftype_id effect_downed( "downed" ); +static const efftype_id effect_grabbed( "grabbed" ); +static const efftype_id effect_grabbing( "grabbing" ); +static const efftype_id effect_heavysnare( "heavysnare" ); +static const efftype_id effect_in_pit( "in_pit" ); +static const efftype_id effect_lightsnare( "lightsnare" ); +static const efftype_id effect_webbed( "webbed" ); + +static const itype_id itype_beartrap( "beartrap" ); +static const itype_id itype_rope_6( "rope_6" ); +static const itype_id itype_snare_trigger( "snare_trigger" ); +static const itype_id itype_string_36( "string_36" ); + +void Character::try_remove_downed() +{ + + /** @EFFECT_DEX increases chance to stand up when knocked down */ + + /** @EFFECT_STR increases chance to stand up when knocked down, slightly */ + if( rng( 0, 40 ) > get_dex() + get_str() / 2 ) { + add_msg_if_player( _( "You struggle to stand." ) ); + } else { + add_msg_player_or_npc( m_good, _( "You stand up." ), + _( " stands up." ) ); + remove_effect( effect_downed ); + } +} + +void Character::try_remove_bear_trap() +{ + /* Real bear traps can't be removed without the proper tools or immense strength; eventually this should + allow normal players two options: removal of the limb or removal of the trap from the ground + (at which point the player could later remove it from the leg with the right tools). + As such we are currently making it a bit easier for players and NPC's to get out of bear traps. + */ + /** @EFFECT_STR increases chance to escape bear trap */ + // If is riding, then despite the character having the effect, it is the mounted creature that escapes. + map &here = get_map(); + if( is_avatar() && is_mounted() ) { + auto *mon = mounted_creature.get(); + if( mon->type->melee_dice * mon->type->melee_sides >= 18 ) { + if( x_in_y( mon->type->melee_dice * mon->type->melee_sides, 200 ) ) { + mon->remove_effect( effect_beartrap ); + remove_effect( effect_beartrap ); + here.spawn_item( pos(), itype_beartrap ); + add_msg( _( "The %s escapes the bear trap!" ), mon->get_name() ); + } else { + add_msg_if_player( m_bad, + _( "Your %s tries to free itself from the bear trap, but can't get loose!" ), mon->get_name() ); + } + } + } else { + if( x_in_y( get_str(), 100 ) ) { + remove_effect( effect_beartrap ); + add_msg_player_or_npc( m_good, _( "You free yourself from the bear trap!" ), + _( " frees themselves from the bear trap!" ) ); + item beartrap( "beartrap", calendar::turn ); + here.add_item_or_charges( pos(), beartrap ); + } else { + add_msg_if_player( m_bad, + _( "You try to free yourself from the bear trap, but can't get loose!" ) ); + } + } +} + +void Character::try_remove_lightsnare() +{ + map &here = get_map(); + if( is_mounted() ) { + auto *mon = mounted_creature.get(); + if( x_in_y( mon->type->melee_dice * mon->type->melee_sides, 12 ) ) { + mon->remove_effect( effect_lightsnare ); + remove_effect( effect_lightsnare ); + here.spawn_item( pos(), itype_string_36 ); + here.spawn_item( pos(), itype_snare_trigger ); + add_msg( _( "The %s escapes the light snare!" ), mon->get_name() ); + } + } else { + /** @EFFECT_STR increases chance to escape light snare */ + + /** @EFFECT_DEX increases chance to escape light snare */ + if( x_in_y( get_str(), 12 ) || x_in_y( get_dex(), 8 ) ) { + remove_effect( effect_lightsnare ); + add_msg_player_or_npc( m_good, _( "You free yourself from the light snare!" ), + _( " frees themselves from the light snare!" ) ); + item string( "string_36", calendar::turn ); + item snare( "snare_trigger", calendar::turn ); + here.add_item_or_charges( pos(), string ); + here.add_item_or_charges( pos(), snare ); + } else { + add_msg_if_player( m_bad, + _( "You try to free yourself from the light snare, but can't get loose!" ) ); + } + } +} + +void Character::try_remove_heavysnare() +{ + map &here = get_map(); + if( is_mounted() ) { + auto *mon = mounted_creature.get(); + if( mon->type->melee_dice * mon->type->melee_sides >= 7 ) { + if( x_in_y( mon->type->melee_dice * mon->type->melee_sides, 32 ) ) { + mon->remove_effect( effect_heavysnare ); + remove_effect( effect_heavysnare ); + here.spawn_item( pos(), itype_rope_6 ); + here.spawn_item( pos(), itype_snare_trigger ); + add_msg( _( "The %s escapes the heavy snare!" ), mon->get_name() ); + } + } + } else { + /** @EFFECT_STR increases chance to escape heavy snare, slightly */ + + /** @EFFECT_DEX increases chance to escape light snare */ + if( x_in_y( get_str(), 32 ) || x_in_y( get_dex(), 16 ) ) { + remove_effect( effect_heavysnare ); + add_msg_player_or_npc( m_good, _( "You free yourself from the heavy snare!" ), + _( " frees themselves from the heavy snare!" ) ); + item rope( "rope_6", calendar::turn ); + item snare( "snare_trigger", calendar::turn ); + here.add_item_or_charges( pos(), rope ); + here.add_item_or_charges( pos(), snare ); + } else { + add_msg_if_player( m_bad, + _( "You try to free yourself from the heavy snare, but can't get loose!" ) ); + } + } +} + +void Character::try_remove_crushed() +{ + /** @EFFECT_STR increases chance to escape crushing rubble */ + + /** @EFFECT_DEX increases chance to escape crushing rubble, slightly */ + if( x_in_y( get_str() + get_dex() / 4.0, 100 ) ) { + remove_effect( effect_crushed ); + add_msg_player_or_npc( m_good, _( "You free yourself from the rubble!" ), + _( " frees themselves from the rubble!" ) ); + } else { + add_msg_if_player( m_bad, _( "You try to free yourself from the rubble, but can't get loose!" ) ); + } +} + +bool Character::try_remove_grab() +{ + int zed_number = 0; + if( is_mounted() ) { + auto *mon = mounted_creature.get(); + if( mon->has_effect( effect_grabbed ) ) { + if( ( dice( mon->type->melee_dice + mon->type->melee_sides, + 3 ) < get_effect_int( effect_grabbed ) ) || + !one_in( 4 ) ) { + add_msg( m_bad, _( "Your %s tries to break free, but fails!" ), mon->get_name() ); + return false; + } else { + add_msg( m_good, _( "Your %s breaks free from the grab!" ), mon->get_name() ); + remove_effect( effect_grabbed ); + mon->remove_effect( effect_grabbed ); + } + } else { + if( one_in( 4 ) ) { + add_msg( m_bad, _( "You are pulled from your %s!" ), mon->get_name() ); + remove_effect( effect_grabbed ); + forced_dismount(); + } + } + } else { + map &here = get_map(); + creature_tracker &creatures = get_creature_tracker(); + for( auto&& dest : here.points_in_radius( pos(), 1, 0 ) ) { // *NOPAD* + const monster *const mon = creatures.creature_at( dest ); + if( mon && mon->has_effect( effect_grabbing ) ) { + zed_number += mon->get_grab_strength(); + } + } + if( zed_number == 0 ) { + add_msg_player_or_npc( m_good, _( "You find yourself no longer grabbed." ), + _( " finds themselves no longer grabbed." ) ); + remove_effect( effect_grabbed ); + + /** @EFFECT_STR increases chance to escape grab */ + } else if( rng( 0, get_str() ) < rng( get_effect_int( effect_grabbed, body_part_torso ), + 8 ) ) { + add_msg_player_or_npc( m_bad, _( "You try break out of the grab, but fail!" ), + _( " tries to break out of the grab, but fails!" ) ); + return false; + } else { + add_msg_player_or_npc( m_good, _( "You break out of the grab!" ), + _( " breaks out of the grab!" ) ); + remove_effect( effect_grabbed ); + for( auto&& dest : here.points_in_radius( pos(), 1, 0 ) ) { // *NOPAD* + monster *mon = creatures.creature_at( dest ); + if( mon && mon->has_effect( effect_grabbing ) ) { + mon->remove_effect( effect_grabbing ); + } + } + } + } + return true; +} + +void Character::try_remove_webs() +{ + if( is_mounted() ) { + auto *mon = mounted_creature.get(); + if( x_in_y( mon->type->melee_dice * mon->type->melee_sides, + 6 * get_effect_int( effect_webbed ) ) ) { + add_msg( _( "The %s breaks free of the webs!" ), mon->get_name() ); + mon->remove_effect( effect_webbed ); + remove_effect( effect_webbed ); + } + /** @EFFECT_STR increases chance to escape webs */ + } else if( x_in_y( get_str(), 6 * get_effect_int( effect_webbed ) ) ) { + add_msg_player_or_npc( m_good, _( "You free yourself from the webs!" ), + _( " frees themselves from the webs!" ) ); + remove_effect( effect_webbed ); + } else { + add_msg_if_player( _( "You try to free yourself from the webs, but can't get loose!" ) ); + } +} + +void Character::try_remove_impeding_effect() +{ + for( const effect &eff : get_effects_with_flag( flag_EFFECT_IMPEDING ) ) { + const efftype_id &eff_id = eff.get_id(); + if( is_mounted() ) { + auto *mon = mounted_creature.get(); + if( x_in_y( mon->type->melee_dice * mon->type->melee_sides, + 6 * get_effect_int( eff_id ) ) ) { + add_msg( _( "The %s breaks free!" ), mon->get_name() ); + mon->remove_effect( eff_id ); + remove_effect( eff_id ); + } + /** @EFFECT_STR increases chance to escape webs */ + } else if( x_in_y( get_str(), 6 * get_effect_int( eff_id ) ) ) { + add_msg_player_or_npc( m_good, _( "You free yourself!" ), + _( " frees themselves!" ) ); + remove_effect( eff_id ); + } else { + add_msg_if_player( _( "You try to free yourself, but can't!" ) ); + } + } +} + +bool Character::move_effects( bool attacking ) +{ + if( has_effect( effect_downed ) ) { + try_remove_downed(); + return false; + } + if( has_effect( effect_webbed ) ) { + try_remove_webs(); + return false; + } + if( has_effect( effect_lightsnare ) ) { + try_remove_lightsnare(); + return false; + + } + if( has_effect( effect_heavysnare ) ) { + try_remove_heavysnare(); + return false; + } + if( has_effect( effect_beartrap ) ) { + try_remove_bear_trap(); + return false; + } + if( has_effect( effect_crushed ) ) { + try_remove_crushed(); + return false; + } + if( has_effect_with_flag( flag_EFFECT_IMPEDING ) ) { + try_remove_impeding_effect(); + return false; + } + + // Below this point are things that allow for movement if they succeed + + // Currently we only have one thing that forces movement if you succeed, should we get more + // than this will need to be reworked to only have success effects if /all/ checks succeed + if( has_effect( effect_in_pit ) ) { + /** @EFFECT_STR increases chance to escape pit */ + + /** @EFFECT_DEX increases chance to escape pit, slightly */ + if( rng( 0, 40 ) > get_str() + get_dex() / 2 ) { + add_msg_if_player( m_bad, _( "You try to escape the pit, but slip back in." ) ); + return false; + } else { + add_msg_player_or_npc( m_good, _( "You escape the pit!" ), + _( " escapes the pit!" ) ); + remove_effect( effect_in_pit ); + } + } + return !has_effect( effect_grabbed ) || attacking || try_remove_grab(); +} + +void Character::wait_effects( bool attacking ) +{ + if( has_effect( effect_downed ) ) { + try_remove_downed(); + return; + } + if( has_effect( effect_beartrap ) ) { + try_remove_bear_trap(); + return; + } + if( has_effect( effect_lightsnare ) ) { + try_remove_lightsnare(); + return; + } + if( has_effect( effect_heavysnare ) ) { + try_remove_heavysnare(); + return; + } + if( has_effect( effect_webbed ) ) { + try_remove_webs(); + return; + } + if( has_effect_with_flag( flag_EFFECT_IMPEDING ) ) { + try_remove_impeding_effect(); + return; + } + if( has_effect( effect_grabbed ) && !attacking && !try_remove_grab() ) { + return; + } +} + diff --git a/src/character_morale.cpp b/src/character_morale.cpp new file mode 100644 index 0000000000000..b8d863122fffe --- /dev/null +++ b/src/character_morale.cpp @@ -0,0 +1,172 @@ +#include "character.h" +#include "messages.h" +#include "morale.h" +#include "map_iterator.h" + +static const efftype_id effect_took_prozac( "took_prozac" ); +static const efftype_id effect_took_xanax( "took_xanax" ); + +static const trait_id trait_HOARDER( "HOARDER" ); +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_PROF_FOODP( "PROF_FOODP" ); + +void Character::update_morale() +{ + morale->decay( 1_minutes ); + apply_persistent_morale(); +} + +void Character::hoarder_morale_penalty() +{ + int pen = free_space() / 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 ); + } +} + +void Character::apply_persistent_morale() +{ + // Hoarders get a morale penalty if they're not carrying a full inventory. + if( has_trait( trait_HOARDER ) ) { + hoarder_morale_penalty(); + } + // 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_abs_omt ompos = global_omt_location(); + float total_time = 0.0f; + // Check how long we've stayed in any overmap tile within 5 of us. + const int max_dist = 5; + for( const tripoint_abs_omt &pos : points_in_radius( ompos, max_dist ) ) { + const float dist = rl_dist( ompos, pos ); + if( dist > max_dist ) { + continue; + } + const auto iter = overmap_time.find( pos.xy() ); + if( iter == 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( iter->second ) * ( max_dist - dist ) / max_dist; + } + // Characters with higher tiers of Nomad suffer worse morale penalties, faster. + int max_unhappiness; + float min_time; + float max_time; + if( has_trait( trait_NOMAD ) ) { + max_unhappiness = 20; + min_time = to_moves( 12_hours ); + max_time = to_moves( 1_days ); + } else if( has_trait( trait_NOMAD2 ) ) { + max_unhappiness = 40; + min_time = to_moves( 4_hours ); + max_time = to_moves( 8_hours ); + } else { // traid_NOMAD3 + max_unhappiness = 60; + min_time = to_moves( 1_hours ); + max_time = to_moves( 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 = std::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 ); + } + } +} + +int Character::get_morale_level() const +{ + return morale->get_level(); +} + +void Character::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 Character::has_morale( const morale_type &type ) const +{ + return morale->has( type ); +} + +void Character::rem_morale( const morale_type &type, const itype *item_type ) +{ + morale->remove( type, item_type ); +} + +void Character::clear_morale() +{ + morale->clear(); +} + +bool Character::has_morale_to_read() const +{ + return get_morale_level() >= -40; +} + +void Character::check_and_recover_morale() +{ + player_morale test_morale; + + for( const item &wit : worn ) { + test_morale.on_item_wear( wit ); + } + + for( const trait_id &mut : get_mutations() ) { + test_morale.on_mutation_gain( mut ); + } + + for( auto &elem : *effects ) { + for( std::pair &_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_debug( debugmode::DF_CHARACTER, "%s morale was recovered.", disp_name( true ) ); + } +} +