From 1ea2a93461635532285c43b00bb9adf1e1190a31 Mon Sep 17 00:00:00 2001 From: Eric Pierce Date: Tue, 21 Dec 2021 06:47:18 -0700 Subject: [PATCH 1/2] Convert max_stamina_modifier to cardio_multiplier New cardio_multiplier field is applied as a scaling factor to total cardio fitness, similar to the max_stamina_modifier field it replaces. Total balance in terms of running distance is now about the same as it was before cardio. - Implement cardio_multiplier in get_cardiofit - Refactor and document update_stamina - Convert CARDIO mutations to cardio_multiplier - CRIT mod: Convert Persistent Body to cardio_multiplier - Update mutation descriptions to reflect cardio effects. - Replace Debug Stamina mutation with Debug Cardio mutation - Document stamina_regen_modifier and cardio_multiplier in JSON_INFO - Remove max_stamina_modifier entirely --- data/json/mutations/mutations.json | 20 +++--- .../mutations/crt_wendigo_mutations.json | 2 +- doc/JSON_INFO.md | 2 + src/character.cpp | 65 ++++++++++++------- src/mutation.h | 2 +- src/mutation_data.cpp | 2 +- 6 files changed, 57 insertions(+), 36 deletions(-) diff --git a/data/json/mutations/mutations.json b/data/json/mutations/mutations.json index 1ab4a807b2305..e5842e0433177 100644 --- a/data/json/mutations/mutations.json +++ b/data/json/mutations/mutations.json @@ -232,26 +232,26 @@ "id": "GOODCARDIO", "name": { "str": "Indefatigable" }, "points": 2, - "description": "Whether due to exercise and good diet, or due to a natural propensity to physical endurance, you tire due to physical exertion much less readily than others. Your maximum stamina is higher than usual.", + "description": "Whether due to exercise and good diet, or due to a natural propensity to physical endurance, you tire due to physical exertion much less readily than others. Your cardio fitness and maximum stamina are higher than usual.", "starting_trait": true, "valid": false, "cancels": [ "BADCARDIO" ], "changes_to": [ "GOODCARDIO2" ], "category": [ "FISH", "LUPINE", "MOUSE", "INSECT" ], - "max_stamina_modifier": 1.25 + "cardio_multiplier": 1.3 }, { "type": "mutation", "id": "GOODCARDIO2", "name": { "str": "Hyperactive" }, "points": 4, - "description": "Your body's efficiency is like that of a tiny furnace, greatly increasing your maximum stamina", + "description": "Your body's efficiency is like that of a tiny furnace, greatly increasing your cardio and stamina.", "valid": false, "prereqs": [ "GOODCARDIO" ], "cancels": [ "BADCARDIO" ], "threshreq": [ "THRESH_MOUSE", "THRESH_RABBIT" ], "category": [ "MOUSE", "RABBIT" ], - "max_stamina_modifier": 1.4 + "cardio_multiplier": 1.6 }, { "type": "mutation", @@ -1024,11 +1024,11 @@ "id": "BADCARDIO", "name": { "str": "Languorous" }, "points": -2, - "description": "Whether due to lack of exercise and poor diet, or due to a natural disinclination to physical endurance, you tire due to physical exertion much more readily than others. Your maximum stamina is lower than usual.", + "description": "Whether due to lack of exercise and poor diet, or due to a natural disinclination to physical endurance, you tire due to physical exertion much more readily than others. Your total cardio fitness and stamina are lower than normal.", "starting_trait": true, "valid": false, "cancels": [ "GOODCARDIO" ], - "max_stamina_modifier": 0.75 + "cardio_multiplier": 0.7 }, { "type": "mutation", @@ -7467,12 +7467,12 @@ }, { "type": "mutation", - "id": "DEBUG_STAMINA", - "name": { "str": "Debug Stamina" }, + "id": "DEBUG_CARDIO", + "name": { "str": "Debug Cardio" }, "points": 99, "valid": false, - "max_stamina_modifier": 999999, - "description": "You can't run from the bugs, but you have enough stamina to at least try to.", + "cardio_multiplier": 999999, + "description": "You can run, but you'll never run out of breath.", "debug": true }, { diff --git a/data/mods/CRT_EXPANSION/mutations/crt_wendigo_mutations.json b/data/mods/CRT_EXPANSION/mutations/crt_wendigo_mutations.json index bc4b16dd52486..2f53e76b5a745 100644 --- a/data/mods/CRT_EXPANSION/mutations/crt_wendigo_mutations.json +++ b/data/mods/CRT_EXPANSION/mutations/crt_wendigo_mutations.json @@ -111,7 +111,7 @@ "threshreq": [ "THRESH_SYLVAN" ], "category": [ "SYLVAN" ], "cancels": [ "BADCARDIO" ], - "max_stamina_modifier": 1.5, + "cardio_multiplier": 1.8, "stamina_regen_modifier": 0.25 } ] diff --git a/doc/JSON_INFO.md b/doc/JSON_INFO.md index dfa041e3b6b26..fdd4dded56a9a 100644 --- a/doc/JSON_INFO.md +++ b/doc/JSON_INFO.md @@ -2173,6 +2173,8 @@ The `id` must be exact as it is hardcoded to look for that. "metabolism_modifier": 0.333, // Extra metabolism rate multiplier. 1.0 doubles usage, -0.5 halves. "fatigue_modifier": 0.5, // Extra fatigue rate multiplier. 1.0 doubles usage, -0.5 halves. "fatigue_regen_modifier": 0.333, // Modifier for the rate at which fatigue and sleep deprivation drops when resting. +"stamina_regen_modifier": 0.1, // Increase stamina regen by this proportion (1.0 being 100% of normal regen) +"cardio_multiplier": 1.5, // Multiply total cardio fitness by this amount "healing_awake": 1.0, // Healing rate per turn while awake. "healing_resting": 0.5, // Healing rate per turn while resting. "mending_modifier": 1.2 // Multiplier on how fast your limbs mend - This value would make your limbs mend 20% faster diff --git a/src/character.cpp b/src/character.cpp index 481c37903fe7d..3ccd22bcf2c80 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -5704,7 +5704,7 @@ mutation_value_map = { { "movecost_flatground_modifier", calc_mutation_value_multiplicative<&mutation_branch::movecost_flatground_modifier> }, { "movecost_obstacle_modifier", calc_mutation_value_multiplicative<&mutation_branch::movecost_obstacle_modifier> }, { "attackcost_modifier", calc_mutation_value_multiplicative<&mutation_branch::attackcost_modifier> }, - { "max_stamina_modifier", calc_mutation_value_multiplicative<&mutation_branch::max_stamina_modifier> }, + { "cardio_multiplier", calc_mutation_value_multiplicative<&mutation_branch::cardio_multiplier> }, { "weight_capacity_modifier", calc_mutation_value_multiplicative<&mutation_branch::weight_capacity_modifier> }, { "hearing_modifier", calc_mutation_value_multiplicative<&mutation_branch::hearing_modifier> }, { "movecost_swim_modifier", calc_mutation_value_multiplicative<&mutation_branch::movecost_swim_modifier> }, @@ -6192,12 +6192,13 @@ int Character::get_stamina_max() const // Default base maximum stamina and cardio scaling are defined in data/core/game_balance.json static const std::string player_max_stamina( "PLAYER_MAX_STAMINA_BASE" ); static const std::string player_cardiofit_stamina_scale( "PLAYER_CARDIOFIT_STAMINA_SCALING" ); - static const std::string max_stamina_modifier( "max_stamina_modifier" ); + // Cardiofit stamina mod defaults to 3, and get_cardiofit() should return a value in the vicinity // of 1000-4000, so this should add somewhere between 3000 to 12000 stamina. int max_stamina = get_option( player_max_stamina ) + get_option( player_cardiofit_stamina_scale ) * get_cardiofit(); max_stamina = enchantment_cache->modify_value( enchant_vals::mod::MAX_STAMINA, max_stamina ); + return max_stamina; } @@ -6261,18 +6262,23 @@ void Character::burn_move_stamina( int moves ) void Character::update_stamina( int turns ) { static const std::string player_base_stamina_regen_rate( "PLAYER_BASE_STAMINA_REGEN_RATE" ); - static const std::string stamina_regen_modifier( "stamina_regen_modifier" ); const float base_regen_rate = get_option( player_base_stamina_regen_rate ); - // Your stamina regen rate works as a function of how fit you are compared to your body size. This allows it to scale more quickly - // than your stamina, so that at higher fitness levels you recover stamina faster. + // Your stamina regen rate works as a function of how fit you are compared to your body size. + // This allows it to scale more quickly than your stamina, so that at higher fitness levels you + // recover stamina faster. const float effective_regen_rate = base_regen_rate * get_cardiofit() / base_bmr(); const int current_stim = get_stim(); - float stamina_recovery = 0.0f; - // Recover some stamina every turn. + // Mutations can affect stamina regen via stamina_regen_modifier (0.0 is normal) + // Values above or below normal will increase or decrease stamina regen + const float mod_regen = mutation_value( "stamina_regen_modifier" ); // Mutated stamina works even when winded - // max stamina modifers from mutation also affect stamina multi - float stamina_multiplier = std::max( 0.1f, ( !has_effect( effect_winded ) ? 1.0f : 0.1f ) + - mutation_value( stamina_regen_modifier ) + ( mutation_value( "max_stamina_modifier" ) - 1.0f ) ); + const float base_multiplier = mod_regen + ( has_effect( effect_winded ) ? 0.1f : 1.0f ); + // Ensure multiplier is at least 0.1 + const float stamina_multiplier = std::max( 0.1f, base_multiplier ); + + // Recover some stamina every turn. Start with zero, then increase recovery factor based on + // mutations, stimulants, and bionics before rolling random recovery based on this factor. + float stamina_recovery = 0.0f; // But mouth encumbrance interferes, even with mutated stamina. stamina_recovery += stamina_multiplier * std::max( 1.0f, effective_regen_rate * get_modifier( character_modifier_stamina_recovery_breathing_mod ) ); @@ -6308,9 +6314,11 @@ void Character::update_stamina( int turns ) } } - mod_stamina( roll_remainder( stamina_recovery * turns ) ); - add_msg_debug( debugmode::DF_CHARACTER, "Stamina recovery: %d", - roll_remainder( stamina_recovery * turns ) ); + // Roll to determine actual stamina recovery over this period + int recover_amount = roll_remainder( stamina_recovery * turns ); + mod_stamina( recover_amount ); + add_msg_debug( debugmode::DF_CHARACTER, "Stamina recovery: %d", recover_amount ); + // Cap at max set_stamina( std::min( std::max( get_stamina(), 0 ), max_stam ) ); } @@ -6324,19 +6332,30 @@ int Character::get_cardiofit() const const int bmr = base_bmr(); const int athletics_mod = get_skill_level( skill_swimming ) * 10; const int health_effect = get_healthy(); - // Traits now exclusively affect cardio, NOT max_stamina directly. In the future, make cardio_acc also be affected by cardio traits so that they don't become less impactful. - const int trait_mod = mutation_value( "max_stamina_modifier" ); + + // FIXME: Delete this untruth + // Traits now exclusively affect cardio, NOT max_stamina directly. In the future, make + // cardio_acc also be affected by cardio traits so that they don't become less impactful. + //const int trait_mod = 0; + // At some point we might have proficiencies that affect this. const int prof_mod = 0; const int cardio_acc_mod = get_cardio_acc(); - int final_cardio_fitness = bmr / 2 + athletics_mod + health_effect + trait_mod + prof_mod + - cardio_acc_mod; - if( final_cardio_fitness > 3 * ( bmr + trait_mod ) ) { - // Set a large sane upper limit to cardio fitness. This could be done asymptotically instead of as a sharp cutoff, but the gradual - // growth rate of cardio_acc_mod should accomplish that naturally. The BMR will mostly determine this as it is based on the - // size of the character, but mutations might push it up. - final_cardio_fitness = 3 * ( bmr + trait_mod ); - } + + // Base formula for cardio fitness + int base_cardio_fitness = bmr / 2 + athletics_mod + health_effect + prof_mod + cardio_acc_mod; + + // Apply trait modifier as a scaling factor to total cardio + // FIXME: Do this additively as a trait_mod using the original formula, somehow + const float scale = mutation_value( "cardio_multiplier" ); + const float scaled_fitness = base_cardio_fitness * scale; + + // Set a large sane upper limit to cardio fitness. This could be done asymptotically instead of + // as a sharp cutoff, but the gradual growth rate of cardio_acc_mod should accomplish that + // naturally. The BMR will mostly determine this as it is based on the size of the character, + // but mutations might push it up. + int final_cardio_fitness = static_cast( std::min( scaled_fitness, 3 * bmr * scale ) ); + return final_cardio_fitness; } diff --git a/src/mutation.h b/src/mutation.h index b66a56885b4e5..1c7157227b879 100644 --- a/src/mutation.h +++ b/src/mutation.h @@ -174,7 +174,7 @@ struct mutation_branch { cata::optional movecost_flatground_modifier = cata::nullopt; cata::optional movecost_obstacle_modifier = cata::nullopt; cata::optional attackcost_modifier = cata::nullopt; - cata::optional max_stamina_modifier = cata::nullopt; + cata::optional cardio_multiplier = cata::nullopt; cata::optional weight_capacity_modifier = cata::nullopt; cata::optional hearing_modifier = cata::nullopt; cata::optional movecost_swim_modifier = cata::nullopt; diff --git a/src/mutation_data.cpp b/src/mutation_data.cpp index abc7932d96e86..c196aa254b2ab 100644 --- a/src/mutation_data.cpp +++ b/src/mutation_data.cpp @@ -405,7 +405,7 @@ void mutation_branch::load( const JsonObject &jo, const std::string & ) optional( jo, was_loaded, "movecost_obstacle_modifier", movecost_obstacle_modifier, cata::nullopt ); optional( jo, was_loaded, "movecost_swim_modifier", movecost_swim_modifier, cata::nullopt ); optional( jo, was_loaded, "attackcost_modifier", attackcost_modifier, cata::nullopt ); - optional( jo, was_loaded, "max_stamina_modifier", max_stamina_modifier, cata::nullopt ); + optional( jo, was_loaded, "cardio_multiplier", cardio_multiplier, cata::nullopt ); optional( jo, was_loaded, "weight_capacity_modifier", weight_capacity_modifier, cata::nullopt ); optional( jo, was_loaded, "hearing_modifier", hearing_modifier, cata::nullopt ); optional( jo, was_loaded, "noise_modifier", noise_modifier, cata::nullopt ); From 2d4a90c880c0bd2cf406658c437ae921525302a1 Mon Sep 17 00:00:00 2001 From: Eric Pierce Date: Sat, 18 Sep 2021 17:11:40 -0600 Subject: [PATCH 2/2] Add cardio_test.cpp This test covers several aspects of cardio fitness, stamina, and running distance, especially related to mutations affecting cardio and stamina, and in particular the three CARDIO traits affected by #52980. Some FIXMEs are noted, where trait buffs have changed significantly since the introduction of cardio, and may need to be rebalanced. Several stub test cases are included to invite future collaboration. --- tests/cardio_test.cpp | 336 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 tests/cardio_test.cpp diff --git a/tests/cardio_test.cpp b/tests/cardio_test.cpp new file mode 100644 index 0000000000000..5dec82ad19a17 --- /dev/null +++ b/tests/cardio_test.cpp @@ -0,0 +1,336 @@ +#include "avatar.h" +#include "cata_catch.h" +#include "game.h" +#include "options.h" +#include "map.h" +#include "map_helpers.h" +#include "player_helpers.h" + +// Cardio Fitness +// -------------- +// The hidden "Cardio Fitness" stat returned by Character::get_cardiofit() is the main factor in +// calculations related to cardiovascular health, including weariness, stamina, and stamina regen, +// more or less as described in the original issue #44370 "Rule #1. CARDIO". +// +// For player characters, cardio fitness is derived from a combination of several attributes: +// +// ( +// [ 1/2 BMR ] // Basal Metabolic Rate, varies with activity and body size +// + [ Health ] // Hidden health modifier, -200..+200 +// + [ Proficiency modifiers ] // NOT IMPLEMENTED +// + [ Cardio_Accumulator ] // Adjustment based on daily activity, starting at 1/2 BMR +// + 10 * [ Athletics skill ] // Formerly swimming skill, now more versatile and valuable +// ) * [ Trait modifiers ] // Good/bad cardio mutations like Languorous and Hyperactive +// +// For NPCs (having no cardio), the formula is simply 2 * BMR. +// +// Important functions: +// - Character::get_stamina +// - Character::set_stamina +// - Character::get_stamina_max +// - Character::get_cardiofit +// - Character::get_cardio_acc +// - Character::set_cardio_acc +// - avatar::update_cardio_acc <-- Critical + +static const efftype_id effect_winded( "winded" ); + +static const move_mode_id move_mode_run( "run" ); + +static const skill_id skill_swimming( "swimming" ); + +// Base BMR for default character +static const int base_bmr = 1738; +static const int base_cardio = base_bmr; +// Base stamina +// MAX_STAMINA_BASE + CARDIOFIT_STAMINA_SCALING * base_bmr == 3500 + 3*1738 +static const int base_stamina = 8714; + +// Ensure the configured options from game_balance.json are what the tests assume they are +static void verify_default_cardio_options() +{ + const int max_stamina_base = get_option( "PLAYER_MAX_STAMINA_BASE" ); + const int cardiofit_stamina_scaling = get_option( "PLAYER_CARDIOFIT_STAMINA_SCALING" ); + REQUIRE( max_stamina_base == 3500 ); + REQUIRE( cardiofit_stamina_scaling == 3 ); +} + +// Count the number of steps (tiles) until character runs out of stamina or becomes winded. +static int running_steps( Character &they, const ter_id &terrain = t_pavement ) +{ + map &here = get_map(); + // Please take off your shoes when entering, and no NPCs allowed + REQUIRE_FALSE( they.is_wearing_shoes() ); + REQUIRE_FALSE( they.is_npc() ); + // You put your left foot in, you put your right foot in + const tripoint left = they.pos(); + const tripoint right = left + tripoint_east; + // You ensure two tiles of terrain to hokey-pokey in + here.ter_set( left, terrain ); + here.ter_set( right, terrain ); + REQUIRE( here.ter( left ) == terrain ); + REQUIRE( here.ter( right ) == terrain ); + // Count how many steps (1-tile moves) it takes to become winded + int steps = 0; + const int STOP_STEPS = 1000; // Safe exit in case of Superman + // Track changes to moves and stamina + int last_moves = they.get_speed(); + int last_stamina = they.get_stamina_max(); + + // Take a deep breath and start running + they.moves = last_moves; + they.set_stamina( last_stamina ); + they.set_movement_mode( move_mode_run ); + // Run until out of stamina or winded (should happen at the same time) + while( they.get_stamina() > 0 && !they.has_effect( effect_winded ) && steps < STOP_STEPS ) { + // Step right on even steps, left on odd steps + if( steps % 2 == 0 ) { + REQUIRE( they.pos() == left ); + REQUIRE( g->walk_move( right, false, false ) ); + } else { + REQUIRE( they.pos() == right ); + REQUIRE( g->walk_move( left, false, false ) ); + } + ++steps; + + // Ensure moves are decreasing, or else a turn will never pass + REQUIRE( they.moves < last_moves ); + const int move_cost = last_moves - they.moves; + // When moves run out, one turn has passed + if( they.moves <= 0 ) { + // Get "speed" moves back each turn + they.moves += they.get_speed(); + calendar::turn += 1_turns; + } + last_moves = they.moves; + + // Update body for stamina regen + they.update_body(); + // NOTE: Stamina cost is always 100, 101, 120, or 121 ?? + const int stamina_cost = last_stamina - they.get_stamina(); + // Total stamina must also be decreasing; if not, quit + CAPTURE( move_cost ); + CAPTURE( stamina_cost ); + REQUIRE( they.get_stamina() < last_stamina ); + last_stamina = they.get_stamina(); + } + // Reset to starting position + they.setpos( left ); + return steps; +} + +// Give character a trait, and verify their expected cardio, max stamina, and running distance. +static void check_trait_cardio_stamina_run( Character &they, std::string trait_name, + const int expect_cardio_fit, const int expect_stamina_max, const int expect_run_tiles ) +{ + clear_avatar(); + if( trait_name.empty() ) { + trait_name = "NONE"; + } else { + set_single_trait( they, trait_name ); + } + GIVEN( "trait: " + trait_name ) { + CHECK( they.get_cardiofit() == Approx( expect_cardio_fit ).margin( 2 ) ); + CHECK( they.get_stamina_max() == Approx( expect_stamina_max ).margin( 5 ) ); + //CHECK( they.get_stamina_max() == Approx( 3500 + 3 * expect_cardio_fit ).margin( 2 ) ); + CHECK( running_steps( they ) == Approx( expect_run_tiles ).margin( 3 ) ); + } +} + + +// Trait Modifiers +// --------------- +// Mutations/traits can influence cardio fitness level in a few ways: +// +// - Mutation affecting total body size, which affects base BMR and thus cardio +// - Little / Tiny: Smaller body has lower BMR, less cardio, cannot run as far +// - Large / Huge: Larger body has higher BMR, more cardio, can run further +// - metabolism_modifier: Affects metabolic_rate_base +// - Heat Dependent / Cold Blooded: Decreased BMR and cardio, less running +// - Fast/Rapid/Extreme Metabolism: Increased BMR and cardio, more running +// +// Some traits affect cardio fitness directly: +// +// - cardio_multiplier: Multiplies maximum cardio +// - Languorous: Bad cardio, less total stamina +// - Indefatigable, Hyperactive: Good cardio, more total stamina +// +// Some traits affect stamina regen and total running distance without affecting cardio: +// +// - stamina_regen_modifier +// - Fast Metabolism, Persistence Hunter: Increased stamina regeneration +// +TEST_CASE( "cardio is affected by certain traits", "[cardio][traits]" ) +{ + verify_default_cardio_options(); + Character &they = get_player_character(); + + clear_map(); + clear_avatar(); + + // Ensure no initial effects that would affect cardio + REQUIRE( they.get_healthy() == 0 ); + REQUIRE( they.get_skill_level( skill_swimming ) == 0 ); + // Ensure base_bmr and starting cardio are what we expect + REQUIRE( they.base_bmr() == base_bmr ); + REQUIRE( they.get_cardiofit() == base_cardio ); + REQUIRE( 2 * they.get_cardio_acc() == base_bmr ); + + // Values trailing after check //123 are pre-Cardio in-game running distances, a high-level + // metric for balancing traits relative to how they were before cardio. Some traits were buffed + // by the change to cardio; the fast metabolism, persistence hunger, and cold-blooded traits got + // a 20-30% boost in total running distance. The old values are preserved here for possible + // future rebalancing and comparison. + + SECTION( "Base character with no traits" ) { + // pre-Cardio, could run 96 steps + // post-Cardio, can run 84 steps in-game, test case reached 87 + // correctly counting moves/steps instead of turns, test case reaches 83 + check_trait_cardio_stamina_run( they, "", base_cardio, base_stamina, 83 ); //96 + } + + // Sprint distance for each body size is only slightly different than it was before cardio + SECTION( "Traits affecting body size" ) { + // Body size determines BMR, which affects base cardio fitness + // Pre-Cardio, body size did not affect how many steps you could run + check_trait_cardio_stamina_run( they, "SMALL2", 1088, 6764, 83 ); //97 + // FIXME: But why the heck can SMALL2 run further than SMALL? + check_trait_cardio_stamina_run( they, "SMALL", 1376, 7628, 77 ); //97 + check_trait_cardio_stamina_run( they, "LARGE", 2162, 9986, 92 ); //97 + check_trait_cardio_stamina_run( they, "HUGE", 2663, 11489, 106 ); //97 + } + + SECTION( "Traits with cardio_multiplier" ) { + // These traits were formerly implemented by max_stamina_modifier, which multiplied + // maximum stamina. Now that cardio fitness is actually implemented, these traits + // directly affect total cardio fitness, and thus maximum stamina (and running distance). + // Languorous + check_trait_cardio_stamina_run( they, "BADCARDIO", 0.7 * base_cardio, 7148, 67 ); //70 + // Indefatigable + check_trait_cardio_stamina_run( they, "GOODCARDIO", 1.3 * base_cardio, 10277, 103 ); //126 + // Hyperactive + check_trait_cardio_stamina_run( they, "GOODCARDIO2", 1.6 * base_cardio, 11840, 125 ); //145 + } + + // FIXME: These traits need a significant nerf (-8 to -32) to reach their pre-Cardio balance + SECTION( "Traits with metabolism_modifier AND stamina_regen_modifier" ) { + // Fast Metabolism + check_trait_cardio_stamina_run( they, "HUNGER", 2173, 10019, 95 ); //76 + // Very Fast Metabolism + check_trait_cardio_stamina_run( they, "HUNGER2", 2608, 11324, 108 ); //87 + // Extreme Metabolism + check_trait_cardio_stamina_run( they, "HUNGER3", 3477, 13931, 132 ); //107 + } + + // FIXME: These traits need a significant nerf (-20) to reach their pre-Cardio balance + SECTION( "Traits with ONLY stamina_regen_modifier" ) { + check_trait_cardio_stamina_run( they, "PERSISTENCE_HUNTER", base_cardio, base_stamina, 85 ); //68 + check_trait_cardio_stamina_run( they, "PERSISTENCE_HUNTER2", base_cardio, base_stamina, 86 ); //69 + } + + // FIXME: These traits need a significant nerf (-20) to reach their pre-Cardio balance + SECTION( "Traits with ONLY metabolism_modifier" ) { + check_trait_cardio_stamina_run( they, "COLDBLOOD", 1449, 7847, 78 ); //63 + check_trait_cardio_stamina_run( they, "COLDBLOOD2", 1304, 7412, 77 ); //62 + check_trait_cardio_stamina_run( they, "COLDBLOOD3", 1304, 7412, 77 ); //62 + check_trait_cardio_stamina_run( they, "COLDBLOOD4", 1304, 7412, 77 ); //62 + check_trait_cardio_stamina_run( they, "LIGHTEATER", 1449, 7847, 78 ); //63 + check_trait_cardio_stamina_run( they, "MET_RAT", 2028, 9584, 90 ); //72 + } +} + +TEST_CASE( "cardio affects stamina regeneration", "[cardio][stamina]" ) +{ + // With baseline cardio, stamina regen is X + // With low cardio, stamina regen is X-- + // With high cardio, stamina regen is X++ +} + +TEST_CASE( "cardio affects weariness", "[cardio][weariness]" ) +{ + // Weariness threshold is 1/2 cardio +} + +TEST_CASE( "cardio is affected by activity level each day", "[cardio][activity]" ) +{ + // When activity increases, cardio goes up + // When activity decreases, cardio goes down + // When activity stays the same, cardio stays the same + + // Given a starting character + // When they get no exercise for a week + // Then their cardio should decrease slightly + // When they get moderate exercise for a week + // Then their cardio should increase slightly +} + +TEST_CASE( "cardio is affected by character height", "[cardio][height]" ) +{ + verify_default_cardio_options(); + Character &they = get_player_character(); + clear_avatar(); + + REQUIRE( they.size_class == creature_size::medium ); + + SECTION( "Within a size class, greater height means greater cardio" ) { + they.set_base_height( Character::default_height( they.size_class ) ); + CHECK( they.get_cardiofit() == base_cardio ); //1739 + they.set_base_height( Character::max_height( they.size_class ) ); + CHECK( they.get_cardiofit() == Approx( base_cardio + 196 ).margin( 5 ) ); //1935 + they.set_base_height( Character::min_height( they.size_class ) ); + CHECK( they.get_cardiofit() == Approx( base_cardio - 214 ).margin( 5 ) ); //1525 + } +} + +TEST_CASE( "cardio is affected by character weight", "[cardio][weight]" ) +{ + // Underweight, overweight +} + +TEST_CASE( "cardio is affected by character health", "[cardio][health]" ) +{ + verify_default_cardio_options(); + Character &they = get_player_character(); + clear_avatar(); + + SECTION( "Hidden health stat adds directly to cardio fitness" ) { + they.set_healthy( 0 ); + CHECK( they.get_cardiofit() == base_cardio ); + they.set_healthy( 200 ); + CHECK( they.get_cardiofit() == base_cardio + 200 ); + they.set_healthy( -200 ); + CHECK( they.get_cardiofit() == base_cardio - 200 ); + } +} + +TEST_CASE( "cardio is affected by athletics skill", "[cardio][athletics]" ) +{ + verify_default_cardio_options(); + Character &they = get_player_character(); + clear_avatar(); + + SECTION( "Athletics skill adds 10 per level to cardio fitness" ) { + they.set_skill_level( skill_swimming, 0 ); + CHECK( they.get_cardiofit() == base_cardio ); + they.set_skill_level( skill_swimming, 1 ); + CHECK( they.get_cardiofit() == base_cardio + 10 ); + they.set_skill_level( skill_swimming, 2 ); + CHECK( they.get_cardiofit() == base_cardio + 20 ); + they.set_skill_level( skill_swimming, 3 ); + CHECK( they.get_cardiofit() == base_cardio + 30 ); + they.set_skill_level( skill_swimming, 4 ); + CHECK( they.get_cardiofit() == base_cardio + 40 ); + they.set_skill_level( skill_swimming, 5 ); + CHECK( they.get_cardiofit() == base_cardio + 50 ); + they.set_skill_level( skill_swimming, 6 ); + CHECK( they.get_cardiofit() == base_cardio + 60 ); + they.set_skill_level( skill_swimming, 7 ); + CHECK( they.get_cardiofit() == base_cardio + 70 ); + they.set_skill_level( skill_swimming, 8 ); + CHECK( they.get_cardiofit() == base_cardio + 80 ); + they.set_skill_level( skill_swimming, 9 ); + CHECK( they.get_cardiofit() == base_cardio + 90 ); + they.set_skill_level( skill_swimming, 10 ); + CHECK( they.get_cardiofit() == base_cardio + 100 ); + } +}