From 6f4bbd5d2f0d0ca72df8896e25b40bbda3392ede Mon Sep 17 00:00:00 2001 From: CountAlex Date: Sun, 12 Apr 2020 06:07:24 +0300 Subject: [PATCH] Add fitness band and heartrate function for character (#36233) --- data/json/item_actions.json | 5 + .../itemgroups/Clothing_Gear/clothing.json | 6 +- .../locations_commercial.json | 3 +- .../Locations_MapExtras/mall_item_groups.json | 3 +- .../Locations_MapExtras/mansion.json | 6 +- data/json/itemgroups/activities_hobbies.json | 6 +- data/json/items/tool_armor.json | 18 +++ src/character.cpp | 117 ++++++++++++++++++ src/character.h | 1 + src/item_factory.cpp | 1 + src/iuse.cpp | 38 ++++++ src/iuse.h | 1 + 12 files changed, 197 insertions(+), 8 deletions(-) diff --git a/data/json/item_actions.json b/data/json/item_actions.json index 9663413e96a32..96de4cbd1d559 100644 --- a/data/json/item_actions.json +++ b/data/json/item_actions.json @@ -749,6 +749,11 @@ "id": "PORTABLE_GAME", "name": { "ctxt": "PORTABLE_GAME", "str": "Play" } }, + { + "type": "item_action", + "id": "FITNESS_CHECK", + "name": "Check health metrics" + }, { "type": "item_action", "id": "deploy_tent", diff --git a/data/json/itemgroups/Clothing_Gear/clothing.json b/data/json/itemgroups/Clothing_Gear/clothing.json index 13b29c4c2913e..5e1ac65c47932 100644 --- a/data/json/itemgroups/Clothing_Gear/clothing.json +++ b/data/json/itemgroups/Clothing_Gear/clothing.json @@ -337,7 +337,8 @@ { "item": "sf_watch", "prob": 5 }, { "item": "pocketwatch", "prob": 30 }, { "item": "diving_watch", "prob": 20 }, - { "item": "game_watch", "prob": 20 } + { "item": "game_watch", "prob": 20 }, + { "item": "fitness_band", "prob": 10 } ] }, { @@ -514,7 +515,8 @@ { "item": "aspirin", "prob": 30 }, { "item": "water_clean", "prob": 30 }, { "item": "sports_drink", "prob": 30 }, - { "item": "1st_aid", "prob": 20 } + { "item": "1st_aid", "prob": 20 }, + { "item": "fitness_band", "prob": 5 } ] }, { diff --git a/data/json/itemgroups/Locations_MapExtras/locations_commercial.json b/data/json/itemgroups/Locations_MapExtras/locations_commercial.json index efb2b9acc77ba..f8f2af2966310 100644 --- a/data/json/itemgroups/Locations_MapExtras/locations_commercial.json +++ b/data/json/itemgroups/Locations_MapExtras/locations_commercial.json @@ -1824,7 +1824,8 @@ [ "briefs", 30 ], [ "yoghurt", 12 ], [ "vitamins", 10 ], - [ "aspirin", 10 ] + [ "aspirin", 10 ], + [ "fitness_band", 10 ] ] }, { diff --git a/data/json/itemgroups/Locations_MapExtras/mall_item_groups.json b/data/json/itemgroups/Locations_MapExtras/mall_item_groups.json index bc54a0e61efcc..5ee85aaf9d14b 100644 --- a/data/json/itemgroups/Locations_MapExtras/mall_item_groups.json +++ b/data/json/itemgroups/Locations_MapExtras/mall_item_groups.json @@ -257,7 +257,8 @@ [ "cell_phone", 60 ], [ "smart_phone", 60 ], [ "wristwatch", 60 ], - [ "mobile_memory_card_used", 10 ] + [ "mobile_memory_card_used", 10 ], + [ "fitness_band", 5 ] ] }, { diff --git a/data/json/itemgroups/Locations_MapExtras/mansion.json b/data/json/itemgroups/Locations_MapExtras/mansion.json index 70996f84a8b51..407e62dfad610 100644 --- a/data/json/itemgroups/Locations_MapExtras/mansion.json +++ b/data/json/itemgroups/Locations_MapExtras/mansion.json @@ -1022,7 +1022,8 @@ [ "yoghurt", 12 ], [ "vitamins", 10 ], [ "aspirin", 15 ], - { "group": "corpse_mansion", "prob": 1 } + { "group": "corpse_mansion", "prob": 1 }, + [ "fitness_band", 3 ] ] }, { @@ -1103,7 +1104,8 @@ [ "vitamins", 10 ], [ "aspirin", 10 ], [ "karate_gi", 10 ], - [ "judo_gi", 5 ] + [ "judo_gi", 5 ], + [ "fitness_band", 5 ] ] }, { diff --git a/data/json/itemgroups/activities_hobbies.json b/data/json/itemgroups/activities_hobbies.json index af0bc62f08bad..f4b12d9a9c008 100644 --- a/data/json/itemgroups/activities_hobbies.json +++ b/data/json/itemgroups/activities_hobbies.json @@ -113,7 +113,8 @@ [ "survnote", 1 ], [ "halter_top", 30 ], [ "iceaxe", 30 ], - [ "tourist_table", 30 ] + [ "tourist_table", 30 ], + [ "fitness_band", 5 ] ] }, { @@ -163,7 +164,8 @@ { "item": "slingshot", "prob": 10 }, { "item": "wristrocket", "prob": 5 }, { "item": "powered_earmuffs", "prob": 80 }, - { "item": "bandolier_wrist", "prob": 100 } + { "item": "bandolier_wrist", "prob": 100 }, + { "item": "fitness_band", "prob": 5 } ] }, { diff --git a/data/json/items/tool_armor.json b/data/json/items/tool_armor.json index 0ca874791426f..ecc53e280a84e 100644 --- a/data/json/items/tool_armor.json +++ b/data/json/items/tool_armor.json @@ -306,6 +306,24 @@ "use_action": "PORTABLE_GAME", "magazines": [ [ "battery", [ "light_minus_battery_cell", "light_minus_atomic_battery_cell", "light_minus_disposable_cell" ] ] ] }, + { + "type": "TOOL_ARMOR", + "id": "fitness_band", + "name": "fitness band", + "category": "clothing", + "volume": "100 ml", + "description": "A fitness band that can track your heartbeat, exercise level and also has a clock. Activate to see your metrics.", + "weight": "30 g", + "to_hit": -1, + "color": "light_gray", + "covers": [ "HAND_EITHER" ], + "price": 5000, + "material": [ "plastic" ], + "flags": [ "WATCH", "WATER_FRIENDLY", "BELTED", "FRAGILE", "ALLOWS_NATURAL_ATTACKS", "OVERSIZE" ], + "coverage": 15, + "symbol": "[", + "use_action": "FITNESS_CHECK" + }, { "id": "holo_cloak", "type": "TOOL_ARMOR", diff --git a/src/character.cpp b/src/character.cpp index 81819410e258f..98eabb26b2305 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -107,6 +107,7 @@ 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_boomered( "boomered" ); +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" ); @@ -214,11 +215,17 @@ static const bionic_id str_bio_night( "bio_night" ); // Aftershock stuff! 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_EYES( "CEPH_EYES" ); static const trait_id trait_CEPH_VISION( "CEPH_VISION" ); +static const trait_id trait_CHEMIMBALANCE( "CHEMIMBALANCE" ); static const trait_id trait_CHLOROMORPH( "CHLOROMORPH" ); +static const trait_id trait_COLDBLOOD( "COLDBLOOD" ); +static const trait_id trait_COLDBLOOD2( "COLDBLOOD2" ); +static const trait_id trait_COLDBLOOD3( "COLDBLOOD3" ); +static const trait_id trait_COLDBLOOD4( "COLDBLOOD4" ); static const trait_id trait_DEAF( "DEAF" ); static const trait_id trait_DEBUG_CLOAK( "DEBUG_CLOAK" ); static const trait_id trait_DEBUG_LS( "DEBUG_LS" ); @@ -264,6 +271,7 @@ static const trait_id trait_PAWS_LARGE( "PAWS_LARGE" ); static const trait_id trait_PER_SLIME( "PER_SLIME" ); static const trait_id trait_PER_SLIME_OK( "PER_SLIME_OK" ); static const trait_id trait_PROF_FOODP( "PROF_FOODP" ); +static const trait_id trait_QUICK( "QUICK" ); static const trait_id trait_PYROMANIA( "PYROMANIA" ); static const trait_id trait_RADIOGENIC( "RADIOGENIC" ); static const trait_id trait_ROOTS2( "ROOTS2" ); @@ -274,6 +282,7 @@ static const trait_id trait_SHELL( "SHELL" ); static const trait_id trait_SHELL2( "SHELL2" ); static const trait_id trait_SHOUT2( "SHOUT2" ); static const trait_id trait_SHOUT3( "SHOUT3" ); +static const trait_id trait_SLIMESPAWNER( "SLIMESPAWNER" ); static const trait_id trait_SLIMY( "SLIMY" ); static const trait_id trait_STRONGSTOMACH( "STRONGSTOMACH" ); static const trait_id trait_THRESH_CEPHALOPOD( "THRESH_CEPHALOPOD" ); @@ -9405,3 +9414,111 @@ void Character::use_fire( const int quantity ) return; } } + +int Character::heartrate_bpm() const +{ + //Dead have no heartbeat usually and no heartbeat in omnicell + if( is_dead_state() || has_trait( trait_SLIMESPAWNER ) ) { + return 0; + } + //This function returns heartrate in BPM basing of health, physical state, tiredness, + //moral effects, stimulators and anything that should fit here. + //Some values are picked to make sense from math point of view + //and seem correct but effects may vary in real life. + //This needs more attention from experienced contributors to work more smooth. + //Average healthy bpm is 60-80. That's a simple imitation of normal distribution. + //Must a better way to do that. Possibly this value should be generated with player creation. + int average_heartbeat = 70 + rng( -5, 5 ) + rng( -5, 5 ); + //Chemical imbalance makes this less predictable. It's possible this range needs tweaking + if( has_trait( trait_CHEMIMBALANCE ) ) { + average_heartbeat += rng( -15, 15 ); + } + //Quick also raises basic BPM + if( has_trait( trait_QUICK ) ) { + average_heartbeat *= 1.1; + } + //Badtemper makes your BPM raise from anger + if( has_trait( trait_BADTEMPER ) ) { + average_heartbeat *= 1.1; + } + //COLDBLOOD dependencies, works almost same way as temperature effect for speed. + const int player_local_temp = g->weather.get_temperature( pos() ); + float temperature_modifier = 0; + if( has_trait( trait_COLDBLOOD ) ) { + temperature_modifier = 0.002; + } + if( has_trait( trait_COLDBLOOD2 ) ) { + temperature_modifier = 0.00333; + } + if( has_trait( trait_COLDBLOOD3 ) || has_trait( trait_COLDBLOOD4 ) ) { + temperature_modifier = 0.005; + } + average_heartbeat *= 1 + ( ( player_local_temp - 65 ) * temperature_modifier ); + //Limit avg from below with 20, arbitary + average_heartbeat = std::max( 20, average_heartbeat ); + const float stamina_level = static_cast( get_stamina() ) / get_stamina_max(); + float stamina_effect = 0; + if( stamina_level >= 0.9 ) { + stamina_effect = 0; + } else if( stamina_level >= 0.8 ) { + stamina_effect = 0.2; + } else if( stamina_level >= 0.6 ) { + stamina_effect = 0.5; + } else if( stamina_level >= 0.4 ) { + stamina_effect = 1; + } else if( stamina_level >= 0.2 ) { + stamina_effect = 1.5; + } else { + stamina_effect = 2; + } + //can triple heartrate + int heartbeat = average_heartbeat * ( 1 + stamina_effect ); + const int stim_level = get_stim(); + int stim_modifer = 0; + if( stim_level > 0 ) { + //that's asymptotical function that is equal to 1 at around 30 stim level + //and slows down all the time almost reaching 2. + //Tweaking x*x multiplier will accordingly change effect accumulation + stim_modifer = 2 - 2 / ( 1 + 0.001 * stim_level * stim_level ); + } + heartbeat += average_heartbeat * stim_modifer; + if( get_effect_dur( effect_cig ) > 0_turns ) { + //Nicotine-induced tachycardia + if( get_effect_dur( effect_cig ) > 10_minutes * ( addiction_level( ADD_CIG ) + 1 ) ) { + heartbeat += average_heartbeat * 0.4; + } else { + heartbeat += average_heartbeat * 0.1; + } + } + //health effect that can make things better or worse is applied in the end. + //Based on get_max_healthy that already has bmi factored + const int healthy = get_max_healthy(); + //a bit arbitary formula that can use some love + float healthy_modifier = -0.05f * round( healthy / 20.0f ); + heartbeat += average_heartbeat * healthy_modifier; + //Pain simply adds 2% per point after it reaches 5 (that's arbitary) + const int cur_pain = get_perceived_pain(); + float pain_modifier = 0; + if( cur_pain > 5 ) { + pain_modifier = 0.02 * ( cur_pain - 5 ); + } + heartbeat += average_heartbeat * pain_modifier; + //if BPM raised at least by 20% for a player with ADRENALINE, it adds 20% of avg to result + if( has_trait( trait_ADRENALINE ) && heartbeat > average_heartbeat * 1.2 ) { + heartbeat += average_heartbeat * 0.2; + } + //Happy get it bit faster and miserable some more. + //Morale effects might need more consideration + const int morale_level = get_morale_level(); + if( morale_level >= 20 ) { + heartbeat += average_heartbeat * 0.1; + } + if( morale_level <= -20 ) { + heartbeat += average_heartbeat * 0.2; + } + //add fear? + //A single clamp in the end should be enough + const int max_heartbeat = average_heartbeat * 3.5; + heartbeat = clamp( heartbeat, average_heartbeat, max_heartbeat ); + return heartbeat; +} diff --git a/src/character.h b/src/character.h index 712b4433cfb32..891827b281524 100644 --- a/src/character.h +++ b/src/character.h @@ -1897,6 +1897,7 @@ class Character : public Creature, public visitable void drench( int saturation, const body_part_set &flags, bool ignore_waterproof ); /** Recalculates morale penalty/bonus from wetness based on mutations, equipment and temperature */ void apply_wetness_morale( int temperature ); + int heartrate_bpm() const; protected: Character(); diff --git a/src/item_factory.cpp b/src/item_factory.cpp index 25ebed82ccdae..a113c83f2fdc8 100644 --- a/src/item_factory.cpp +++ b/src/item_factory.cpp @@ -873,6 +873,7 @@ void Item_factory::init() add_iuse( "PLANTBLECH", &iuse::plantblech ); add_iuse( "POISON", &iuse::poison ); add_iuse( "PORTABLE_GAME", &iuse::portable_game ); + add_iuse( "FITNESS_CHECK", &iuse::fitness_check ); add_iuse( "PORTAL", &iuse::portal ); add_iuse( "PROZAC", &iuse::prozac ); add_iuse( "PURIFIER", &iuse::purifier ); diff --git a/src/iuse.cpp b/src/iuse.cpp index 70ea0eedcab31..66e9529505c5f 100644 --- a/src/iuse.cpp +++ b/src/iuse.cpp @@ -4448,6 +4448,44 @@ int iuse::portable_game( player *p, item *it, bool, const tripoint & ) return it->type->charges_to_use(); } +int iuse::fitness_check( player *p, item *it, bool, const tripoint & ) +{ + if( p->has_trait( trait_ILLITERATE ) ) { + p->add_msg_if_player( m_info, _( "You don't know what you're looking at." ) ); + return 0; + } else { + //What else should block using f-band? + const int bpm = p->heartrate_bpm(); + p->add_msg_if_player( _( "You check your health metrics on your %s." ), it->tname() ); + //Maybe should pick better words + p->add_msg_if_player( _( "Your %s displays your heart's BPM: %i." ), it->tname(), bpm ); + if( bpm > 179 ) { + p->add_msg_if_player( _( "Your %s shows warning: 'Slow down! " + "Your pulse is getting too high, champion!'" ), it->tname() ); + } + const std::string exercise = p->activity_level_str(); + if( exercise == "NO_EXERCISE" ) { + p->add_msg_if_player( _( "Your %s shows your overall activity: " + "'You are not really active today. Try going for a walk!'." ), it->tname() ); + } else if( exercise == "LIGHT_EXERCISE" ) { + p->add_msg_if_player( _( "Your %s shows your overall activity: " + "'Good start! Keep it up and move more.'" ), it->tname() ); + } else if( exercise == "MODERATE_EXERCISE" ) { + p->add_msg_if_player( _( "Your %s shows your overall activity: " + "'Doing good! Don't stop, push the limit!'" ), it->tname() ); + } else if( exercise == "ACTIVE_EXERCISE" ) { + //Ad will most likely need to go + p->add_msg_if_player( _( "Your %s shows your overall activity: 'Great job! " + "Take a break from workout and refresh with a bottle of sport drink!'" ), it->tname() ); + } else { + p->add_msg_if_player( _( "Your %s shows your overall activity: " + "'You are too active! Avoid overexertion for your safety and health.'" ), it->tname() ); + } + //TODO add whatever else makes sense (sleep quality, health level approximation?) + } + return it->type->charges_to_use(); +} + int iuse::hand_crank( player *p, item *it, bool, const tripoint & ) { if( p->is_npc() ) { diff --git a/src/iuse.h b/src/iuse.h index d3801b077b4f1..219eaa7d87d60 100644 --- a/src/iuse.h +++ b/src/iuse.h @@ -138,6 +138,7 @@ class iuse int dive_tank( player *, item *, bool, const tripoint & ); int gasmask( player *, item *, bool, const tripoint & ); int portable_game( player *, item *, bool, const tripoint & ); + int fitness_check( player *p, item *it, bool, const tripoint & ); int vibe( player *, item *, bool, const tripoint & ); int hand_crank( player *, item *, bool, const tripoint & ); int vortex( player *, item *, bool, const tripoint & );