From ac8bae17a005027a80df60fe6cf2547825c85d8c Mon Sep 17 00:00:00 2001 From: Kevin Granade Date: Mon, 9 Nov 2020 01:03:49 -0800 Subject: [PATCH] Extract activity and weariness tracking to a helper class. --- src/activity_tracker.cpp | 155 ++++++++++++++++++++++++ src/activity_tracker.h | 54 +++++++++ src/avatar_action.cpp | 2 +- src/character.cpp | 169 +++++---------------------- src/character.h | 38 +----- src/game.cpp | 6 - src/handle_action.cpp | 2 +- src/iexamine.cpp | 4 +- src/melee.cpp | 4 +- src/player_activity.cpp | 4 +- src/savegame_json.cpp | 25 +++- src/suffer.cpp | 2 +- src/vehicle.cpp | 2 +- tests/activity_scheduling_helper.cpp | 6 +- tests/activity_tracker_test.cpp | 112 ++++++++++++++++++ tests/char_biometrics_test.cpp | 90 +------------- tests/stomach_contents_test.cpp | 4 + 17 files changed, 397 insertions(+), 282 deletions(-) create mode 100644 src/activity_tracker.cpp create mode 100644 src/activity_tracker.h create mode 100644 tests/activity_tracker_test.cpp diff --git a/src/activity_tracker.cpp b/src/activity_tracker.cpp new file mode 100644 index 0000000000000..559181bc31d35 --- /dev/null +++ b/src/activity_tracker.cpp @@ -0,0 +1,155 @@ +#include "activity_tracker.h" + +#include "game_constants.h" +#include "options.h" +#include "string_formatter.h" + +int activity_tracker::weariness() const +{ + if( intake > tracker ) { + return tracker * 0.5; + } + return tracker - intake * 0.5; +} + +// Called every 5 minutes, when activity level is logged +void activity_tracker::try_reduce_weariness( int bmr, bool sleeping ) +{ + tick_counter++; + if( average_activity() - NO_EXERCISE <= std::numeric_limits::epsilon() ) { + low_activity_ticks++; + // Recover twice as fast at rest + if( sleeping ) { + low_activity_ticks++; + } + } + + const float recovery_mult = get_option( "WEARY_RECOVERY_MULT" ); + + if( low_activity_ticks >= 6 ) { + int reduction = tracker; + // 1/20 of whichever's bigger + if( bmr > reduction ) { + reduction = bmr * recovery_mult; + } else { + reduction *= recovery_mult; + } + low_activity_ticks -= 6; + + tracker -= reduction; + } + + if( tick_counter >= 12 ) { + intake *= 1 - recovery_mult; + tick_counter -= 12; + } + + // Normalize values, make sure we stay above 0 + intake = std::max( intake, 0 ); + tracker = std::max( tracker, 0 ); + tick_counter = std::max( tick_counter, 0 ); + low_activity_ticks = std::max( low_activity_ticks, 0 ); +} + +void activity_tracker::weary_clear() +{ + tracker = 0; + intake = 0; + low_activity_ticks = 0; + tick_counter = 0; +} + +std::string activity_tracker::debug_weary_info() const +{ + return string_format( "Intake: %d Tracker: %d", intake, tracker ); +} + +void activity_tracker::calorie_adjust( int nkcal ) +{ + if( nkcal > 0 ) { + intake += nkcal; + } else { + // nkcal is negative, we need positive + tracker -= nkcal; + } +} + +float activity_tracker::activity() const +{ + if( current_turn == calendar::turn ) { + return current_activity; + } + return 1.0f; +} + +float activity_tracker::average_activity() const +{ + if( activity_reset && current_turn != calendar::turn ) { + return previous_activity / num_events; + } + return ( accumulated_activity + current_activity ) / num_events; +} + +float activity_tracker::instantaneous_activity_level() const +{ + if( current_turn == calendar::turn ) { + return current_activity; + } + return previous_turn_activity; +} + +// The idea here is the character is going about their business logging activities, +// and log_activity() handles sorting them out, it records the largest magnitude for a given turn, +// and then rolls the previous turn's value into the accumulator once a new activity is logged. +// After a reset, we have to pretend the previous values weren't logged. +void activity_tracker::log_activity( float new_level ) +{ + current_activity = std::max( current_activity, new_level ); + current_turn = calendar::turn; +} + +void activity_tracker::new_turn() +{ + if( activity_reset ) { + activity_reset = false; + previous_turn_activity = current_activity; + current_activity = NO_EXERCISE; + accumulated_activity = 0.0f; + num_events = 1; + } else { + // This is for the last turn that had activity logged. + accumulated_activity += current_activity; + // Then handle the interventing turns that had no activity logged. + int num_turns = to_turns( calendar::turn - current_turn ); + if( num_turns > 1 ) { + accumulated_activity += ( num_turns - 1 ) * NO_EXERCISE; + num_events += num_turns - 1; + } + previous_turn_activity = current_activity; + current_activity = NO_EXERCISE; + num_events++; + } +} + +void activity_tracker::reset_activity_level() +{ + previous_activity = accumulated_activity; + activity_reset = true; +} + +std::string activity_tracker::activity_level_str() const +{ + if( current_activity <= NO_EXERCISE ) { + return _( "NO_EXERCISE" ); + } else if( current_activity <= LIGHT_EXERCISE ) { + return _( "LIGHT_EXERCISE" ); + } else if( current_activity <= MODERATE_EXERCISE ) { + return _( "MODERATE_EXERCISE" ); + } else if( current_activity <= BRISK_EXERCISE ) { + return _( "BRISK_EXERCISE" ); + } else if( current_activity <= ACTIVE_EXERCISE ) { + return _( "ACTIVE_EXERCISE" ); + } else { + return _( "EXTRA_EXERCISE" ); + } +} diff --git a/src/activity_tracker.h b/src/activity_tracker.h new file mode 100644 index 0000000000000..45a0b195dd696 --- /dev/null +++ b/src/activity_tracker.h @@ -0,0 +1,54 @@ +#pragma once +#ifndef CATA_SRC_ACTIVITY_TRACKER_H +#define CATA_SRC_ACTIVITY_TRACKER_H + +#include "calendar.h" + +class JsonIn; +class JsonOut; + +class activity_tracker +{ + private: + float current_activity = 0.0; + float accumulated_activity = 0.0; + float previous_activity = 0.0; + float previous_turn_activity = 0.0; + time_point current_turn = calendar::turn_zero; + bool activity_reset = false; + int num_events = 0; + + // Weariness metadata. + int tracker = 0; + int intake = 0; + // Semi-consecutive 5 minute ticks of low activity (or 2.5 if we're sleeping) + int low_activity_ticks = 0; + // How many ticks since we've decreased intake + int tick_counter = 0; + public: + // Logs activity level. If called multiple times in one turn, will preserve the highest. + void log_activity( float new_level ); + // Informs the tracker that a new turn has started. + void new_turn(); + // Resets accumulated activity level. + void reset_activity_level(); + // outputs player activity level to a printable string + std::string activity_level_str() const; + // Returns activity level recorded for the current turn. + float activity() const; + // Returns average of activity level for the current period. + float average_activity() const; + // Returns the previous turn's activity level until an action is tanken on the current turn. + float instantaneous_activity_level() const; + + int weariness() const; + void try_reduce_weariness( int bmr, bool sleeping ); + void calorie_adjust( int nkcal ); + void weary_clear(); + std::string debug_weary_info() const; + + void serialize( JsonOut &json ) const; + void deserialize( JsonIn &jsin ); +}; + +#endif // CATA_SRC_ACTIVITY_TRACKER_H diff --git a/src/avatar_action.cpp b/src/avatar_action.cpp index ff490f03daf8d..9cdddc7d00606 100644 --- a/src/avatar_action.cpp +++ b/src/avatar_action.cpp @@ -144,7 +144,7 @@ bool avatar_action::move( avatar &you, map &m, const tripoint &d ) // by this point we're either walking, running, crouching, or attacking, so update the activity level to match if( !is_riding ) { - you.increase_activity_level( you.current_movement_mode()->exertion_level() ); + you.set_activity_level( you.current_movement_mode()->exertion_level() ); } // If the player is *attempting to* move on the X axis, update facing direction of their sprite to match. diff --git a/src/character.cpp b/src/character.cpp index bf0b35423cc9e..681ec5e2aeef3 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -4237,7 +4237,7 @@ void Character::normalize() { Creature::normalize(); - weary.clear(); + activity_history.weary_clear(); martial_arts_data->reset_style(); weapon = item( "null", calendar::turn_zero ); @@ -5073,32 +5073,23 @@ static std::string exert_lvl_to_str( float level ) } std::string Character::debug_weary_info() const { - int amt = weariness(); + int amt = activity_history.weariness(); std::string max_act = exert_lvl_to_str( maximum_exertion_level() ); float move_mult = exertion_adjusted_move_multiplier( EXTRA_EXERCISE ); int bmr = base_bmr(); - int intake = weary.intake; - int input = weary.tracker; + std::string weary_internals = activity_history.debug_weary_info(); int thresh = weary_threshold(); int current = weariness_level(); int morale = get_morale_level(); int weight = units::to_gram( bodyweight() ); float bmi = get_bmi(); - return string_format( "Weariness: %s Max Full Exert: %s Mult: %g\nBMR: %d Intake: %d Tracker: %d Thresh: %d At: %d\nCal: %d/%d Fatigue: %d Morale: %d Wgt: %d (BMI %.1f)", - amt, max_act, move_mult, bmr, intake, input, thresh, current, get_stored_kcal(), + return string_format( "Weariness: %s Max Full Exert: %s Mult: %g\nBMR: %d %s Thresh: %d At: %d\nCal: %d/%d Fatigue: %d Morale: %d Wgt: %d (BMI %.1f)", + amt, max_act, move_mult, bmr, weary_internals, thresh, current, get_stored_kcal(), get_healthy_kcal(), fatigue, morale, weight, bmi ); } -void weariness_tracker::clear() -{ - tracker = 0; - intake = 0; - low_activity_ticks = 0; - tick_counter = 0; -} - void Character::mod_stored_kcal( int nkcal, const bool ignore_weariness ) { const bool npc_no_food = is_npc() && get_option( "NO_NPC_FOOD" ); @@ -5112,15 +5103,12 @@ void Character::mod_stored_calories( int ncal, const bool ignore_weariness ) int nkcal = ncal / 1000; if( nkcal > 0 ) { add_gained_calories( nkcal ); - if( !ignore_weariness ) { - weary.intake += nkcal; - } } else { add_spent_calories( -nkcal ); - // nkcal is negative, we need positive - if( !ignore_weariness ) { - weary.tracker -= nkcal; - } + } + + if( !ignore_weariness ) { + activity_history.calorie_adjust( nkcal ); } set_stored_calories( stored_calories + ncal ); } @@ -5542,19 +5530,17 @@ void Character::update_body( const time_point &from, const time_point &to ) } const int five_mins = ticks_between( from, to, 5_minutes ); if( five_mins > 0 ) { - try_reduce_weariness( attempted_activity_level ); - if( !activity.is_null() ) { - decrease_activity_level( activity.exertion_level() ); - } else { - reset_activity_level(); - } + 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(); } @@ -5610,6 +5596,11 @@ item *Character::best_quality_item( const quality_id &qual ) return best_qual; } +int Character::weariness() const +{ + return activity_history.weariness(); +} + int Character::weary_threshold() const { const int bmr = base_bmr(); @@ -5623,13 +5614,6 @@ int Character::weary_threshold() const return std::max( threshold, bmr / 10 ); } -int Character::weariness() const -{ - if( weary.intake > weary.tracker ) { - return weary.tracker * 0.5; - } - return weary.tracker - weary.intake * 0.5; -} std::pair Character::weariness_transition_progress() const { @@ -5651,7 +5635,7 @@ std::pair Character::weariness_transition_progress() const int Character::weariness_level() const { - int amount = weariness(); + int amount = activity_history.weariness(); int threshold = weary_threshold(); int level = 0; amount -= threshold * get_option( "WEARY_INITIAL_STEP" ); @@ -5691,7 +5675,7 @@ float Character::exertion_adjusted_move_multiplier( float level ) const // And any values we get that are negative or 0 // will cause incorrect behavior if( level <= 0 ) { - level = attempted_activity_level; + level = activity_history.activity(); } const float max = maximum_exertion_level(); if( level < max ) { @@ -5700,88 +5684,16 @@ float Character::exertion_adjusted_move_multiplier( float level ) const return max / level; } -// Called every 5 minutes, when activity level is logged -void Character::try_reduce_weariness( const float exertion ) -{ - weary.tick_counter++; - if( exertion == NO_EXERCISE ) { - weary.low_activity_ticks++; - // Recover twice as fast at rest - if( in_sleep_state() ) { - weary.low_activity_ticks++; - } - } - - const float recovery_mult = get_option( "WEARY_RECOVERY_MULT" ); - - if( weary.low_activity_ticks >= 6 ) { - int reduction = weary.tracker; - const int bmr = base_bmr(); - // 1/20 of whichever's bigger - if( bmr > reduction ) { - reduction = bmr * recovery_mult; - } else { - reduction *= recovery_mult; - } - weary.low_activity_ticks = 0; - - weary.tracker -= reduction; - } - - if( weary.tick_counter >= 12 ) { - weary.intake *= 1 - recovery_mult; - weary.tick_counter = 0; - } - - // Normalize values, make sure we stay above 0 - weary.intake = std::max( weary.intake, 0 ); - weary.tracker = std::max( weary.tracker, 0 ); - weary.tick_counter = std::max( weary.tick_counter, 0 ); - weary.low_activity_ticks = std::max( weary.low_activity_ticks, 0 ); -} - -// Remove all this instantaneous stuff when activity tracking moves to per turn float Character::instantaneous_activity_level() const { - // As this is for display purposes, we want to show last turn's activity. - if( calendar::turn > act_turn ) { - return act_cursor; - } else { - return last_act; - } -} - -// Basically, advance one turn -void Character::reset_activity_cursor() -{ - - if( calendar::turn > act_turn ) { - last_act = act_cursor; - act_cursor = NO_EXERCISE; - act_turn = calendar::turn; - } else { - act_cursor = NO_EXERCISE; - } -} - -// Log the highest activity level for this turn, and advance one turn if needed -void Character::log_instant_activity( float level ) -{ - if( calendar::turn > act_turn ) { - reset_activity_cursor(); - act_cursor = level; - } else if( level > act_cursor ) { - act_cursor = level; - } + return activity_history.instantaneous_activity_level(); } float Character::activity_level() const { float max = maximum_exertion_level(); - if( attempted_activity_level > max ) { - return max; - } - return attempted_activity_level; + float attempted_level = activity_history.activity(); + return std::min( max, attempted_level ); } void Character::update_stomach( const time_point &from, const time_point &to ) @@ -5810,7 +5722,7 @@ void Character::update_stomach( const time_point &from, const time_point &to ) mod_stored_kcal( digested_to_body.nutr.kcal() ); vitamins_mod( digested_to_body.nutr.vitamins, false ); - log_activity_level( activity_level() ); + log_activity_level( activity_history.average_activity() ); if( !foodless && rates.hunger > 0.0f ) { mod_hunger( roll_remainder( rates.hunger * five_mins ) ); @@ -8463,46 +8375,23 @@ int Character::base_bmr() const int Character::get_bmr() const { int base_bmr_calc = base_bmr(); - base_bmr_calc *= activity_level(); + base_bmr_calc *= std::min( activity_history.average_activity(), maximum_exertion_level() ); return std::ceil( enchantment_cache->modify_value( enchant_vals::mod::METABOLISM, base_bmr_calc ) ); } -void Character::increase_activity_level( float new_level ) +void Character::set_activity_level( float new_level ) { - if( attempted_activity_level < new_level ) { - attempted_activity_level = new_level; - } - log_instant_activity( new_level ); + activity_history.log_activity( new_level ); } -void Character::decrease_activity_level( float new_level ) -{ - if( attempted_activity_level > new_level ) { - attempted_activity_level = new_level; - } - log_instant_activity( new_level ); -} void Character::reset_activity_level() { - attempted_activity_level = NO_EXERCISE; - reset_activity_cursor(); + activity_history.reset_activity_level(); } std::string Character::activity_level_str() const { - if( attempted_activity_level <= NO_EXERCISE ) { - return _( "NO_EXERCISE" ); - } else if( attempted_activity_level <= LIGHT_EXERCISE ) { - return _( "LIGHT_EXERCISE" ); - } else if( attempted_activity_level <= MODERATE_EXERCISE ) { - return _( "MODERATE_EXERCISE" ); - } else if( attempted_activity_level <= BRISK_EXERCISE ) { - return _( "BRISK_EXERCISE" ); - } else if( attempted_activity_level <= ACTIVE_EXERCISE ) { - return _( "ACTIVE_EXERCISE" ); - } else { - return _( "EXTRA_EXERCISE" ); - } + return activity_history.activity_level_str(); } int Character::get_armor_bash( bodypart_id bp ) const diff --git a/src/character.h b/src/character.h index e417895b8d257..c36cc59a2cb33 100644 --- a/src/character.h +++ b/src/character.h @@ -21,6 +21,7 @@ #include #include +#include "activity_tracker.h" #include "activity_type.h" #include "bodypart.h" #include "calendar.h" @@ -294,20 +295,6 @@ struct consumption_event { void deserialize( JsonIn &jsin ); }; -struct weariness_tracker { - int tracker = 0; - int intake = 0; - - // Semi-consecutive 5 minute ticks of low activity (or 2.5 if we're sleeping) - int low_activity_ticks = 0; - // How many ticks since we've decreased intake - int tick_counter = 0; - - void clear(); - void serialize( JsonOut &json ) const; - void deserialize( JsonIn &jsin ); -}; - inline social_modifiers operator+( social_modifiers lhs, const social_modifiers &rhs ) { lhs += rhs; @@ -2138,24 +2125,16 @@ class Character : public Creature, public visitable units::mass bodyweight() const; // returns total weight of installed bionics units::mass bionics_weight() const; - // increases the activity level to the next level + // increases the activity level to the specified level // does not decrease activity level - void increase_activity_level( float new_level ); - // decreases the activity level to the previous level + void set_activity_level( float new_level ); + // decreases activity level to the specified level // does not increase activity level void decrease_activity_level( float new_level ); // sets activity level to NO_EXERCISE void reset_activity_level(); // outputs player activity level to a printable string std::string activity_level_str() const; - // NOT SUITABLE FOR USE OTHER THAN DISPLAY - // The activity level this turn - float instantaneous_activity_level() const; - // Basically, advance this display one turn - void reset_activity_cursor(); - // When we log an activity for metabolic purposes - // log it in our cursor too - void log_instant_activity( float ); /** Returns overall bashing resistance for the body_part */ int get_armor_bash( bodypart_id bp ) const override; @@ -2719,8 +2698,8 @@ class Character : public Creature, public visitable int weary_threshold() const; int weariness() const; float activity_level() const; + float instantaneous_activity_level() const; float exertion_adjusted_move_multiplier( float level = -1.0f ) const; - void try_reduce_weariness( float exertion ); float maximum_exertion_level() const; std::string debug_weary_info() const; // returns empty because this is avatar specific @@ -2782,7 +2761,6 @@ class Character : public Creature, public visitable int healthy = 0; int healthy_mod = 0; - weariness_tracker weary; // Our bmr at no activity level int base_bmr() const; @@ -2794,11 +2772,7 @@ class Character : public Creature, public visitable creature_size size_class = creature_size::medium; // the player's activity level for metabolism calculations - float attempted_activity_level = NO_EXERCISE; - // Display purposes only - the highest activity this turn and last - float act_cursor = NO_EXERCISE; - float last_act = NO_EXERCISE; - time_point act_turn = calendar::turn_zero; + activity_tracker activity_history; trap_map known_traps; mutable std::map cached_info; diff --git a/src/game.cpp b/src/game.cpp index cb5b0f38c45c1..136f9a5b9f111 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -1675,12 +1675,6 @@ bool game::do_turn() // reset player noise u.volume = 0; - // This is a hack! Remove this when we have per-turn activity tracking - // This prevents the display from erroneously updating when we use more - // than our allotted moves in a single turn - if( u.moves < 0 ) { - u.increase_activity_level( NO_EXERCISE ); - } return false; } diff --git a/src/handle_action.cpp b/src/handle_action.cpp index f253d5f473d5f..dc16dbdecba6e 100644 --- a/src/handle_action.cpp +++ b/src/handle_action.cpp @@ -791,7 +791,7 @@ static void smash() float weary_mult = 1.0f; if( didit ) { if( !mech_smash ) { - player_character.increase_activity_level( MODERATE_EXERCISE ); + player_character.set_activity_level( MODERATE_EXERCISE ); player_character.handle_melee_wear( player_character.weapon ); weary_mult = 1.0f / player_character.exertion_adjusted_move_multiplier( MODERATE_EXERCISE ); diff --git a/src/iexamine.cpp b/src/iexamine.cpp index 198c93c1e38cd..a2eb9668d11a2 100644 --- a/src/iexamine.cpp +++ b/src/iexamine.cpp @@ -4639,7 +4639,7 @@ void iexamine::ledge( player &p, const tripoint &examp ) add_msg( m_warning, _( "You are not going to jump over an obstacle only to fall down." ) ); } else { add_msg( m_info, _( "You jump over an obstacle." ) ); - p.increase_activity_level( LIGHT_EXERCISE ); + p.set_activity_level( LIGHT_EXERCISE ); g->place_player( dest ); } break; @@ -4680,7 +4680,7 @@ void iexamine::ledge( player &p, const tripoint &examp ) return; } else if( height == 1 ) { const char *query; - p.increase_activity_level( MODERATE_EXERCISE ); + p.set_activity_level( MODERATE_EXERCISE ); weary_mult = 1.0f / p.exertion_adjusted_move_multiplier( MODERATE_EXERCISE ); if( !has_grapnel ) { diff --git a/src/melee.cpp b/src/melee.cpp index d97cf104a3df4..2cb96b7e050fd 100644 --- a/src/melee.cpp +++ b/src/melee.cpp @@ -514,7 +514,7 @@ bool Character::melee_attack_abstract( Creature &t, bool allow_special, } // Fighting is hard work - increase_activity_level( EXTRA_EXERCISE ); + set_activity_level( EXTRA_EXERCISE ); item *cur_weapon = allow_unarmed ? &used_weapon() : &weapon; @@ -791,7 +791,7 @@ void Character::reach_attack( const tripoint &p ) } // Fighting is hard work - increase_activity_level( EXTRA_EXERCISE ); + set_activity_level( EXTRA_EXERCISE ); Creature *critter = g->critter_at( p ); // Original target size, used when there are monsters in front of our target diff --git a/src/player_activity.cpp b/src/player_activity.cpp index 3a46ef8a67711..e244684c4ef00 100644 --- a/src/player_activity.cpp +++ b/src/player_activity.cpp @@ -240,7 +240,7 @@ void player_activity::do_turn( player &p ) } } } - const float activity_mult = p.exertion_adjusted_move_multiplier(); + const float activity_mult = p.exertion_adjusted_move_multiplier( exertion_level() ); if( type->based_on() == based_on_type::TIME ) { if( moves_left >= 100 ) { moves_left -= 100 * activity_mult; @@ -268,7 +268,7 @@ void player_activity::do_turn( player &p ) return; } const bool travel_activity = id() == ACT_TRAVELLING; - p.increase_activity_level( exertion_level() ); + p.set_activity_level( exertion_level() ); // This might finish the activity (set it to null) if( actor ) { actor->do_turn( *this, p ); diff --git a/src/savegame_json.cpp b/src/savegame_json.cpp index 8e17933d6facd..c7536d389ed04 100644 --- a/src/savegame_json.cpp +++ b/src/savegame_json.cpp @@ -496,9 +496,16 @@ void consumption_event::deserialize( JsonIn &jsin ) jo.read( "component_hash", component_hash ); } -void weariness_tracker::serialize( JsonOut &json ) const +void activity_tracker::serialize( JsonOut &json ) const { json.start_object(); + json.member( "current_activity", current_activity ); + json.member( "accumulated_activity", accumulated_activity ); + json.member( "previous_activity", previous_activity ); + json.member( "current_turn", current_turn ); + json.member( "activity_reset", activity_reset ); + json.member( "num_events", num_events ); + json.member( "tracker", tracker ); json.member( "intake", intake ); json.member( "low_activity_ticks", low_activity_ticks ); @@ -506,10 +513,18 @@ void weariness_tracker::serialize( JsonOut &json ) const json.end_object(); } -void weariness_tracker::deserialize( JsonIn &jsin ) +void activity_tracker::deserialize( JsonIn &jsin ) { JsonObject jo = jsin.get_object(); + jo.allow_omitted_members(); + jo.read( "current_activity", current_activity ); + jo.read( "accumulated_activity", accumulated_activity ); + jo.read( "previous_activity", previous_activity ); + jo.read( "current_turn", current_turn ); + jo.read( "activity_reset", activity_reset ); + jo.read( "num_events", num_events ); + jo.read( "tracker", tracker ); jo.read( "intake", intake ); jo.read( "low_activity_ticks", low_activity_ticks ); @@ -570,7 +585,9 @@ void Character::load( const JsonObject &data ) data.read( "thirst", thirst ); data.read( "hunger", hunger ); data.read( "fatigue", fatigue ); - data.read( "weary", weary ); + // Legacy read, remove after 0.F + data.read( "weary", activity_history ); + data.read( "activity_history", activity_history ); data.read( "sleep_deprivation", sleep_deprivation ); data.read( "stored_calories", stored_calories ); // stored_calories was changed from being in kcal to being in just cal @@ -959,7 +976,7 @@ void Character::store( JsonOut &json ) const json.member( "thirst", thirst ); json.member( "hunger", hunger ); json.member( "fatigue", fatigue ); - json.member( "weary", weary ); + json.member( "activity_history", activity_history ); json.member( "sleep_deprivation", sleep_deprivation ); json.member( "stored_calories", stored_calories ); json.member( "radiation", radiation ); diff --git a/src/suffer.cpp b/src/suffer.cpp index 0c106371c4a8f..1a86826ae5df4 100644 --- a/src/suffer.cpp +++ b/src/suffer.cpp @@ -1386,7 +1386,7 @@ void Character::suffer_from_exertion() // Significantly slow the rate of messaging when in an activity const int chance = activity ? to_turns( 48_minutes ) : to_turns( 5_minutes ); - if( attempted_activity_level > max_activity && one_in( chance ) && !in_sleep_state() ) { + if( activity_history.activity() > max_activity && one_in( chance ) && !in_sleep_state() ) { add_msg_if_player( m_bad, _( "You're tiring out; continuing to work at this rate will be slower." ) ); } diff --git a/src/vehicle.cpp b/src/vehicle.cpp index e670fc8d0679f..1ae966b4973f2 100644 --- a/src/vehicle.cpp +++ b/src/vehicle.cpp @@ -4650,7 +4650,7 @@ void vehicle::consume_fuel( int load, bool idling ) } // we want this to update the activity level whenever we're using muscle power to move if( load > 0 && fuel_left( fuel_type_muscle ) > 0 ) { - player_character.increase_activity_level( ACTIVE_EXERCISE ); + player_character.set_activity_level( ACTIVE_EXERCISE ); //do this as a function of current load // But only if the player is actually there! int eff_load = load / 10; diff --git a/tests/activity_scheduling_helper.cpp b/tests/activity_scheduling_helper.cpp index b951bafee19bd..bcebf9ec4ba51 100644 --- a/tests/activity_scheduling_helper.cpp +++ b/tests/activity_scheduling_helper.cpp @@ -103,14 +103,14 @@ weariness_events do_activity( tasklist tasks ) // How many turn's we've been at it time_duration turns = 0_seconds; while( turns <= task.interval && !task.instantaneous() ) { - // Start each turn with a fresh set of moves - guy.moves = 100; - task.do_turn( guy ); // Advance a turn calendar::turn += 1_turns; turns += 1_seconds; // Consume food, become weary, etc guy.update_body(); + // Start each turn with a fresh set of moves + guy.moves = 100; + task.do_turn( guy ); } // Cancel our activity, now that we're done guy.cancel_activity(); diff --git a/tests/activity_tracker_test.cpp b/tests/activity_tracker_test.cpp new file mode 100644 index 0000000000000..4623accdb5743 --- /dev/null +++ b/tests/activity_tracker_test.cpp @@ -0,0 +1,112 @@ +#include "catch/catch.hpp" + +#include "activity_tracker.h" +#include "calendar.h" +#include "game_constants.h" +#include "rng.h" + +#include + +static void test_activity_tracker( const std::vector &values ) +{ + activity_tracker tracker; + for( float i : values ) { + calendar::turn += 1_turns; + tracker.new_turn(); + // If we're on a "new turn", we should have nominal activity. + CHECK( tracker.activity() == 1.0 ); + // Smaller values inserted before and after the highest value should be irrelevant. + tracker.log_activity( rng_float( 0.0f, i - 0.01f ) ); + tracker.log_activity( i ); + tracker.log_activity( rng_float( 0.0f, i - 0.01f ) ); + // Verify the highest value inerted is the current value. + CHECK( tracker.activity() == i ); + } + int end_value = values.back(); + // activity() still returns most recently logged activity. + CHECK( tracker.activity() == end_value ); + const float expected_activity = std::accumulate( values.begin(), values.end(), 0.0f ) / + static_cast( values.size() ); + // average_activity() returns average of the most recent period. + CHECK( tracker.average_activity() == expected_activity ); + tracker.reset_activity_level(); + // activity() should be unchanged after a reset. (it's still the same turn) + CHECK( tracker.activity() == end_value ); + // average_activity() also continues to return the previous value. + CHECK( tracker.average_activity() == expected_activity ); + calendar::turn += 1_turns; + // activity() returns 1.0 now that it's a new turn. + CHECK( tracker.activity() == 1.0f ); + tracker.new_turn(); + tracker.log_activity( 5.0f ); + // After starting a new recording cycle, activity() and average_activity() return the new data. + CHECK( tracker.activity() == 5.0f ); + CHECK( tracker.average_activity() == 5.0f ); + calendar::turn += 1_turns; + tracker.new_turn(); + tracker.log_activity( 7.0f ); + // And the behavior continues. + CHECK( tracker.activity() == 7.0f ); + CHECK( tracker.average_activity() == 6.0f ); + calendar::turn = calendar::turn_zero; +} + +TEST_CASE( "activity_counter_from_1_to_300", "[activity_tracker]" ) +{ + std::vector values; + for( int i = 1; i <= 300; ++i ) { + values.push_back( i ); + } + test_activity_tracker( values ); +} + +TEST_CASE( "activity_tracker_from_300_to_1", "[activity_tracker]" ) +{ + std::vector values; + for( int i = 300; i > 0; --i ) { + values.push_back( i ); + } + test_activity_tracker( values ); +} + +TEST_CASE( "activity_tracker_constant_value", "[activity_tracker]" ) +{ + std::vector values( 300, 1.0 ); + test_activity_tracker( values ); +} + +TEST_CASE( "activity_tracker_intermittent_values", "[activity_tracker]" ) +{ + std::vector values( 300, 1.0 ); + for( int i = 0; i < 300; i += 30 ) { + values[ i ] = 10.0f; + } + test_activity_tracker( values ); +} + +TEST_CASE( "activity_tracker_string_representation", "[activity_tracker]" ) +{ + activity_tracker tracker; + // Start at the lowest level + tracker.reset_activity_level(); + REQUIRE( tracker.activity_level_str() == "NO_EXERCISE" ); + + // Increase level a couple times + tracker.log_activity( LIGHT_EXERCISE ); + CHECK( tracker.activity_level_str() == "LIGHT_EXERCISE" ); + tracker.log_activity( MODERATE_EXERCISE ); + CHECK( tracker.activity_level_str() == "MODERATE_EXERCISE" ); + // Cannot 'increase' to lower level + tracker.log_activity( LIGHT_EXERCISE ); + CHECK( tracker.activity_level_str() == "MODERATE_EXERCISE" ); + tracker.log_activity( NO_EXERCISE ); + CHECK( tracker.activity_level_str() == "MODERATE_EXERCISE" ); + // Increase to highest level + tracker.log_activity( ACTIVE_EXERCISE ); + CHECK( tracker.activity_level_str() == "ACTIVE_EXERCISE" ); + tracker.log_activity( EXTRA_EXERCISE ); + CHECK( tracker.activity_level_str() == "EXTRA_EXERCISE" ); + // Cannot increase beyond the highest + tracker.log_activity( EXTRA_EXERCISE ); + CHECK( tracker.activity_level_str() == "EXTRA_EXERCISE" ); +} diff --git a/tests/char_biometrics_test.cpp b/tests/char_biometrics_test.cpp index 8d222d3fb395a..5e9a395d91658 100644 --- a/tests/char_biometrics_test.cpp +++ b/tests/char_biometrics_test.cpp @@ -86,7 +86,8 @@ static float metabolic_rate_with_mutation( player &dummy, const std::string &tra static int bmr_at_act_level( player &dummy, float activity_level ) { dummy.reset_activity_level(); - dummy.increase_activity_level( activity_level ); + dummy.update_body( calendar::turn, calendar::turn ); + dummy.set_activity_level( activity_level ); return dummy.get_bmr(); } @@ -415,89 +416,6 @@ TEST_CASE( "size and height determine body weight", "[biometrics][bodyweight]" ) } -TEST_CASE( "activity level reset, increase and decrease", "[biometrics][activity]" ) -{ - // Activity level is a floating-point number, but only set to discrete values: - // - // NO_EXERCISE = 1.2f; - // LIGHT_EXERCISE = 2.0f; - // MODERATE_EXERCISE = 4.5f; - // ACTIVE_EXERCISE = 8.0f; - // EXTRA_EXERCISE = 10.0f; - - // Functions tested: - // activity_level_str (return string constant for each range) - // reset_activity_level (to NO_EXERCISE) - // increase_activity_level (only if greater than current) - // decrease_activity_level (only if less than current) - - avatar dummy; - - SECTION( "reset activity level to NO_EXERCISE" ) { - // Start at no exercise - dummy.reset_activity_level(); - CHECK( dummy.activity_level_str() == "NO_EXERCISE" ); - // Increase and confirm - dummy.increase_activity_level( MODERATE_EXERCISE ); - CHECK( dummy.activity_level_str() == "MODERATE_EXERCISE" ); - // Reset back and ensure it worked - dummy.reset_activity_level(); - CHECK( dummy.activity_level_str() == "NO_EXERCISE" ); - } - - SECTION( "increase_activity_level" ) { - // Start at the lowest level - dummy.reset_activity_level(); - REQUIRE( dummy.activity_level_str() == "NO_EXERCISE" ); - - // Increase level a couple times - dummy.increase_activity_level( LIGHT_EXERCISE ); - CHECK( dummy.activity_level_str() == "LIGHT_EXERCISE" ); - dummy.increase_activity_level( MODERATE_EXERCISE ); - CHECK( dummy.activity_level_str() == "MODERATE_EXERCISE" ); - // Cannot 'increase' to lower level - dummy.increase_activity_level( LIGHT_EXERCISE ); - CHECK( dummy.activity_level_str() == "MODERATE_EXERCISE" ); - dummy.increase_activity_level( NO_EXERCISE ); - CHECK( dummy.activity_level_str() == "MODERATE_EXERCISE" ); - // Increase to highest level - dummy.increase_activity_level( ACTIVE_EXERCISE ); - CHECK( dummy.activity_level_str() == "ACTIVE_EXERCISE" ); - dummy.increase_activity_level( EXTRA_EXERCISE ); - CHECK( dummy.activity_level_str() == "EXTRA_EXERCISE" ); - // Cannot increase beyond the highest - dummy.increase_activity_level( EXTRA_EXERCISE ); - CHECK( dummy.activity_level_str() == "EXTRA_EXERCISE" ); - - } - - SECTION( "decrease_activity_level" ) { - // Start at the highest level - dummy.reset_activity_level(); - dummy.increase_activity_level( EXTRA_EXERCISE ); - REQUIRE( dummy.activity_level_str() == "EXTRA_EXERCISE" ); - - // Decrease level a couple times - dummy.decrease_activity_level( ACTIVE_EXERCISE ); - CHECK( dummy.activity_level_str() == "ACTIVE_EXERCISE" ); - dummy.decrease_activity_level( MODERATE_EXERCISE ); - CHECK( dummy.activity_level_str() == "MODERATE_EXERCISE" ); - // Cannot 'decrease' to higher level - dummy.decrease_activity_level( EXTRA_EXERCISE ); - CHECK( dummy.activity_level_str() == "MODERATE_EXERCISE" ); - dummy.decrease_activity_level( ACTIVE_EXERCISE ); - CHECK( dummy.activity_level_str() == "MODERATE_EXERCISE" ); - // Decrease to lowest level - dummy.decrease_activity_level( LIGHT_EXERCISE ); - CHECK( dummy.activity_level_str() == "LIGHT_EXERCISE" ); - dummy.decrease_activity_level( NO_EXERCISE ); - CHECK( dummy.activity_level_str() == "NO_EXERCISE" ); - // Cannot decrease below lowest - dummy.decrease_activity_level( NO_EXERCISE ); - CHECK( dummy.activity_level_str() == "NO_EXERCISE" ); - } -} - // Return a string with multiple consecutive spaces replaced with a single space static std::string condensed_spaces( const std::string &text ) { @@ -512,12 +430,10 @@ static void test_activity_duration( avatar &dummy, const float at_level, // Pass time at this level, updating body each turn for( int turn = 0; turn < to_turns( how_long ); ++turn ) { // Make sure activity level persists (otherwise may be reset after 5 minutes) - dummy.increase_activity_level( at_level ); + dummy.set_activity_level( at_level ); calendar::turn += 1_turns; dummy.update_body(); } - // Stop exercising - dummy.decrease_activity_level( NO_EXERCISE ); } TEST_CASE( "activity levels and calories in daily diary", "[avatar][biometrics][activity][diary]" ) diff --git a/tests/stomach_contents_test.cpp b/tests/stomach_contents_test.cpp index f65dcf5434055..f28cb10819775 100644 --- a/tests/stomach_contents_test.cpp +++ b/tests/stomach_contents_test.cpp @@ -92,6 +92,10 @@ TEST_CASE( "starve_test", "[starve][slow]" ) Character &dummy = get_player_character(); reset_time(); clear_stomach( dummy ); + dummy.reset_activity_level(); + calendar::turn += 1_seconds; + dummy.update_body( calendar::turn, calendar::turn ); + dummy.set_activity_level( 1.0 ); CAPTURE( dummy.metabolic_rate_base() ); CAPTURE( dummy.activity_level_str() );