diff --git a/tests/iteminfo_test.cpp b/tests/iteminfo_test.cpp index 60de192b795f1..6273e607de01f 100644 --- a/tests/iteminfo_test.cpp +++ b/tests/iteminfo_test.cpp @@ -11,575 +11,1926 @@ #include "itype.h" #include "player_helpers.h" #include "options_helpers.h" +#include "output.h" #include "recipe.h" #include "recipe_dictionary.h" #include "type_id.h" #include "value_ptr.h" -static void test_info_equals( const item &i, const iteminfo_query &q, - const std::string &reference ) -{ - int encumber = i.type->armor ? i.type->armor->encumber : -1; - int max_encumber = i.type->armor ? i.type->armor->max_encumber : -1; - CAPTURE( encumber ); - CAPTURE( max_encumber ); - CAPTURE( i.typeId() ); - CAPTURE( i.has_flag( "FIT" ) ); - CAPTURE( i.has_flag( "VARSIZE" ) ); - CAPTURE( i.get_clothing_mod_val( clothing_mod_type_encumbrance ) ); - CAPTURE( i.get_sizing( g->u, true ) ); - std::vector info_v; - std::string info = i.info( info_v, &q, 1 ); - CHECK( info == reference ); -} - -static void test_info_contains( const item &i, const iteminfo_query &q, - const std::string &reference ) -{ - int encumber = i.type->armor ? i.type->armor->encumber : -1; - int max_encumber = i.type->armor ? i.type->armor->max_encumber : -1; - CAPTURE( encumber ); - CAPTURE( max_encumber ); - CAPTURE( i.typeId() ); - CAPTURE( i.has_flag( "FIT" ) ); - CAPTURE( i.has_flag( "VARSIZE" ) ); - CAPTURE( i.get_clothing_mod_val( clothing_mod_type_encumbrance ) ); - CAPTURE( i.get_sizing( g->u, true ) ); + +// ITEM INFO +// ========= +// +// When looking at the description of any item in the game, the information you see comes from the +// item::info function, which calls functions like basic_info, armor_info, food_info and many others +// to build a complete string describing the item. +// +// The included info depends on: +// +// - Relevancy to current item (damage for weapons, coverage for armor, vitamins for food, etc.) +// - Including one or more appropriate iteminfo_part::PART_NAME flags in the `info` function call +// +// Color-highlighted text in item info uses a semantic markup in the code (ex. "good", "bad") that +// becomes translated to color codes for output: +// +// +// +// +// +// +// +//
+// +// Each xxx_info function takes a std::vector reference, where each line or snippet of +// info will be appended. The main item::info function assembles all these into a string. +// +// The test cases here mostly test item::info directly, using std::vector flags to +// request only the parts relevant to the current test. +// +// To run all the tests in this file: +// +// tests/cata_test [iteminfo] +// +// Other tags: [book], [food], [pocket], [quality], [weapon], [volume], [weight], and many others + + +// Call the info() function on an item with given flags, and return the formatted string. +static std::string item_info_str( const item &it, const std::vector &part_flags ) +{ + // Old captures from test_info_equals/contains, in case needed + //int encumber = i.type->armor ? i.type->armor->encumber : -1; + //int max_encumber = i.type->armor ? i.type->armor->max_encumber : -1; + //CAPTURE( encumber ); + //CAPTURE( max_encumber ); + //CAPTURE( i.typeId() ); + //CAPTURE( i.has_flag( "FIT" ) ); + //CAPTURE( i.has_flag( "VARSIZE" ) ); + //CAPTURE( i.get_clothing_mod_val( clothing_mod_type_encumbrance ) ); + //CAPTURE( i.get_sizing( g->u, true ) ); + std::vector info_v; - std::string info = i.info( info_v, &q, 1 ); - using Catch::Matchers::Contains; - REQUIRE_THAT( info, Contains( reference ) ); + const iteminfo_query query_v( part_flags ); + it.info( info_v, &query_v, 1 ); + return format_item_info( info_v, {} ); } -/* - * Wrap the iteminfo_query() constructor to avoid MacOS clang compiler errors like this: - * - * iteminfo_test.cpp:NN: error: call to constructor of 'iteminfo_query' is ambiguous - * iteminfo_query q( { iteminfo_parts::BASE_RIGIDITY, iteminfo_parts::ARMOR_ENCUMBRANCE } ); - * ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - * ../src/iteminfo_query.h:245:9: note: candidate constructor - * iteminfo_query( const std::string &bits ); - * ../src/iteminfo_query.h:246:9: note: candidate constructor - * iteminfo_query( const std::vector &setBits ); - * - * Using this wrapper should force it to use the vector constructor. - */ -static iteminfo_query q_vec( const std::vector &part_flags ) +// Related JSON fields: +// "volume" +// "weight" +// "longest_side" +// +// Functions: +// item::basic_info +TEST_CASE( "item volume and weight", "[iteminfo][volume][weight]" ) { - return iteminfo_query( part_flags ); + clear_avatar(); + + item plank( "test_2x4" ); + + // Volume and weight are shown together, though the units may differ + std::vector vol_weight = { iteminfo_parts::BASE_VOLUME, iteminfo_parts::BASE_WEIGHT }; + + SECTION( "metric volume" ) { + override_option opt_volume( "VOLUME_UNITS", "l" ); + SECTION( "metric weight" ) { + override_option opt_weight( "USE_METRIC_WEIGHTS", "kg" ); + CHECK( item_info_str( plank, vol_weight ) == + "Volume: 4.40 L Weight: 2.20 kg\n" ); + } + SECTION( "imperial weight" ) { + override_option opt_weight( "USE_METRIC_WEIGHTS", "lbs" ); + CHECK( item_info_str( plank, vol_weight ) == + "Volume: 4.40 L Weight: 4.85 lbs\n" ); + } + } + + SECTION( "imperial volume" ) { + override_option opt_volume( "VOLUME_UNITS", "qt" ); + SECTION( "metric weight" ) { + override_option opt_weight( "USE_METRIC_WEIGHTS", "kg" ); + CHECK( item_info_str( plank, vol_weight ) == + "Volume: 4.65 qt Weight: 2.20 kg\n" ); + } + SECTION( "imperial weight" ) { + override_option opt_weight( "USE_METRIC_WEIGHTS", "lbs" ); + CHECK( item_info_str( plank, vol_weight ) == + "Volume: 4.65 qt Weight: 4.85 lbs\n" ); + } + } + + SECTION( "imperial length" ) { + override_option opt_dist( "DISTANCE_UNITS", "imperial" ); + CHECK( item_info_str( plank, { iteminfo_parts::BASE_LENGTH } ) == + "Length: 51 in.\n" ); + } + + SECTION( "metric length" ) { + override_option opt_dist( "DISTANCE_UNITS", "metric" ); + CHECK( item_info_str( plank, { iteminfo_parts::BASE_LENGTH } ) == + "Length: 130 cm\n" ); + } } -TEST_CASE( "item description and physical attributes", "[item][iteminfo][primary]" ) +// Related JSON fields: +// "material" +// "category" +// "description" +// +// Functions: +// item::basic_info +TEST_CASE( "item material, category, description", "[iteminfo][material][category][description]" ) { clear_avatar(); - iteminfo_query q = q_vec( { iteminfo_parts::BASE_CATEGORY, iteminfo_parts::BASE_MATERIAL, - iteminfo_parts::BASE_VOLUME, iteminfo_parts::BASE_WEIGHT, - iteminfo_parts::DESCRIPTION - } ); - override_option opt_weight( "USE_METRIC_WEIGHTS", "lbs" ); - override_option opt_vol( "VOLUME_UNITS", "l" ); - SECTION( "volume, weight, category, material, description" ) { - test_info_equals( - item( "test_jug_plastic" ), q, - "Material: Plastic\n" - "Volume: 3.75 L Weight: 0.42 lbs\n" - "Category: CONTAINERS\n" - "--\n" - "A standard plastic jug used for milk and household cleaning chemicals.\n" ); + // TODO: Test magical focus, in-progress crafts (also part of DESCRIPTION) + + std::vector material = { iteminfo_parts::BASE_MATERIAL }; + std::vector category = { iteminfo_parts::BASE_CATEGORY }; + std::vector description = { iteminfo_parts::DESCRIPTION }; + + SECTION( "fire ax" ) { + item axe( "test_fire_ax" ); + CHECK( item_info_str( axe, material ) == + "Material: Steel, Wood\n" ); + + CHECK( item_info_str( axe, category ) == + "Category: TOOLS\n" ); + + CHECK( item_info_str( axe, description ) == + "--\n" + "This is a large, two-handed pickhead axe normally used by firefighters." + " It makes a powerful melee weapon, but is a bit slow to recover between swings.\n" ); + } + + SECTION( "plank" ) { + item plank( "test_2x4" ); + + CHECK( item_info_str( plank, material ) == + "Material: Wood\n" ); + + CHECK( item_info_str( plank, category ) == + "Category: SPARE PARTS\n" ); + + CHECK( item_info_str( plank, description ) == + "--\n" + "A narrow, thick plank of wood, like a 2 by 4 or similar piece of dimensional lumber." + " Makes a decent melee weapon, and can be used for all kinds of construction.\n" ); } } -TEST_CASE( "item owner, price, and barter value", "[item][iteminfo][price]" ) +// Related JSON fields: +// none +// +// Functions: +// item::basic_info +TEST_CASE( "item owner", "[iteminfo][owner]" ) { clear_avatar(); - iteminfo_query q = q_vec( std::vector( { iteminfo_parts::BASE_PRICE, iteminfo_parts::BASE_BARTER } ) ); - SECTION( "owner and price" ) { + SECTION( "item owned by player" ) { item my_rock( "test_rock" ); my_rock.set_owner( g->u ); - test_info_equals( - my_rock, q, - "Owner: Your Followers\n" - "--\n" - "Price: $0.00" ); + REQUIRE_FALSE( my_rock.get_owner().is_null() ); + CHECK( item_info_str( my_rock, { iteminfo_parts::BASE_OWNER } ) == "Owner: Your Followers\n" ); } - SECTION( "owner, price and barter value" ) { - item my_pipe( "test_pipe" ); - my_pipe.set_owner( g->u ); - test_info_equals( - my_pipe, q, - "Owner: Your Followers\n" - "--\n" - "Price: $75.00 Barter value: $3.00\n" ); - } - - SECTION( "zero price item with no owner" ) { + SECTION( "item with no owner" ) { item nobodys_rock( "test_rock" ); REQUIRE( nobodys_rock.get_owner().is_null() ); - test_info_equals( - nobodys_rock, q, - "--\n" - "Price: $0.00" ); + CHECK( item_info_str( nobodys_rock, { iteminfo_parts::BASE_OWNER } ).empty() ); } } -TEST_CASE( "item rigidity", "[item][iteminfo][rigidity]" ) +// Related JSON fields: +// "min_skills" +// "min_strength" +// "min_intelligence" +// "min_perception" +// +// Functions: +// item::basic_info +TEST_CASE( "item requirements", "[iteminfo][requirements]" ) { + // TODO: + // - get_min_str() - type->min_str with special gun/gunmod handling + + // NOTE: BOOK_REQUIREMENTS_INT is separate + // NOTE: There are presently no in-game items with min_dex, min_int, or min_per + clear_avatar(); - iteminfo_query q = q_vec( { iteminfo_parts::BASE_RIGIDITY, iteminfo_parts::ARMOR_ENCUMBRANCE } ); + + std::vector reqs = { iteminfo_parts::BASE_REQUIREMENTS }; + + item compbow( "test_compbow" ); + item sonic( "test_sonic_screwdriver" ); + + REQUIRE( compbow.type->min_str == 6 ); + CHECK( item_info_str( compbow, reqs ) == + "--\n" + "Minimum requirements:\n" + "strength 6\n" ); + + REQUIRE( sonic.type->min_int == 9 ); + REQUIRE( sonic.type->min_per == 5 ); + CHECK( item_info_str( sonic, reqs ) == + "--\n" + "Minimum requirements:\n" + "intelligence 9, perception 5, electronics 3, and lock picking 2\n" ); +} + +// Functions: +// item::basic_info +TEST_CASE( "item contents", "[iteminfo][contents]" ) +{ + clear_avatar(); + + // TODO: Test "Contains:", if it is still possible (couldn't find any items with it) + //std::vector contents = { iteminfo_parts::BASE_CONTENTS }; + + // Amount is shown for items having count_by_charges(), and are not food or medication + // This includes all kinds of ammo and arrows, thread, and some chemicals like sulfur. + item ammo( "test_9mm_ammo" ); + std::vector amount = { iteminfo_parts::BASE_AMOUNT }; + CHECK( item_info_str( ammo, amount ) == "--\nAmount: 50\n" ); +} + +// Related JSON fields: +// "comestible_type": "MED": +// "quench" +// "fun" +// "stim" +// "charges" (portions) +// "addiction_potential" +// +// Functions: +// item::med_info +TEST_CASE( "med_info", "[iteminfo][med]" ) +{ + clear_avatar(); + + // Parts to test + std::vector quench = { iteminfo_parts::MED_QUENCH }; + std::vector joy = { iteminfo_parts::MED_JOY }; + std::vector stimulation = { iteminfo_parts::MED_STIMULATION }; + std::vector portions = { iteminfo_parts::MED_PORTIONS }; + std::vector consume_time = { iteminfo_parts::MED_CONSUME_TIME }; + std::vector addicting = { iteminfo_parts::DESCRIPTION_MED_ADDICTING }; + + // Items with comestible_type "MED" + SECTION( "item that is medication shows medicinal attributes in med_info" ) { + item gum( "test_gum" ); + REQUIRE( gum.is_medication() ); + + CHECK( item_info_str( gum, quench ) == + "--\nQuench: 50\n" ); + + CHECK( item_info_str( gum, joy ) == + "--\nEnjoyability: 5\n" ); + + CHECK( item_info_str( gum, stimulation ) == + "--\nStimulation: Upper\n" ); + + CHECK( item_info_str( gum, portions ) == + "--\nPortions: 10\n" ); + + CHECK( item_info_str( gum, consume_time ) == + "--\nConsume time: 5 seconds\n" ); + + CHECK( item_info_str( gum, addicting ) == + "--\n* Consuming this item is addicting.\n" ); + } + + SECTION( "item that is not medication does not show med_info" ) { + item apple( "test_apple" ); + REQUIRE_FALSE( apple.is_medication() ); + + CHECK( item_info_str( apple, quench ).empty() ); + CHECK( item_info_str( apple, joy ).empty() ); + CHECK( item_info_str( apple, stimulation ).empty() ); + CHECK( item_info_str( apple, portions ).empty() ); + CHECK( item_info_str( apple, consume_time ).empty() ); + CHECK( item_info_str( apple, addicting ).empty() ); + } +} + +// Related JSON fields: +// "price" +// "price_postapoc" +// +// Functions: +// item::final_info +TEST_CASE( "item price and barter value", "[iteminfo][price]" ) +{ + clear_avatar(); + + // Price and barter value are displayed together on a single line + std::vector price_barter = { iteminfo_parts::BASE_PRICE, iteminfo_parts::BASE_BARTER }; + + SECTION( "item with different price and barter value" ) { + item pipe( "test_pipe" ); + REQUIRE( pipe.price( false ) == 7500 ); + REQUIRE( pipe.price( true ) == 300 ); + + CHECK( item_info_str( pipe, price_barter ) == + "--\n" + "Price: $75.00 Barter value: $3.00\n" ); + } + + SECTION( "item with same price and barter value shows only price" ) { + item nuts( "test_pine_nuts" ); + REQUIRE( nuts.price( false ) == 136 ); + REQUIRE( nuts.price( true ) == 136 ); + + CHECK( item_info_str( nuts, price_barter ) == + "--\n" + "Price: $1.36" ); + } + + SECTION( "item with no price or barter value" ) { + item rock( "test_rock" ); + REQUIRE( rock.price( false ) == 0 ); + REQUIRE( rock.price( true ) == 0 ); + + CHECK( item_info_str( rock, price_barter ) == + "--\n" + "Price: $0.00" ); + } +} + +// Related JSON fields: +// "encumbrance" +// "max_encumbrance" +// "pocket_data" +// - "rigid" +// - "max_contains_volume" +// +// Functions: +// item::armor_info +TEST_CASE( "item rigidity", "[iteminfo][rigidity]" ) +{ + clear_avatar(); + + // Rigidity and encumbrance are related; non-rigid items have flexible encumbrance. + std::vector rigidity = { iteminfo_parts::BASE_RIGIDITY }; + std::vector encumbrance = { iteminfo_parts::ARMOR_ENCUMBRANCE }; + + SECTION( "items with rigid pockets have a single encumbrance value" ) { + item briefcase( "test_briefcase" ); + REQUIRE( briefcase.contents.all_pockets_rigid() ); + CHECK( item_info_str( briefcase, encumbrance ) == + "--\n" + "Encumbrance: 30\n" ); + } SECTION( "non-rigid items indicate their flexible volume/encumbrance" ) { - // Waterskin uses the default encumbrance increase of 1 per 250ml - test_info_equals( - item( "test_waterskin" ), q, - "--\n" - "Encumbrance: 0" - " Encumbrance when full: 6\n" - "--\n" - "* This item is not rigid." - " Its volume and encumbrance increase with contents.\n" ); - - // test_backpack has an explicit max_encumbrance - test_info_equals( - item( "test_backpack" ), q, - "--\n" - "Encumbrance: 2" - " Encumbrance when full: 15\n" - "--\n" - "* This item is not rigid." - " Its volume and encumbrance increase with contents.\n" ); - - // quiver has no volume, only an implicit volume via ammo - test_info_equals( - item( "quiver" ), q, - "--\n" - "Encumbrance: 3" - " Encumbrance when full: 11\n" - "--\n" - "* This item is not rigid." - " Its volume and encumbrance increase with contents.\n" ); - } - - SECTION( "rigid items do not indicate they are rigid, since almost all items are" ) { - test_info_equals( - item( "test_briefcase" ), q, - "--\n" - "Encumbrance: 30\n" ); - - test_info_equals( item( "test_jug_plastic" ), q, "" ); - test_info_equals( item( "test_pipe" ), q, "" ); - test_info_equals( item( "test_pine_nuts" ), q, "" ); - } -} - -TEST_CASE( "weapon attack ratings and moves", "[item][iteminfo][weapon]" ) + item waterskin( "test_waterskin" ); + item backpack( "test_backpack" ); + item quiver( "test_quiver" ); + + SECTION( "rigidity indicator" ) { + REQUIRE_FALSE( waterskin.contents.all_pockets_rigid() ); + REQUIRE_FALSE( backpack.contents.all_pockets_rigid() ); + REQUIRE_FALSE( quiver.contents.all_pockets_rigid() ); + + CHECK( item_info_str( waterskin, rigidity ) == + "--\n" + "* This item is not rigid." + " Its volume and encumbrance increase with contents.\n" ); + + CHECK( item_info_str( backpack, rigidity ) == + "--\n" + "* This item is not rigid." + " Its volume and encumbrance increase with contents.\n" ); + + CHECK( item_info_str( quiver, rigidity ) == + "--\n" + "* This item is not rigid." + " Its volume and encumbrance increase with contents.\n" ); + } + + SECTION( "encumbrance when empty and full" ) { + // test_waterskin does not define "encumbrance" or "max_encumbrance", so base + // encumbrance is 0, and max_encumbrance is set by the item factory (finalize_post) + // based on the pocket "max_contains_volume" (1 encumbrance per 250 ml). + CHECK( item_info_str( waterskin, encumbrance ) == + "--\n" + "Encumbrance: 0" + " Encumbrance when full: 6\n" ); + + // test_backpack has an explicit "encumbrance" and "max_encumbrance" + CHECK( item_info_str( backpack, encumbrance ) == + "--\n" + "Encumbrance: 2" + " Encumbrance when full: 15\n" ); + + // quiver has no volume, only an implicit volume via ammo + CHECK( item_info_str( quiver, encumbrance ) == + "--\n" + "Encumbrance: 3" + " Encumbrance when full: 11\n" ); + } + } +} + +// Related JSON fields: +// "bashing" +// "cutting" +// "to_hit" +// +// Functions: +// item::combat_info +TEST_CASE( "weapon attack ratings and moves", "[iteminfo][weapon]" ) { clear_avatar(); // new DPS calculations depend on the avatar's stats, so make sure they're consistent REQUIRE( g->u.get_str() == 8 ); REQUIRE( g->u.get_dex() == 8 ); - iteminfo_query q = q_vec( { iteminfo_parts::BASE_DAMAGE, iteminfo_parts::BASE_TOHIT, - iteminfo_parts::BASE_MOVES - } ); - - SECTION( "bash damage" ) { - test_info_equals( - item( "test_rock" ), q, - "--\n" - "Melee damage: Bash: 7" - " To-hit bonus: -2\n" - "Moves per attack: 79\n" - "Typical damage per second:\n" - "Best: 4.92" - " Vs. Agile: 2.05" - " Vs. Armored: 0.15\n" ); + + item rag( "test_rag" ); + item rock( "test_rock" ); + item halligan( "test_halligan" ); + item mr_pointy( "test_pointy_stick" ); + item arrow( "test_arrow_wood" ); + + SECTION( "melee damage" ) { + // Melee damage comes from the "bashing" and "cutting" attributes in JSON + + // NOTE: BASE_DAMAGE info has no newline, since BASE_TOHIT always follows it + std::vector damage = { iteminfo_parts::BASE_DAMAGE }; + + SECTION( "no damage" ) { + CHECK( item_info_str( rag, damage ).empty() ); + } + + SECTION( "bash" ) { + CHECK( item_info_str( rock, damage ) == + "--\n" + "Melee damage:" + " Bash: 7" ); + } + SECTION( "bash and cut" ) { + CHECK( item_info_str( halligan, damage ) == + "--\n" + "Melee damage:" + " Bash: 20" + " Cut: 5" ); + } + SECTION( "bash and pierce" ) { + // Pierce damage comes from "cut" value, if item has the STAB or SPEAR flag + REQUIRE( mr_pointy.has_flag( "SPEAR" ) ); + CHECK( item_info_str( mr_pointy, damage ) == + "--\n" + "Melee damage:" + " Bash: 5" + " Pierce: 9" ); + } + SECTION( "bash and cut (ranged ammo)" ) { + CHECK( item_info_str( arrow, damage ) == + "--\n" + "Melee damage:" + " Bash: 2" + " Cut: 1" ); + } } - SECTION( "bash and cut damage" ) { - test_info_equals( - item( "test_halligan" ), q, - "--\n" - "Melee damage: Bash: 20" - " Cut: 5" - " To-hit bonus: +2\n" - "Moves per attack: 145\n" - "Typical damage per second:\n" - "Best: 9.38" - " Vs. Agile: 5.74" - " Vs. Armored: 2.84\n" ); + SECTION( "to-hit rating" ) { + // To-hit rating comes from the "to_hit" attribute in the item's JSON. + + std::vector to_hit = { iteminfo_parts::BASE_TOHIT }; + + CHECK( item_info_str( rock, to_hit ) == + "--\n" + " To-hit bonus: -2\n" ); + + CHECK( item_info_str( halligan, to_hit ) == + "--\n" + " To-hit bonus: +2\n" ); + + CHECK( item_info_str( mr_pointy, to_hit ) == + "--\n" + " To-hit bonus: -1\n" ); + + CHECK( item_info_str( arrow, to_hit ) == + "--\n" + " To-hit bonus: +0\n" ); } - SECTION( "bash and pierce damage" ) { - test_info_equals( - item( "pointy_stick" ), q, - "--\n" - "Melee damage: Bash: 5" - " Pierce: 9" - " To-hit bonus: -1\n" - "Moves per attack: 100\n" - "Typical damage per second:\n" - "Best: 6.87" - " Vs. Agile: 3.20" - " Vs. Armored: 0.12\n" ); + SECTION( "base moves" ) { + // Moves are calculated in item::attack_time based on item volume, weight, and count. + // Those calculations are outside the scope of these tests, but we can at least ensure + // they have expected values before checking the item info string. + // If one of these fails, it suggests attack_time() changed: + REQUIRE( rock.attack_time() == 79 ); + REQUIRE( halligan.attack_time() == 145 ); + REQUIRE( mr_pointy.attack_time() == 100 ); + REQUIRE( arrow.attack_time() == 65 ); + + std::vector moves = { iteminfo_parts::BASE_MOVES }; + + CHECK( item_info_str( rock, moves ) == + "--\n" + "Moves per attack: 79\n" ); + + CHECK( item_info_str( halligan, moves ) == + "--\n" + "Moves per attack: 145\n" ); + + CHECK( item_info_str( mr_pointy, moves ) == + "--\n" + "Moves per attack: 100\n" ); + + CHECK( item_info_str( arrow, moves ) == + "--\n" + "Moves per attack: 65\n" ); } - SECTION( "melee and ranged damaged" ) { - test_info_equals( - item( "arrow_wood" ), q, - "--\n" - "Melee damage: Bash: 2" - " Cut: 1" - " To-hit bonus: +0\n" - "Moves per attack: 65\n" - "Typical damage per second:\n" - "Best: 4.90" - " Vs. Agile: 2.46" - " Vs. Armored: 0.00\n" ); + SECTION( "base damage per second" ) { + // Damage per second is dynamically calculated in item::dps and item::effective_dps based on + // many different factors, all outside the scope of these tests. Here we just hope they + // have the expected values in the item info summary. + + std::vector dps = { iteminfo_parts::BASE_DPS }; + + CHECK( item_info_str( rock, dps ) == + "--\n" + "Typical damage per second:\n" + "Best: 4.92" + " Vs. Agile: 2.05" + " Vs. Armored: 0.15\n" ); + + CHECK( item_info_str( halligan, dps ) == + "--\n" + "Typical damage per second:\n" + "Best: 9.38" + " Vs. Agile: 5.74" + " Vs. Armored: 2.84\n" ); + + CHECK( item_info_str( mr_pointy, dps ) == + "--\n" + "Typical damage per second:\n" + "Best: 6.87" + " Vs. Agile: 3.20" + " Vs. Armored: 0.12\n" ); + + CHECK( item_info_str( arrow, dps ) == + "--\n" + "Typical damage per second:\n" + "Best: 4.90" + " Vs. Agile: 2.46" + " Vs. Armored: 0.00\n" ); + } +} + +// Related JSON fields: +// "techniques" - list of technique ids, ex. [ "WBLOCK_1", "BRUTAL", "SWEEP" ] +// +// Technique descriptions are defined in data/json/techniques.json +// +// Functions: +// item::combat_info +TEST_CASE( "techniques when wielded", "[iteminfo][weapon][techniques]" ) +{ + clear_avatar(); + + item halligan( "test_halligan" ); + CHECK( item_info_str( halligan, { iteminfo_parts::DESCRIPTION_TECHNIQUES } ) == + "--\n" + "Techniques when wielded:" + " Brutal Strike:" + " Stun 1 turn, knockback 1 tile, crit only," + " Sweep Attack:" + " Down 2 turns, and" + " Block:" + " Medium blocking ability\n" ); + + item plank( "test_2x4" ); + CHECK( item_info_str( plank, { iteminfo_parts::DESCRIPTION_TECHNIQUES } ) == + "--\n" + "Techniques when wielded:" + " Block:" + " Medium blocking ability\n" ); +} + +// Related JSON fields: +// "covers" +// "coverage" +// "warmth" +// "encumbrance" +// +// Functions: +// item::armor_info +TEST_CASE( "armor coverage, warmth, and encumbrance", "[iteminfo][armor][coverage]" ) +{ + clear_avatar(); + + SECTION( "armor with coverage shows covered body parts, warmth, encumbrance, and protection values" ) { + // Long-sleeved shirt covering torso and arms + item longshirt( "test_longshirt" ); + REQUIRE( longshirt.get_covered_body_parts().any() ); + + CHECK( item_info_str( longshirt, { iteminfo_parts::ARMOR_BODYPARTS } ) == + "--\n" + "Covers:" + " The torso." + " The arms. \n" ); // FIXME: Remove trailing space + + CHECK( item_info_str( longshirt, { iteminfo_parts::ARMOR_LAYER } ) == + "--\n" + "Layer: Normal. \n" ); + + // Coverage and warmth are displayed together on a single line + std::vector cov_warm = { iteminfo_parts::ARMOR_COVERAGE, iteminfo_parts::ARMOR_WARMTH }; + REQUIRE( longshirt.get_coverage() == 90 ); + REQUIRE( longshirt.get_warmth() == 5 ); + CHECK( item_info_str( longshirt, cov_warm ) + == + "--\n" + "Coverage: 90% Warmth: 5\n" ); + + REQUIRE( longshirt.get_encumber( g->u ) == 3 ); + CHECK( item_info_str( longshirt, { iteminfo_parts::ARMOR_ENCUMBRANCE } ) == + "--\n" + "Encumbrance:" + " 3" + " (poor fit)\n" ); } - SECTION( "no damage" ) { - test_info_equals( item( "test_rag" ), q, "" ); + SECTION( "armor with no coverage omits irrelevant info" ) { + // Ear plugs with no coverage, and no other info to display + item ear_plugs( "test_ear_plugs" ); + REQUIRE_FALSE( ear_plugs.get_covered_body_parts().any() ); + + CHECK( item_info_str( ear_plugs, { iteminfo_parts::ARMOR_BODYPARTS, iteminfo_parts::ARMOR_LAYER, + iteminfo_parts::ARMOR_COVERAGE, iteminfo_parts::ARMOR_WARMTH, + iteminfo_parts::ARMOR_ENCUMBRANCE, iteminfo_parts::ARMOR_PROTECTION + } ) == + "--\n" + "Covers: Nothing.\n" ); } } -TEST_CASE( "techniques when wielded", "[item][iteminfo][weapon]" ) +// Related JSON fields: +// "covers" +// "flags" +// "power_armor" +// +// Functions: +// item::armor_fit_info +TEST_CASE( "armor fit and sizing", "[iteminfo][armor][fit]" ) +{ + clear_avatar(); + + std::vector varsize = { iteminfo_parts::DESCRIPTION_FLAGS_VARSIZE }; + std::vector sided = { iteminfo_parts::DESCRIPTION_FLAGS_SIDED }; + std::vector powerarmor = { iteminfo_parts::DESCRIPTION_FLAGS_POWERARMOR }; + std::vector powerarmor_rad = { iteminfo_parts::DESCRIPTION_FLAGS_POWERARMOR_RADIATIONHINT }; + + // TODO: Test items with these + //std::vector helmet_compat = { iteminfo_parts::DESCRIPTION_FLAGS_HELMETCOMPAT }; + //std::vector fits = { iteminfo_parts::DESCRIPTION_FLAGS_FITS }; + //std::vector irradiation = { iteminfo_parts::DESCRIPTION_IRRADIATION }; + + // Items with VARSIZE flag can be fitted + item socks( "test_socks" ); + CHECK( item_info_str( socks, varsize ) == + "--\n" + "* This clothing can be refitted.\n" ); + + // Items with "covers" LEG_EITHER, ARM_EITHER, FOOT_EITHER, HAND_EITHER are "sided" + item briefcase( "test_briefcase" ); + CHECK( item_info_str( briefcase, sided ) == + "--\n" + "* This item can be worn on either side of the body.\n" ); + + item power_armor( "test_power_armor" ); + CHECK( item_info_str( power_armor, powerarmor ) == + "--\n" + "* This gear is a part of power armor.\n" ); + CHECK( item_info_str( power_armor, powerarmor_rad ) == + "--\n" + "* When worn with a power armor helmet, it will" + " fully protect you from radiation.\n" ); +} + +// Armor protction is based on materials, thickness, and/or environmental protection rating. +// For armor defined in JSON: +// +// - "materials" determine the resistance factors (bash, cut, fire etc.) +// - "material_thickness" multiplies bash, cut, and bullet resistance +// - "environmental_protection" gives acid and fire resist (N/10 if less than 10) +// +// See doc/DEVELOPER_FAQ.md "How armor protection is calculated" for more. +// +// Materials and protection calculations are not tested here; only their display in item info. +// +// item::armor_protection_info +TEST_CASE( "armor protection", "[iteminfo][armor][protection]" ) { clear_avatar(); - iteminfo_query q = q_vec( { iteminfo_parts::DESCRIPTION_TECHNIQUES } ); - test_info_equals( - item( "test_halligan" ), q, - "--\n" - "Techniques when wielded:" - " Brutal Strike: Stun 1 turn, knockback 1 tile, crit only," - " Sweep Attack: Down 2 turns, and" - " Block: Medium blocking ability\n" ); + std::vector protection = { iteminfo_parts::ARMOR_PROTECTION }; + + // TODO: + // - Air filtration or gas mask (inactive/active) + // - Damaged armor reduces protection + + SECTION( "minimal protection from physical, no protection from environmental" ) { + // Long-sleeved shirt, material:cotton, thickness:1 + // 1/1/1 bash/cut/bullet x 1 thickness + // 0/0/0 acid/fire/env + item longshirt( "test_longshirt" ); + REQUIRE( longshirt.get_covered_body_parts().any() ); + REQUIRE( longshirt.bash_resist() == 1 ); + REQUIRE( longshirt.cut_resist() == 1 ); + REQUIRE( longshirt.bullet_resist() == 1 ); + REQUIRE( longshirt.acid_resist() == 0 ); + REQUIRE( longshirt.fire_resist() == 0 ); + REQUIRE( longshirt.get_env_resist() == 0 ); + + // Protection info displayed on two lines + CHECK( item_info_str( longshirt, protection ) == + "--\n" + "Protection:" + " Bash: 1" + " Cut: 1" + " Ballistic: 1\n" + " Acid: 0" + " Fire: 0" + " Environmental: 0\n" ); + } + + SECTION( "moderate protection from physical and environmental damage" ) { + // Hazmat suit, material:plastic, thickness:2 + // 2/2/2 bash/cut/bullet x 2 thickness + // 9/1/20 acid/fire/env + item hazmat( "test_hazmat_suit" ); + REQUIRE( hazmat.get_covered_body_parts().any() ); + REQUIRE( hazmat.bash_resist() == 4 ); + REQUIRE( hazmat.cut_resist() == 4 ); + REQUIRE( hazmat.bullet_resist() == 4 ); + REQUIRE( hazmat.acid_resist() == 9 ); + REQUIRE( hazmat.fire_resist() == 1 ); + REQUIRE( hazmat.get_env_resist() == 20 ); + + // Protection info displayed on two lines + CHECK( item_info_str( hazmat, protection ) == + "--\n" + "Protection:" + " Bash: 4" + " Cut: 4" + " Ballistic: 4\n" + " Acid: 9" + " Fire: 1" + " Environmental: 20\n" ); + } + + SECTION( "pet armor with good physical and environmental protection" ) { + // Kevlar cat harness, for reasons + // material:layered_kevlar, thickness:2 + // 2/3/5 bash/cut/bullet x 2 thickness + // 5/3/10 acid/fire/env + item meower_armor( "test_meower_armor" ); + REQUIRE( meower_armor.bash_resist() == 4 ); + REQUIRE( meower_armor.cut_resist() == 6 ); + REQUIRE( meower_armor.bullet_resist() == 10 ); + REQUIRE( meower_armor.acid_resist() == 5 ); + REQUIRE( meower_armor.fire_resist() == 3 ); + REQUIRE( meower_armor.get_env_resist() == 10 ); + + CHECK( item_info_str( meower_armor, protection ) == + "--\n" + "Protection:" + " Bash: 4" + " Cut: 6" + " Ballistic: 10\n" + " Acid: 5" + " Fire: 3" + " Environmental: 10\n" ); + } } -TEST_CASE( "armor coverage and protection values", "[item][iteminfo][armor]" ) +// Related JSON fields: +// "fun" +// "time" +// "skill" +// "required_level" +// "max_level" +// "intelligence" +// +// Functions: +// item::book_info +TEST_CASE( "book info", "[iteminfo][book]" ) { clear_avatar(); - iteminfo_query q = q_vec( { iteminfo_parts::ARMOR_BODYPARTS, iteminfo_parts::ARMOR_LAYER, - iteminfo_parts::ARMOR_COVERAGE, iteminfo_parts::ARMOR_WARMTH, - iteminfo_parts::ARMOR_ENCUMBRANCE, iteminfo_parts::ARMOR_PROTECTION - } ); - SECTION( "shows coverage, encumbrance, and protection for armor with coverage" ) { - test_info_equals( - item( "test_longshirt" ), q, - "--\n" - // NOLINTNEXTLINE(cata-text-style) - "Covers: The torso. The arms. \n" - // NOLINTNEXTLINE(cata-text-style) - "Layer: Normal. \n" - "Coverage: 90% Warmth: 5\n" - "--\n" - "Encumbrance: 3 (poor fit)\n" - "Protection: Bash: 1 Cut: 1 Ballistic: 1\n" - " Acid: 0 Fire: 0 Environmental: 0\n" ); + // Parts to test + std::vector summary = { iteminfo_parts::BOOK_SUMMARY }; + std::vector reqs_begin = { iteminfo_parts::BOOK_REQUIREMENTS_BEGINNER }; + std::vector skill_min = { iteminfo_parts::BOOK_SKILLRANGE_MIN }; + std::vector skill_max = { iteminfo_parts::BOOK_SKILLRANGE_MAX }; + std::vector reqs_int = { iteminfo_parts::BOOK_REQUIREMENTS_INT }; + std::vector morale = { iteminfo_parts::BOOK_MORALECHANGE }; + std::vector time_per_chapter = { iteminfo_parts::BOOK_TIMEPERCHAPTER }; + + // TODO: include test for these: + // std::vector num_unread = { iteminfo_parts::BOOK_NUMUNREADCHAPTERS }; + // std::vector included_recipes = { iteminfo_parts::BOOK_INCLUDED_RECIPES }; + + item dragon( "test_dragon_book" ); + item cmdline( "test_cmdline_book" ); + // TODO: add martial arts book to test data + + REQUIRE( dragon.is_book() ); + REQUIRE( cmdline.is_book() ); + + // summary is shown for martial arts books and "just for fun" books, but no others + WHEN( "book has not been identified" ) { + THEN( "some basic book info is shown" ) { + // Some info is always shown + CHECK( item_info_str( cmdline, summary ) == + "--\n" + "Just for fun.\n" ); + + CHECK( item_info_str( cmdline, reqs_begin ) == + "--\n" + "It can be understood by beginners.\n" ); + } + + THEN( "other book info is hidden" ) { + CHECK( item_info_str( cmdline, skill_min ).empty() ); + CHECK( item_info_str( cmdline, skill_max ).empty() ); + CHECK( item_info_str( cmdline, reqs_int ).empty() ); + CHECK( item_info_str( cmdline, morale ).empty() ); + CHECK( item_info_str( cmdline, time_per_chapter ).empty() ); + } } - SECTION( "omits irrelevant info if it covers nothing" ) { - test_info_equals( - item( "test_ear_plugs" ), q, - "--\n" - "Covers: Nothing.\n" ); + WHEN( "book has been identified" ) { + g->u.do_read( cmdline ); + g->u.do_read( dragon ); + + THEN( "some basic book info is shown" ) { + CHECK( item_info_str( cmdline, summary ) == + "--\n" + "Just for fun.\n" ); + + CHECK( item_info_str( cmdline, reqs_begin ) == + "--\n" + "It can be understood by beginners.\n" ); + + } + + THEN( "morale (fun) info is shown" ) { + // JSON field: "fun" + CHECK( item_info_str( cmdline, morale ) == + "--\n" + "Reading this book affects your morale by +2\n" ); + + CHECK( item_info_str( dragon, morale ) == + "--\n" + "Reading this book affects your morale by -1\n" ); + } + + THEN( "time per chapter is shown" ) { + // JSON field: "time" + CHECK( item_info_str( cmdline, time_per_chapter ) == + "--\n" + "A chapter of this book takes 5" + " minutes to read.\n" ); + + CHECK( item_info_str( dragon, time_per_chapter ) == + "--\n" + "A chapter of this book takes 50" + " minutes to read.\n" ); + } + + THEN( "book requirement info is shown" ) { + // Book with skill min/max and intelligence requirement + // JSON fields: "required_level", "max_level", "intelligence" + CHECK( item_info_str( dragon, skill_min ) == + "--\n" + "Requires computers level 4 to understand.\n" ); + + CHECK( item_info_str( dragon, skill_max ) == + "--\n" + "Can bring your computers skill to 7.\n" + "Your current computers skill is 0.\n" ); + + CHECK( item_info_str( dragon, reqs_int ) == + "--\n" + "Requires intelligence of 12 to easily read.\n" ); + } + + THEN( "no requirement info is shown if book has none" ) { + // Book with no requirements + CHECK( item_info_str( cmdline, skill_min ).empty() ); + CHECK( item_info_str( cmdline, skill_max ).empty() ); + CHECK( item_info_str( cmdline, reqs_int ).empty() ); + } } } -TEST_CASE( "ranged weapon attributes", "[item][iteminfo][weapon][ranged][gun]" ) +// Related JSON fields: +// "range" +// "ranged_damage" +// - "amount" +// "skill" +// "critical_multiplier" (ammo) +// "magazines" +// +// Functions: +// item::gun_info +TEST_CASE( "gun or other ranged weapon attributes", "[iteminfo][weapon][gun]" ) { clear_avatar(); + item compbow( "test_compbow" ); + item glock( "test_glock" ); + item rag( "test_rag" ); + + SECTION( "weapon damage including floating-point multiplier" ) { + // Ranged damage info is displayed on a single line, in three parts: + // + // - base ranged damage (GUN_DAMAGE) + // - floating-point multiplier (GUN_DAMAGE_AMMOPROP) + // - total damage calculation (GUN_DAMAGE_TOTAL) + // + std::vector damage = { iteminfo_parts::GUN_DAMAGE, + iteminfo_parts::GUN_DAMAGE_AMMOPROP, + iteminfo_parts::GUN_DAMAGE_TOTAL + }; + + CHECK( item_info_str( compbow, damage ) == + "--\n" + "Ranged damage:" + " 18*1.50 = 27\n" ); + } + // TODO: Test glock damage with and without ammo (test_glock has -1 damage when unloaded) + + SECTION( "maximum range" ) { + std::vector max_range = { iteminfo_parts::GUN_MAX_RANGE }; + + CHECK( item_info_str( compbow, max_range ) == + "--\n" + "Maximum range: 18\n" ); + + CHECK( item_info_str( glock, max_range ) == + "--\n" + "Maximum range: 14\n" ); + } + SECTION( "skill used" ) { - iteminfo_query q = q_vec( { iteminfo_parts::GUN_USEDSKILL } ); - test_info_equals( - item( "test_compbow" ), q, - "--\n" - "Skill used: archery\n" ); + std::vector used_skill = { iteminfo_parts::GUN_USEDSKILL }; + + CHECK( item_info_str( compbow, used_skill ) == + "--\n" + "Skill used: archery\n" ); + + CHECK( item_info_str( glock, used_skill ) == + "--\n" + "Skill used: handguns\n" ); } SECTION( "ammo capacity of weapon" ) { - iteminfo_query q = q_vec( { iteminfo_parts::GUN_CAPACITY } ); - test_info_equals( - item( "test_compbow" ), q, - "--\n" - "Capacity: 1 round of arrows\n" ); + std::vector gun_capacity = { iteminfo_parts::GUN_CAPACITY }; + + CHECK( item_info_str( compbow, gun_capacity ) == + "--\n" + "Capacity: 1 round of arrows\n" ); + + // FIXME: Why empty? + CHECK( item_info_str( glock, gun_capacity ).empty() ); } SECTION( "default ammo when weapon is unloaded" ) { - iteminfo_query q = q_vec( { iteminfo_parts::GUN_DEFAULT_AMMO } ); - test_info_equals( - item( "test_compbow" ), q, - "--\n" - "Weapon is not loaded, so stats below assume the default ammo:" - " wooden broadhead arrow\n" ); + std::vector default_ammo = { iteminfo_parts::GUN_DEFAULT_AMMO }; + + CHECK( item_info_str( compbow, default_ammo ) == + "--\n" + "Weapon is not loaded, so stats below assume the default ammo:" + " wooden broadhead arrow\n" ); + + CHECK( item_info_str( glock, default_ammo ) == + "--\n" + "Weapon is not loaded, so stats below assume the default ammo:" + " 9x19mm JHP\n" ); } - SECTION( "weapon damage including floating-point multiplier" ) { - iteminfo_query q = q_vec( { iteminfo_parts::GUN_DAMAGE, iteminfo_parts::GUN_DAMAGE_AMMOPROP, - iteminfo_parts::GUN_DAMAGE_TOTAL, iteminfo_parts::GUN_ARMORPIERCE, - iteminfo_parts::AMMO_DAMAGE_CRIT_MULTIPLIER - } ); - test_info_equals( - item( "test_compbow" ), q, - "--\n" - "Ranged damage:" - " 18*1.50 = 27\n" - "Critical multiplier: 10\n" - "Armor-pierce: 0\n" ); + SECTION( "critical multiplier" ) { + std::vector crit_multiplier = { iteminfo_parts::AMMO_DAMAGE_CRIT_MULTIPLIER }; + + CHECK( item_info_str( compbow, crit_multiplier ) == + "--\n" + "Critical multiplier: 10\n" ); + + CHECK( item_info_str( glock, crit_multiplier ) == + "--\n" + "Critical multiplier: 2\n" ); + } + + SECTION( "recoil" ) { + std::vector recoil = { iteminfo_parts::GUN_RECOIL }; + + CHECK( item_info_str( compbow, recoil ).empty() ); + + CHECK( item_info_str( glock, recoil ) == + "--\n" + "Effective recoil: 312\n" ); + } + + SECTION( "more gun stuff to split up" ) { + std::vector gun_type = { iteminfo_parts::GUN_TYPE }; + std::vector magazine = { iteminfo_parts::GUN_MAGAZINE }; + std::vector aim_stats = { iteminfo_parts::GUN_AIMING_STATS }; + + // FIXME: Why empty? + CHECK( item_info_str( glock, gun_type ) == "--\nType: \n" ); + CHECK( item_info_str( glock, magazine ).empty() ); + + CHECK( item_info_str( glock, aim_stats ) == + "--\n" + "Base aim speed: 104\n" + "Regular\n" + "Even chance of good hit at range: 3\n" + "Time to reach aim level: 115 moves \n" + "Careful\n" + "Even chance of good hit at range: 4\n" + "Time to reach aim level: 145 moves \n" + "Precise\n" + "Even chance of good hit at range: 6\n" + "Time to reach aim level: 174 moves \n" ); + } + + SECTION( "compatible magazines" ) { + std::vector allowed_mags = { iteminfo_parts::GUN_ALLOWED_MAGAZINES }; + + // expect magazine_compatible().empty() if magazine_integral() + + // Compound bow has integral magazine, not compatible ones + REQUIRE( compbow.magazine_integral() ); + REQUIRE( compbow.magazine_compatible().empty() ); + // No compatible magazine info should be shown + CHECK( item_info_str( compbow, allowed_mags ).empty() ); + + // Glock has compatible magazines, not integral + REQUIRE_FALSE( glock.magazine_integral() ); + REQUIRE_FALSE( glock.magazine_compatible().empty() ); + // Should show compatible magazines + CHECK( item_info_str( glock, allowed_mags ) == + "--\n" + "Compatible magazines:" + " Glock extended magazine and Glock magazine\n" ); + + // Rag does not have integral or compatible magazines + REQUIRE_FALSE( rag.magazine_integral() ); + REQUIRE( rag.magazine_compatible().empty() ); + // No magazine info should be shown + CHECK( item_info_str( rag, allowed_mags ).empty() ); + } + + SECTION( "armor piercing" ) { + CHECK( item_info_str( compbow, { iteminfo_parts::GUN_ARMORPIERCE } ) == + "--\n" + "Armor-pierce: 0\n" ); } SECTION( "time to reload weapon" ) { - iteminfo_query q = q_vec( { iteminfo_parts::GUN_RELOAD_TIME } ); - test_info_equals( - item( "test_compbow" ), q, - "--\n" - "Reload time: 110 moves \n" ); // NOLINT(cata-text-style) + CHECK( item_info_str( compbow, { iteminfo_parts::GUN_RELOAD_TIME } ) == + "--\n" + "Reload time: 110 moves \n" ); // NOLINT(cata-text-style) } SECTION( "weapon firing modes" ) { - iteminfo_query q = q_vec( { iteminfo_parts::GUN_FIRE_MODES } ); - test_info_equals( - item( "test_compbow" ), q, - "--\n" - "Fire modes: manual (1)\n" ); + CHECK( item_info_str( compbow, { iteminfo_parts::GUN_FIRE_MODES } ) == + "--\n" + "Fire modes: manual (1)\n" ); } SECTION( "weapon mods" ) { - iteminfo_query q = q_vec( { iteminfo_parts::DESCRIPTION_GUN_MODS } ); - test_info_equals( - item( "test_compbow" ), q, - "--\n" - "Mods: 0/2 accessories;" - " 0/1 dampening; 0/1 sights;" - " 0/1 stabilizer; 0/1 underbarrel.\n" ); + CHECK( item_info_str( compbow, { iteminfo_parts::DESCRIPTION_GUN_MODS } ) == + "--\n" + "Mods: 0/2 accessories;" + " 0/1 dampening; 0/1 sights;" + " 0/1 stabilizer; 0/1 underbarrel.\n" ); } SECTION( "weapon dispersion" ) { - iteminfo_query q = q_vec( { iteminfo_parts::GUN_DISPERSION } ); - test_info_equals( - item( "test_compbow" ), q, - "--\n" - "Dispersion: 850\n" ); + CHECK( item_info_str( compbow, { iteminfo_parts::GUN_DISPERSION } ) == + "--\n" + "Dispersion: 850\n" ); + } + + SECTION( "needing two hands to fire" ) { + REQUIRE( compbow.has_flag( "FIRE_TWOHAND" ) ); + + CHECK( item_info_str( compbow, { iteminfo_parts::DESCRIPTION_TWOHANDED } ) == + "--\n" + "This weapon needs two free hands to fire.\n" ); } } -TEST_CASE( "ammunition", "[item][iteminfo][ammo]" ) +// Functions: +// item::gun_info +TEST_CASE( "gun armor piercing, dispersion and other stats", "[iteminfo][gun][misc]" ) +{ + clear_avatar(); + + std::vector dmg_loaded = { iteminfo_parts::GUN_DAMAGE_LOADEDAMMO }; + std::vector ap_loaded = { iteminfo_parts::GUN_ARMORPIERCE_LOADEDAMMO }; + std::vector ap_total = { iteminfo_parts::GUN_ARMORPIERCE_TOTAL }; + std::vector disp_loaded = { iteminfo_parts::GUN_DISPERSION_LOADEDAMMO }; + std::vector disp_total = { iteminfo_parts::GUN_DISPERSION_TOTAL }; + std::vector disp_sight = { iteminfo_parts::GUN_DISPERSION_SIGHT }; + + // TODO: Test these + //std::vector recoil_bipod = { iteminfo_parts::GUN_RECOIL_BIPOD }; + //std::vector ammo_remain = { iteminfo_parts::AMMO_REMAINING }; + //std::vector ammo_upscost = { iteminfo_parts::AMMO_UPSCOST }; + //std::vector gun_casings = { iteminfo_parts::DESCRIPTION_GUN_CASINGS }; + + item glock( "test_glock" ); + + CHECK( item_info_str( glock, dmg_loaded ) == + "--\n+26\n" ); + + CHECK( item_info_str( glock, ap_loaded ) == + "--\n+0\n" ); + CHECK( item_info_str( glock, ap_total ) == + "--\n = 0\n" ); + + CHECK( item_info_str( glock, disp_loaded ) == + "--\n+60\n" ); + CHECK( item_info_str( glock, disp_total ) == + "--\n = 540\n" ); + + CHECK( item_info_str( glock, disp_sight ) == + "--\n" + "Sight dispersion: 30" + "+14" + " = 44\n" ); + + // TODO: Add a test gun with thest attributes + //CHECK( item_info_str( glock, recoil_bipod ).empty() ); + //CHECK( item_info_str( glock, ammo_remain ).empty() ); + //CHECK( item_info_str( glock, ammo_upscost ).empty() ); + //CHECK( item_info_str( glock, gun_casings ).empty() ); +} + +// Related JSON fields: +// "sight_dispersion" +// "aim_speed" +// "handling_modifier" +// "damage_modifier" +// - "amount" +// "flags" +// "mod_targets" +// "location" +// +// Functions: +// item::gunmod_info +TEST_CASE( "gunmod info", "[iteminfo][gunmod]" ) { clear_avatar(); - iteminfo_query q = q_vec( { iteminfo_parts::AMMO_REMAINING_OR_TYPES, iteminfo_parts::AMMO_DAMAGE_VALUE, - iteminfo_parts::AMMO_DAMAGE_PROPORTIONAL, iteminfo_parts::AMMO_DAMAGE_AP, - iteminfo_parts::AMMO_DAMAGE_RANGE, iteminfo_parts::AMMO_DAMAGE_DISPERSION, - iteminfo_parts::AMMO_DAMAGE_RECOIL, iteminfo_parts::AMMO_DAMAGE_CRIT_MULTIPLIER - } ); + + std::vector reach = { iteminfo_parts::DESCRIPTION_GUNMOD_REACH }; + std::vector no_sights = { iteminfo_parts::DESCRIPTION_GUNMOD_DISABLESSIGHTS }; + std::vector consumable = { iteminfo_parts::DESCRIPTION_GUNMOD_CONSUMABLE }; + std::vector disp_sight = { iteminfo_parts::GUNMOD_DISPERSION_SIGHT }; + std::vector aim_speed = { iteminfo_parts::GUNMOD_AIMSPEED }; + std::vector damage = { iteminfo_parts::GUNMOD_DAMAGE }; + std::vector handling = { iteminfo_parts::GUNMOD_HANDLING }; + std::vector usedon = { iteminfo_parts::GUNMOD_USEDON }; + std::vector location = { iteminfo_parts::GUNMOD_LOCATION }; + + // TODO for gunmod_info: + //std::vector gunmod = { iteminfo_parts::DESCRIPTION_GUNMOD }; + //std::vector armorpierce = { iteminfo_parts::GUNMOD_ARMORPIERCE }; + //std::vector ammo = { iteminfo_parts::GUNMOD_AMMO }; + //std::vector reload = { iteminfo_parts::GUNMOD_RELOAD }; + //std::vector strength = { iteminfo_parts::GUNMOD_STRENGTH }; + //std::vector add_mod = { iteminfo_parts::GUNMOD_ADD_MOD }; + //std::vector blacklist_mod = { iteminfo_parts::GUNMOD_BLACKLIST_MOD }; + + item supp( "test_crafted_suppressor" ); + REQUIRE( supp.is_gunmod() ); + + /* FIXME: This only applies if is_gun() ?? + CHECK( item_info_str( supp, { iteminfo_parts::DESCRIPTION_GUNMOD } ) == + "--\n" + "* This mod must be attached to a gun, it can not be fired separately.\n" ); + */ + + SECTION( "gunmod flags" ) { + REQUIRE( supp.has_flag( "REACH_ATTACK" ) ); + CHECK( item_info_str( supp, reach ) == + "--\n" + "When attached to a gun, allows making" + " reach melee attacks with it.\n" ); + + REQUIRE( supp.has_flag( "DISABLE_SIGHTS" ) ); + CHECK( item_info_str( supp, no_sights ) == + "--\n" + "This mod obscures sights of the base weapon.\n" ); + + REQUIRE( supp.has_flag( "CONSUMABLE" ) ); + CHECK( item_info_str( supp, consumable ) == + "--\n" + "This mod might suffer wear when firing the base weapon.\n" ); + } + + CHECK( item_info_str( supp, disp_sight ) == + "--\n" + "Sight dispersion: 11\n" ); + + CHECK( item_info_str( supp, aim_speed ) == + "--\n" + "Aim speed: 4\n" ); + + CHECK( item_info_str( supp, damage ) == + "--\n" + "Damage: -5\n" ); + + CHECK( item_info_str( supp, handling ) == + "--\n" + "Handling modifier: +1\n" ); + + // FIXME: This is a different order than given in JSON, and not alphabetical either; + // could be made more predictable? + CHECK( item_info_str( supp, usedon ) == + "--\n" + "Used on: rifle and pistol\n" ); + + CHECK( item_info_str( supp, location ) == + "--\n" + "Location: muzzle\n" ); + +} + +// Functions: +// item::ammo_info +TEST_CASE( "ammunition", "[iteminfo][ammo]" ) +{ + clear_avatar(); + + std::vector ammo = { iteminfo_parts::AMMO_REMAINING_OR_TYPES, + iteminfo_parts::AMMO_DAMAGE_VALUE, + iteminfo_parts::AMMO_DAMAGE_PROPORTIONAL, + iteminfo_parts::AMMO_DAMAGE_AP, + iteminfo_parts::AMMO_DAMAGE_RANGE, + iteminfo_parts::AMMO_DAMAGE_DISPERSION, + iteminfo_parts::AMMO_DAMAGE_RECOIL, + iteminfo_parts::AMMO_DAMAGE_CRIT_MULTIPLIER + }; SECTION( "simple item with ammo damage" ) { - test_info_equals( - item( "test_rock" ), q, - "--\n" - "Ammunition type: rocks\n" - "Damage: 7 Armor-pierce: 0\n" - "Range: 10 Dispersion: 14\n" - "Recoil: 0 Critical multiplier: 2\n" ); + item rock( "test_rock" ); + + CHECK( item_info_str( rock, ammo ) == + "--\n" + "Ammunition type: rocks\n" + "Damage: 7 Armor-pierce: 0\n" + "Range: 10 Dispersion: 14\n" + "Recoil: 0 Critical multiplier: 2\n" ); + } + + SECTION( "batteries" ) { + item batt_dispose( "test_battery_disposable" ); + + // FIXME: is_battery is only true if type = "BATTERY" + // no items in the game have this property anymore + //REQUIRE( batt_dispose.is_battery() ); + + REQUIRE( batt_dispose.ammo_data() ); + REQUIRE( batt_dispose.ammo_data()->nname( 1 ) == "battery" ); + + // No ammo info should be displayed + CHECK( item_info_str( batt_dispose, ammo ).empty() ); } } -TEST_CASE( "nutrients in food", "[item][iteminfo][food]" ) +// Functions: +// item::food_info +TEST_CASE( "nutrients in food", "[iteminfo][food]" ) { clear_avatar(); - iteminfo_query q = q_vec( { iteminfo_parts::FOOD_NUTRITION, iteminfo_parts::FOOD_VITAMINS, - iteminfo_parts::FOOD_QUENCH - } ); + + item ice_cream( "icecream" ); + SECTION( "fixed nutrient values in regular item" ) { - item i( "icecream" ); - test_info_equals( - i, q, - "--\n" - "Calories (kcal): 325 " - "Quench: 0\n" - "Vitamins (RDA): Calcium (9%), Vitamin A (9%), and Vitamin B12 (11%)\n" ); + CHECK( item_info_str( ice_cream, { iteminfo_parts::FOOD_NUTRITION, iteminfo_parts::FOOD_QUENCH } ) + == + "--\n" + "Calories (kcal): 325" + " Quench: 0\n" ); + + CHECK( item_info_str( ice_cream, { iteminfo_parts::FOOD_VITAMINS } ) == + "--\n" + "Vitamins (RDA): Calcium (9%), Vitamin A (9%), and Vitamin B12 (11%)\n" ); } - SECTION( "nutrient ranges for recipe exemplars", "[item][iteminfo]" ) { - item i( "icecream" ); - i.set_var( "recipe_exemplar", "icecream" ); - test_info_equals( - i, q, - "--\n" - "Nutrition will vary with chosen ingredients.\n" - "Calories (kcal): 317-" - "469 Quench: 0\n" - "Vitamins (RDA): Calcium (7-28%), Iron (0-83%), " - "Vitamin A (3-11%), Vitamin B12 (2-6%), and Vitamin C (1-85%)\n" ); + SECTION( "nutrient ranges for recipe exemplars", "[iteminfo]" ) { + ice_cream.set_var( "recipe_exemplar", "icecream" ); + + CHECK( item_info_str( ice_cream, { iteminfo_parts::FOOD_NUTRITION, iteminfo_parts::FOOD_QUENCH } ) + == + "--\n" + "Nutrition will vary with chosen ingredients.\n" + "Calories (kcal):" + " 317-469" + " Quench: 0\n" ); + + CHECK( item_info_str( ice_cream, { iteminfo_parts::FOOD_VITAMINS } ) == + "--\n" + "Nutrition will vary with chosen ingredients.\n" + "Vitamins (RDA): Calcium (7-28%), Iron (0-83%), " + "Vitamin A (3-11%), Vitamin B12 (2-6%), and Vitamin C (1-85%)\n" ); } } -TEST_CASE( "food freshness and lifetime", "[item][iteminfo][food]" ) +// Related JSON fields: +// "spoils_in" +// +// Functions: +// item::food_info +TEST_CASE( "food freshness and lifetime", "[iteminfo][food]" ) { clear_avatar(); - iteminfo_query q = q_vec( { iteminfo_parts::FOOD_ROT } ); // Ensure test character has no skill estimating spoilage g->u.empty_skills(); REQUIRE_FALSE( g->u.can_estimate_rot() ); + item nuts( "test_pine_nuts" ); + REQUIRE( nuts.goes_bad() ); + + // TODO: + // - flags FREEZERBURN, MUSHY, NO_PARASITES + // - traits: SAPROVORE, bio_digestion + // - with can_estimate_rot() == true + SECTION( "food is fresh" ) { - test_info_equals( - item( "test_pine_nuts" ), q, - "--\n" - "* This food is perishable, and at room temperature has" - " an estimated nominal shelf life of 6 weeks.\n" - "* This food looks as fresh as it can be.\n" ); + REQUIRE( nuts.is_fresh() ); + CHECK( item_info_str( nuts, { iteminfo_parts::FOOD_ROT } ) == + "--\n" + "* This food is perishable, and at room temperature has" + " an estimated nominal shelf life of 6 weeks.\n" + "* This food looks as fresh as it can be.\n" ); } SECTION( "food is old" ) { - item nuts( "test_pine_nuts" ); nuts.mod_rot( nuts.type->comestible->spoils ); - test_info_equals( - nuts, q, - "--\n" - "* This food is perishable, and at room temperature has" - " an estimated nominal shelf life of 6 weeks.\n" - "* This food looks old. It's on the brink of becoming inedible.\n" ); + REQUIRE( nuts.is_going_bad() ); + CHECK( item_info_str( nuts, { iteminfo_parts::FOOD_ROT } ) == + "--\n" + "* This food is perishable, and at room temperature has" + " an estimated nominal shelf life of 6 weeks.\n" + "* This food looks old. It's on the brink of becoming inedible.\n" ); + } +} + +// Related JSON fields: +// "fun" +// "charges" +// +// Functions: +// item::food_info +TEST_CASE( "basic food info", "[iteminfo][food]" ) +{ + clear_avatar(); + + std::vector joy = { iteminfo_parts::FOOD_JOY }; + std::vector portions = { iteminfo_parts::FOOD_PORTIONS }; + std::vector consume_time = { iteminfo_parts::FOOD_CONSUME_TIME }; + + // TODO: Test these: + //std::vector smell = { iteminfo_parts::FOOD_SMELL }; + //std::vector vit_effects = { iteminfo_parts::FOOD_VIT_EFFECTS }; + + item apple( "test_apple" ); + item nuts( "test_pine_nuts" ); + item wine( "test_wine" ); + + REQUIRE( apple.is_food() ); + REQUIRE( nuts.is_food() ); + REQUIRE( wine.is_food() ); + + CHECK( item_info_str( apple, joy ) == + "--\nEnjoyability: 10\n" ); + CHECK( item_info_str( apple, portions ) == + "--\nPortions: 1\n" ); + CHECK( item_info_str( apple, consume_time ) == + "--\nConsume time: 50 seconds\n" ); + + CHECK( item_info_str( nuts, joy ) == + "--\nEnjoyability: 2\n" ); + CHECK( item_info_str( nuts, portions ) == + "--\nPortions: 4\n" ); + CHECK( item_info_str( nuts, consume_time ) == + "--\nConsume time: 12 seconds\n" ); + + CHECK( item_info_str( wine, joy ) == + "--\nEnjoyability: -5\n" ); + CHECK( item_info_str( wine, portions ) == + "--\nPortions: 7\n" ); + CHECK( item_info_str( wine, consume_time ) == + "--\nConsume time: 2 seconds\n" ); +} + +// Related JSON fields: +// "material" (determines allergen) +// +// Functions: +// item::food_info +TEST_CASE( "food character is allergic to", "[iteminfo][food][allergy]" ) +{ + clear_avatar(); + + std::vector allergen = { iteminfo_parts::FOOD_ALLERGEN }; + + GIVEN( "character allergic to fruit" ) { + g->u.toggle_trait( trait_id( "ANTIFRUIT" ) ); + REQUIRE( g->u.has_trait( trait_id( "ANTIFRUIT" ) ) ); + + THEN( "fruit indicates an allergic reaction" ) { + item apple( "test_apple" ); + REQUIRE( apple.has_flag( "ALLERGEN_FRUIT" ) ); + CHECK( item_info_str( apple, allergen ) == + "--\n" + "* This food will cause an allergic reaction.\n" ); + } + + THEN( "nuts do not indicate an allergic reaction" ) { + item nuts( "test_pine_nuts" ); + REQUIRE_FALSE( nuts.has_flag( "ALLERGEN_FRUIT" ) ); + CHECK( item_info_str( nuts, allergen ).empty() ); + } + } +} + +// Related JSON fields: +// "flags" +// +// Functions: +// item::food_info +TEST_CASE( "food with hidden poison or hallucinogen", "[iteminfo][food][poison][hallu]" ) +{ + clear_avatar(); + + // Test food with hidden effects + item almond( "test_bitter_almond" ); + item nutmeg( "test_hallu_nutmeg" ); + + // Ensure they are food + REQUIRE( almond.is_food() ); + REQUIRE( nutmeg.is_food() ); + + // Ensure they have the expected flags + REQUIRE( almond.has_flag( "HIDDEN_POISON" ) ); + REQUIRE( nutmeg.has_flag( "HIDDEN_HALLU" ) ); + + // Parts flags for display + std::vector poison = { iteminfo_parts::FOOD_POISON }; + std::vector hallu = { iteminfo_parts::FOOD_HALLUCINOGENIC }; + + // Seeing hidden poison or hallucinogen depends on character survival skill + // At low level, no info is shown + GIVEN( "survival 2" ) { + g->u.set_skill_level( skill_id( "survival" ), 2 ); + REQUIRE( g->u.get_skill_level( skill_id( "survival" ) ) == 2 ); + + THEN( "cannot see hidden poison or hallucinogen" ) { + CHECK( item_info_str( almond, poison ).empty() ); + CHECK( item_info_str( nutmeg, hallu ).empty() ); + } + } + + // Hidden poison is visible at survival level 3 + GIVEN( "survival 3" ) { + g->u.set_skill_level( skill_id( "survival" ), 3 ); + REQUIRE( g->u.get_skill_level( skill_id( "survival" ) ) == 3 ); + + THEN( "can see hidden poison" ) { + CHECK( item_info_str( almond, poison ) == + "--\n" + "* On closer inspection, this appears to be" + " poisonous.\n" ); + } + + THEN( "cannot see hidden hallucinogen" ) { + CHECK( item_info_str( nutmeg, hallu ).empty() ); + } + } + + // Hidden hallucinogen is not visible until survival level 5 + GIVEN( "survival 4" ) { + g->u.set_skill_level( skill_id( "survival" ), 4 ); + REQUIRE( g->u.get_skill_level( skill_id( "survival" ) ) == 4 ); + + THEN( "still cannot see hidden hallucinogen" ) { + CHECK( item_info_str( nutmeg, hallu ).empty() ); + } + } + + GIVEN( "survival 5" ) { + g->u.set_skill_level( skill_id( "survival" ), 5 ); + REQUIRE( g->u.get_skill_level( skill_id( "survival" ) ) == 5 ); + + THEN( "can see hidden hallucinogen" ) { + CHECK( item_info_str( nutmeg, hallu ) == + "--\n" + "* On closer inspection, this appears to be" + " hallucinogenic.\n" ); + } + } +} + +// Related JSON fields: +// "material" (human flesh is "hflesh") +// +// Functions: +// item::food_info +TEST_CASE( "food that is made of human flesh", "[iteminfo][food][cannibal]" ) +{ + // TODO: Test food that is_tainted(): "This food is *tainted* and will poison you" + + clear_avatar(); + + std::vector cannibal = { iteminfo_parts::FOOD_CANNIBALISM }; + + item thumb( "test_thumb" ); + REQUIRE( thumb.has_flag( "CANNIBALISM" ) ); + + GIVEN( "character is not a cannibal" ) { + REQUIRE_FALSE( g->u.has_trait( trait_id( "CANNIBAL" ) ) ); + THEN( "human flesh is indicated as bad" ) { + // red highlight + CHECK( item_info_str( thumb, cannibal ) == + "--\n" + "* This food contains human flesh.\n" ); + } + } + + GIVEN( "character is a cannibal" ) { + g->u.toggle_trait( trait_id( "CANNIBAL" ) ); + REQUIRE( g->u.has_trait( trait_id( "CANNIBAL" ) ) ); + + THEN( "human flesh is indicated as good" ) { + // green highlight + CHECK( item_info_str( thumb, cannibal ) == + "--\n" + "* This food contains human flesh.\n" ); + } } } -TEST_CASE( "item conductivity", "[item][iteminfo][conductivity]" ) +// Related JSON fields: +// "flags" +// +// Functions: +// item::final_info +// FIXME: Move conducivity out of final_info +TEST_CASE( "item conductivity", "[iteminfo][conductivity]" ) { clear_avatar(); - iteminfo_query q = q_vec( { iteminfo_parts::DESCRIPTION_CONDUCTIVITY } ); + + std::vector conductivity = { iteminfo_parts::DESCRIPTION_CONDUCTIVITY }; SECTION( "non-conductive items" ) { - test_info_equals( - item( "test_2x4" ), q, - "--\n" - "* This item does not conduct electricity.\n" ); - test_info_equals( - item( "test_fire_ax" ), q, - "--\n" - "* This item does not conduct electricity.\n" ); + item plank( "test_2x4" ); + REQUIRE_FALSE( plank.conductive() ); + CHECK( item_info_str( plank, conductivity ) == + "--\n" + "* This item does not conduct electricity.\n" ); + + item axe( "test_fire_ax" ); + REQUIRE_FALSE( axe.conductive() ); + CHECK( item_info_str( axe, conductivity ) == + "--\n" + "* This item does not conduct electricity.\n" ); } SECTION( "conductive items" ) { - test_info_equals( - item( "test_pipe" ), q, - "--\n" - "* This item conducts electricity.\n" ); - test_info_equals( - item( "test_halligan" ), q, - "--\n" - "* This item conducts electricity.\n" ); + // Pipe is made of conductive material (steel) + item pipe( "test_pipe" ); + REQUIRE( pipe.conductive() ); + CHECK( item_info_str( pipe, conductivity ) == + "--\n" + "* This item conducts electricity.\n" ); + + // Halligan bar is made of conductive material (steel) + item halligan( "test_halligan" ); + REQUIRE( halligan.conductive() ); + CHECK( item_info_str( halligan, conductivity ) == + "--\n" + "* This item conducts electricity.\n" ); + + // Balloon is made of non-conductive rubber, but has CONDUCTIVE flag + item balloon( "test_balloon" ); + REQUIRE( balloon.conductive() ); + CHECK( item_info_str( balloon, conductivity ) == + "--\n" + "* This item effectively conducts" + " electricity, as it has no guard.\n" ); } } -TEST_CASE( "list of item qualities", "[item][iteminfo][quality]" ) +// Related JSON fields: +// "qualities" +// +// Functions: +// item::qualities_info +TEST_CASE( "list of item qualities", "[iteminfo][quality]" ) { clear_avatar(); - iteminfo_query q = q_vec( { iteminfo_parts::QUALITIES } ); + + std::vector qualities = { iteminfo_parts::QUALITIES }; SECTION( "Halligan bar" ) { - test_info_equals( - item( "test_halligan" ), q, - "--\n" - "Has level 1 digging quality.\n" - "Has level 2 hammering quality.\n" - "Has level 4 prying quality.\n" ); + item halligan( "test_halligan" ); + CHECK( item_info_str( halligan, qualities ) == + "--\n" + "Has level 1 digging quality.\n" + "Has level 2 hammering quality.\n" + "Has level 4 prying quality.\n" ); } SECTION( "bottle jack" ) { - override_option opt( "USE_METRIC_WEIGHTS", "lbs" ); - test_info_equals( - item( "test_jack_small" ), q, - "--\n" - "Has level 4 jacking quality and is rated at 4409 lbs\n" ); + item jack( "test_jack_small" ); + + SECTION( "metric units" ) { + override_option opt_kg( "USE_METRIC_WEIGHTS", "kg" ); + CHECK( item_info_str( jack, qualities ) == + "--\n" + "Has level 4 jacking quality and is rated at" + " 2000 kg\n" ); + } + SECTION( "imperial units" ) { + override_option opt_lbs( "USE_METRIC_WEIGHTS", "lbs" ); + CHECK( item_info_str( jack, qualities ) == + "--\n" + "Has level 4 jacking quality and is rated at" + " 4409 lbs\n" ); + } } SECTION( "sonic screwdriver" ) { - test_info_equals( - item( "test_sonic_screwdriver" ), q, - "--\n" - "Has level 2 prying quality.\n" - "Has level 2 screw driving quality.\n" - "Has level 1 fine screw driving quality.\n" - "Has level 1 bolt turning quality.\n" ); + item sonic( "test_sonic_screwdriver" ); + + CHECK( item_info_str( sonic, qualities ) == + "--\n" + "Has level 2 prying quality.\n" + "Has level 2 screw driving quality.\n" + "Has level 1 fine screw driving quality.\n" + "Has level 1 bolt turning quality.\n" ); } } -TEST_CASE( "repairable and with what tools", "[item][iteminfo][repair]" ) +// Related JSON fields: +// "flags" (USE_UPS, RECHARGE) +// +// Functions: +// item::tool_info +TEST_CASE( "tool info", "[iteminfo][tool]" ) { + // TODO: Find a tool using this + //std::vector mag_current = { iteminfo_parts::TOOL_MAGAZINE_CURRENT }; + clear_avatar(); - iteminfo_query q = q_vec( { iteminfo_parts::DESCRIPTION_REPAIREDWITH } ); - test_info_contains( - item( "test_halligan" ), q, - "Repair using extended toolset, arc welder, or makeshift arc welder.\n" ); + SECTION( "maximum charges" ) { + std::vector capacity = { iteminfo_parts::TOOL_CAPACITY }; + + item matches( "test_matches" ); + CHECK( item_info_str( matches, capacity ) == + "--\n" + "Maximum 20 charges of match.\n" ); + } + + SECTION( "tool with charges" ) { + std::vector charges = { iteminfo_parts::TOOL_CHARGES }; + + item matches( "test_matches" ); + matches.ammo_set( itype_id( "match" ) ); + REQUIRE( matches.ammo_remaining() > 0 ); - test_info_contains( - item( "test_hazmat_suit" ), q, - "Repair using soldering iron, TEST soldering iron, or extended toolset.\n" ); + CHECK( item_info_str( matches, charges ) == + "--\n" + "Charges: 20\n" ); + } - test_info_contains( - item( "test_rock" ), q, "* This item is not repairable.\n" ); + SECTION( "UPS charged tool" ) { + std::vector recharge_ups = { iteminfo_parts::DESCRIPTION_RECHARGE_UPSMODDED }; - test_info_contains( - item( "test_socks" ), q, "* This item can be reinforced.\n" ); + item smartphone( "test_smart_phone" ); + REQUIRE( smartphone.has_flag( "USE_UPS" ) ); + + CHECK( item_info_str( smartphone, recharge_ups ) == + "--\n" + "* This tool has been modified to use a" + " universal power supply and is" + " not compatible with" + " standard batteries.\n" ); + } + + SECTION( "compatible magazines" ) { + std::vector magazine_compat = { iteminfo_parts::TOOL_MAGAZINE_COMPATIBLE }; + + // Rag has no magazine capacity + item rag( "test_rag" ); + REQUIRE_FALSE( rag.magazine_integral() ); + REQUIRE( rag.magazine_compatible().empty() ); + + CHECK( item_info_str( rag, magazine_compat ).empty() ); + + // Acetylene torch is a tool with compatible magazines + // Other tools with "Compatible magazine": electric hair trimmer, circular saw + item oxy_torch( "oxy_torch" ); + REQUIRE_FALSE( oxy_torch.magazine_integral() ); + REQUIRE_FALSE( oxy_torch.magazine_compatible().empty() ); + + CHECK( item_info_str( oxy_torch, magazine_compat ) == + "--\n" + "Compatible magazines: small welding tank and welding tank\n" ); + } } -TEST_CASE( "disassembly time and yield", "[item][iteminfo][disassembly]" ) +// Related JSON fields: +// "type": "bionic" +// "capacity" +// "occupied_bodyparts" +// "fuel_capacity" +// "env_protec" +// +// Functions: +// item::bionic_info +TEST_CASE( "bionic info", "[iteminfo][bionic]" ) { + // TODO: Add a test for this + //std::vector slots = { iteminfo_parts::DESCRIPTION_CBM_SLOTS }; + // FIXME: bionic_info has NO OTHER iteminfo_parts filters! + + // TODO: Add tests for: + // - bash protection + // - cut protection + // - balliastic protection + // - stat bonus + // - weight capacity modifier + // - weight capacity bonus + clear_avatar(); - iteminfo_query q = q_vec( { iteminfo_parts::DESCRIPTION_COMPONENTS_DISASSEMBLE } ); - test_info_equals( - item( "test_soldering_iron" ), q, - "--\n" - "Disassembly takes about 20 minutes and might yield:" - " 2 electronic scraps, copper (1), scrap metal (1), and copper wire (5).\n" ); + item burner( "bio_ethanol" ); + item power( "bio_power_storage" ); + item nostril( "bio_nostril" ); + item purifier( "bio_purifier" ); + + REQUIRE( burner.is_bionic() ); + REQUIRE( power.is_bionic() ); + REQUIRE( nostril.is_bionic() ); + REQUIRE( purifier.is_bionic() ); + + CHECK( item_info_str( burner, {} ) == + "--\n" + "* This bionic can produce power from the following fuels:" + " ethanol," + " methanol," + " and denatured alcohol\n" ); + + // NOTE: No trailing newline + CHECK( item_info_str( power, {} ) == + "--\n" + "Power Capacity:" + " 100000000 mJ" ); + + // NOTE: Funky trailing space + CHECK( item_info_str( nostril, {} ) == + "--\n" + "Encumbrance: \n" + "Mouth 10 " ); + + CHECK( item_info_str( purifier, {} ) == + "--\n" + "Environmental Protection: \n" + "Mouth 7 " ); +} + +// Functions: +// item::repair_info +TEST_CASE( "repairable and with what tools", "[iteminfo][repair]" ) +{ + clear_avatar(); - test_info_equals( - item( "test_sheet_metal" ), q, - "--\n" - "Disassembly takes about 2 minutes and might yield: TEST small metal sheet (24).\n" ); + item halligan( "test_halligan" ); + item hazmat( "test_hazmat_suit" ); + item rock( "test_rock" ); + + std::vector repaired = { iteminfo_parts::DESCRIPTION_REPAIREDWITH }; + + // TODO: Move reinforcement to a different part flag ? Repair tools interfere here (especially + // with Magiclysm, which has enchanted tailor's kit) + /* + item socks( "test_socks" ); + CHECK( item_info_str( socks, repaired ) == + "--\n" + "* This item can be reinforced.\n" ); + */ + + CHECK( item_info_str( halligan, repaired ) == + "--\n" + "Repair using extended toolset, arc welder, or makeshift arc welder.\n" ); + + // FIXME: Use an item that can only be repaired by test tools + CHECK( item_info_str( hazmat, repaired ) == + "--\n" + "Repair using soldering iron, TEST soldering iron, or extended toolset.\n" ); + + CHECK( item_info_str( rock, repaired ) == + "--\n" + "* This item is not repairable.\n" ); } -TEST_CASE( "item description flags", "[item][iteminfo]" ) +// Functions: +// item::disassembly_info +TEST_CASE( "disassembly time and yield", "[iteminfo][disassembly]" ) { clear_avatar(); - iteminfo_query q = q_vec( { iteminfo_parts::DESCRIPTION_FLAGS } ); - test_info_equals( - item( "test_halligan" ), q, - "--\n" - "* This item can be clipped on to a belt loop of the appropriate size.\n" - "* As a weapon, this item is well-made and will" - " withstand the punishment of combat.\n" ); + std::vector disassemble = { iteminfo_parts::DESCRIPTION_COMPONENTS_DISASSEMBLE }; + + item iron( "test_soldering_iron" ); + item metal( "test_sheet_metal" ); + + CHECK( item_info_str( iron, disassemble ) == + "--\n" + "Disassembly takes about 20 minutes and might yield:" + " 2 electronic scraps, copper (1), scrap metal (1), and copper wire (5).\n" ); + + CHECK( item_info_str( metal, disassemble ) == + "--\n" + "Disassembly takes about 2 minutes and might yield:" + " TEST small metal sheet (24).\n" ); +} + +// Related JSON fields: +// "flags" +// +// The DESCRIPTION_FLAGS part shows descriptions of item "flags" from data/json/flags.json. +// +// Functions: +// item::final_info +// FIXME: Factor out of final_info +TEST_CASE( "item description flags", "[iteminfo][flags]" ) +{ + clear_avatar(); - test_info_equals( - item( "test_hazmat_suit" ), q, - "--\n" - "* This gear completely protects you from" - " electric discharges.\n" - "* This gear completely protects you from" - " any gas.\n" - "* This gear is generally worn over clothing.\n" - "* This clothing completely protects you from" - " radiation.\n" - "* This clothing is designed to keep you dry in the rain.\n" - "* This clothing won't let water through." - " Unless you jump in the river or something like that.\n" ); + std::vector flags = { iteminfo_parts::DESCRIPTION_FLAGS }; + + item halligan( "test_halligan" ); + item hazmat( "test_hazmat_suit" ); + + // Halligan bar has a couple flags + REQUIRE( halligan.has_flag( "BELT_CLIP" ) ); + REQUIRE( halligan.has_flag( "DURABLE_MELEE" ) ); + CHECK( item_info_str( halligan, flags ) == + "--\n" + "* This item can be clipped on to a belt loop of the appropriate size.\n" + "* As a weapon, this item is well-made and will" + " withstand the punishment of combat.\n" ); + + // Hazmat suit has a lot of flags + REQUIRE( hazmat.has_flag( "ELECTRIC_IMMUNE" ) ); + REQUIRE( hazmat.has_flag( "GAS_PROOF" ) ); + REQUIRE( hazmat.has_flag( "OUTER" ) ); + REQUIRE( hazmat.has_flag( "RAD_PROOF" ) ); + REQUIRE( hazmat.has_flag( "RAINPROOF" ) ); + REQUIRE( hazmat.has_flag( "WATERPROOF" ) ); + CHECK( item_info_str( hazmat, flags ) == + "--\n" + "* This gear completely protects you from" + " electric discharges.\n" + "* This gear completely protects you from" + " any gas.\n" + "* This gear is generally worn over clothing.\n" + "* This clothing completely protects you from" + " radiation.\n" + "* This clothing is designed to keep you dry in the rain.\n" + "* This clothing won't let water through." + " Unless you jump in the river or something like that.\n" ); } -TEST_CASE( "show available recipes with item as an ingredient", "[item][iteminfo][recipes]" ) +// Functions: +// item::final_info +TEST_CASE( "show available recipes with item as an ingredient", "[iteminfo][recipes]" ) { clear_avatar(); - iteminfo_query q = q_vec( { iteminfo_parts::DESCRIPTION_APPLICABLE_RECIPES } ); + const recipe *purtab = &recipe_id( "pur_tablets" ).obj(); recipe_subset &known_recipes = const_cast( g->u.get_learned_recipes() ); known_recipes.clear(); + // FIXME: Factor out of final_info + std::vector crafting = { iteminfo_parts::DESCRIPTION_APPLICABLE_RECIPES }; + GIVEN( "character has a potassium iodide tablet and no skill" ) { g->u.worn.push_back( item( "backpack" ) ); item &iodine = g->u.i_add( item( "iodine" ) ); @@ -587,9 +1938,8 @@ TEST_CASE( "show available recipes with item as an ingredient", "[item][iteminfo REQUIRE( !g->u.knows_recipe( purtab ) ); THEN( "nothing is craftable from it" ) { - test_info_equals( - iodine, q, - "--\nYou know of nothing you could craft with it.\n" ); + CHECK( item_info_str( iodine, crafting ) == + "--\nYou know of nothing you could craft with it.\n" ); } WHEN( "they acquire the needed skills" ) { @@ -597,9 +1947,8 @@ TEST_CASE( "show available recipes with item as an ingredient", "[item][iteminfo REQUIRE( g->u.get_skill_level( purtab->skill_used ) == purtab->difficulty ); THEN( "still nothing is craftable from it" ) { - test_info_equals( - iodine, q, - "--\nYou know of nothing you could craft with it.\n" ); + CHECK( item_info_str( iodine, crafting ) == + "--\nYou know of nothing you could craft with it.\n" ); } WHEN( "they have no book, but have the recipe memorized" ) { @@ -607,11 +1956,10 @@ TEST_CASE( "show available recipes with item as an ingredient", "[item][iteminfo REQUIRE( g->u.knows_recipe( purtab ) ); THEN( "they can use potassium iodide tablets to craft it" ) { - test_info_equals( - iodine, q, - "--\n" - "You could use it to craft: " - "water purification tablet\n" ); + CHECK( item_info_str( iodine, crafting ) == + "--\n" + "You could use it to craft: " + "water purification tablet\n" ); } } @@ -623,44 +1971,61 @@ TEST_CASE( "show available recipes with item as an ingredient", "[item][iteminfo g->u.moves++; THEN( "they can use potassium iodide tablets to craft it" ) { - test_info_equals( - iodine, q, - "--\n" - "You could use it to craft: " - "water purification tablet\n" ); + CHECK( item_info_str( iodine, crafting ) == + "--\n" + "You could use it to craft: " + "water purification tablet\n" ); } } } } } +// Pocket tests below relate to te JSON "pocket_data" fields: +// +// - "max_contains_volume" +// - "max_contains_weight" +// - "max_item_length" +// - "max_item_volume" +// - "watertight" +// - "moves" +// - "rigid" + +// Functions: +// item_contents::info +// item_pocket::general_info TEST_CASE( "pocket info for a simple container", "[iteminfo][pocket][container]" ) { clear_avatar(); + item test_waterskin( "test_waterskin" ); + std::vector pockets = { iteminfo_parts::DESCRIPTION_POCKETS }; + override_option opt_vol( "VOLUME_UNITS", "l" ); override_option opt_weight( "USE_METRIC_WEIGHTS", "kg" ); override_option opt_dist( "DISTANCE_UNITS", "metric" ); - item test_waterskin( "test_waterskin" ); - // Simple containers with only one pocket show a "Total capacity" section // with all the pocket's restrictions and other attributes - test_info_equals( - test_waterskin, q_vec( { iteminfo_parts::DESCRIPTION_POCKETS } ), - "--\n" - "Total capacity:\n" - "Volume: 1.50 L Weight: 3.00 kg\n" - "Maximum item length: 155 mm\n" - "Maximum item volume: 0.015 L\n" - "Base moves to remove item: 100\n" - "This pocket can contain a liquid.\n" ); + CHECK( item_info_str( test_waterskin, pockets ) == + "--\n" + "Total capacity:\n" + "Volume: 1.50 L Weight: 3.00 kg\n" + "Maximum item length: 12 cm\n" + "Maximum item volume: 0.015 L\n" + "Base moves to remove item: 220\n" + "This pocket can contain a liquid.\n" ); } +// Functions: +// item_contents::info +// item_pocket::general_info TEST_CASE( "pocket info units - imperial or metric", "[iteminfo][pocket][units]" ) { clear_avatar(); + item test_jug( "test_jug_plastic" ); + std::vector pockets = { iteminfo_parts::DESCRIPTION_POCKETS }; GIVEN( "metric units" ) { override_option opt_vol( "VOLUME_UNITS", "l" ); @@ -668,16 +2033,15 @@ TEST_CASE( "pocket info units - imperial or metric", "[iteminfo][pocket][units]" override_option opt_dist( "DISTANCE_UNITS", "metric" ); THEN( "pocket capacity is shown in liters, kilograms, and millimeters" ) { - test_info_equals( - test_jug, q_vec( { iteminfo_parts::DESCRIPTION_POCKETS } ), - "--\n" - "Total capacity:\n" - "Volume: 3.75 L Weight: 5.00 kg\n" - "Maximum item length: 226 mm\n" - "Maximum item volume: 0.015 L\n" - "Base moves to remove item: 100\n" - "This pocket is rigid.\n" - "This pocket can contain a liquid.\n" ); + CHECK( item_info_str( test_jug, pockets ) == + "--\n" + "Total capacity:\n" + "Volume: 3.75 L Weight: 5.00 kg\n" + "Maximum item length: 226 mm\n" + "Maximum item volume: 0.015 L\n" + "Base moves to remove item: 100\n" + "This pocket is rigid.\n" + "This pocket can contain a liquid.\n" ); } } @@ -687,52 +2051,57 @@ TEST_CASE( "pocket info units - imperial or metric", "[iteminfo][pocket][units]" override_option opt_dist( "DISTANCE_UNITS", "imperial" ); THEN( "pocket capacity is shown in quarts, pounds, and inches" ) { - test_info_equals( - test_jug, q_vec( { iteminfo_parts::DESCRIPTION_POCKETS } ), - "--\n" - "Total capacity:\n" - "Volume: 3.97 qt Weight: 11.02 lbs\n" - "Maximum item length: 8 in.\n" - "Maximum item volume: 0.016 qt\n" - "Base moves to remove item: 100\n" - "This pocket is rigid.\n" - "This pocket can contain a liquid.\n" ); + CHECK( item_info_str( test_jug, pockets ) == + "--\n" + "Total capacity:\n" + "Volume: 3.97 qt Weight: 11.02 lbs\n" + "Maximum item length: 8 in.\n" + "Maximum item volume: 0.016 qt\n" + "Base moves to remove item: 100\n" + "This pocket is rigid.\n" + "This pocket can contain a liquid.\n" ); } } } +// Functions: +// item_contents::info +// item_pocket::general_info TEST_CASE( "pocket info for a multi-pocket item", "[iteminfo][pocket][multiple]" ) { clear_avatar(); + item test_belt( "test_tool_belt" ); + std::vector pockets = { iteminfo_parts::DESCRIPTION_POCKETS }; + override_option opt_vol( "VOLUME_UNITS", "l" ); override_option opt_weight( "USE_METRIC_WEIGHTS", "kg" ); override_option opt_dist( "DISTANCE_UNITS", "metric" ); - item test_belt( "test_tool_belt" ); - // When two pockets have the same attributes, they are combined with a heading like: // // 2 Pockets with capacity: // Volume: ... Weight: ... // // The "Total capacity" indicates the sum Volume/Weight capacity of all pockets. - test_info_equals( - test_belt, q_vec( { iteminfo_parts::DESCRIPTION_POCKETS } ), - "--\n" - "Total capacity:\n" - "Volume: 6.00 L Weight: 4.00 kg\n" - "--\n" - "4 Pockets with capacity:\n" - "Volume: 1.50 L Weight: 1.00 kg\n" - "Maximum item length: 155 mm\n" - "Minimum item volume: 0.050 L\n" - "Base moves to remove item: 50\n" ); -} - -// Test vol_to_info function from item.cpp + CHECK( item_info_str( test_belt, pockets ) == + "--\n" + "Total capacity:\n" + "Volume: 6.00 L Weight: 4.00 kg\n" + "--\n" + "4 Pockets with capacity:\n" + "Volume: 1.50 L Weight: 1.00 kg\n" + "Maximum item length: 155 mm\n" + "Minimum item volume: 0.050 L\n" + "Base moves to remove item: 50\n" ); +} + +// Functions: +// vol_to_info from item.cpp TEST_CASE( "vol_to_info", "[iteminfo][volume]" ) { + clear_avatar(); + override_option opt_vol( "VOLUME_UNITS", "l" ); iteminfo vol = vol_to_info( "BASE", "Volume", 3_liter ); // strings @@ -751,9 +2120,12 @@ TEST_CASE( "vol_to_info", "[iteminfo][volume]" ) CHECK( vol.three_decimal == false ); } -// Test weight_to_info function from item.cpp +// Functions: +// weight_to_info from item.cpp TEST_CASE( "weight_to_info", "[iteminfo][weight]" ) { + clear_avatar(); + override_option opt( "USE_METRIC_WEIGHTS", "kg" ); iteminfo wt = weight_to_info( "BASE", "Weight", 3_kilogram ); // strings @@ -772,3 +2144,110 @@ TEST_CASE( "weight_to_info", "[iteminfo][weight]" ) CHECK( wt.three_decimal == false ); } +// Functions: +// item::final_info +TEST_CASE( "final info", "[iteminfo][final]" ) +{ + clear_avatar(); + + SECTION( "material allergy" ) { + item socks( "test_socks" ); + REQUIRE( socks.made_of( material_id( "wool" ) ) ); + + WHEN( "avatar has a wool allergy" ) { + g->u.toggle_trait( trait_id( "WOOLALLERGY" ) ); + REQUIRE( g->u.has_trait( trait_id( "WOOLALLERGY" ) ) ); + + CHECK( item_info_str( socks, { iteminfo_parts::DESCRIPTION_ALLERGEN } ) == + "--\n" + "* This clothing will give you an allergic reaction.\n" ); + } + } + + SECTION( "fermentation and brewing" ) { + std::vector brew_duration = { iteminfo_parts::DESCRIPTION_BREWABLE_DURATION }; + std::vector brew_products = { iteminfo_parts::DESCRIPTION_BREWABLE_PRODUCTS }; + + item wine_must( "test_brew_wine" ); + REQUIRE( wine_must.brewing_time() == 12_hours ); + + // TODO: DESCRIPTION_ACTIVATABLE_TRANSFORMATION (sourdough?) + + CHECK( item_info_str( wine_must, brew_duration ) == + "--\n" + "* Once set in a vat, this will ferment in around 12 hours.\n" ); + + CHECK( item_info_str( wine_must, brew_products ) == + "--\n" + "* Fermenting this will produce test tennis ball wine.\n" + "* Fermenting this will produce yeast.\n" ); + } + + SECTION( "radioactivity" ) { + std::vector radioactive = { iteminfo_parts::DESCRIPTION_RADIOACTIVITY_ALWAYS }; + + item carafe( "test_nuclear_carafe" ); + REQUIRE( carafe.has_flag( "RADIOACTIVE" ) ); + REQUIRE( carafe.has_flag( "LEAK_ALWAYS" ) ); + + CHECK( item_info_str( carafe, radioactive ) == + "--\n" + "* This object is surrounded" + " by a sickly green glow.\n" ); + } +} + +// Each item::xxx_info function has a `debug` argument, though only a few use it. The main +// item::info function sets it based on whether the global game variable `debug_mode` is true. +// +// The item::debug_info function prints debug info for the item if iteminfo_parts::BASE_DEBUG is +// given, and the `debug` argument passed to the function is true. This function used to be part of +// item::basic_info, and as of this writing is now invoked just after that function. +// +// Functions: +// item::debug_info +// FIXME: This fails when run with other tests. May not be worth having a test on... +TEST_CASE( "item debug info", "[iteminfo][debug][!mayfail][.]" ) +{ + clear_avatar(); + + SECTION( "debug info displayed when debug_mode is true" ) { + // Lightly aged pine nuts + item nuts( "test_pine_nuts" ); + calendar::turn += 8_hours; + // Quick-check a couple expected values for debug info + REQUIRE( nuts.age() == 8_hours ); + REQUIRE( nuts.charges == 4 ); + + // Debug info may be enabled with an iteminfo_parts flag + std::vector debug_part = { iteminfo_parts::BASE_DEBUG }; + const iteminfo_query debug_query( debug_part ); + + // Invoke item::debug_info directly, so we can pass debug = true + // (instead of relying on item::info to use the debug_mode global setting) + std::vector info_vec; + nuts.debug_info( info_vec, &debug_query, 1, true ); + + // FIXME: "last rot" and "last temp" are expected to be 0, but may have values (ex. 43200) + // Neex to figure out what processing to do before this check to make them predictable + CHECK( format_item_info( info_vec, {} ) == + "age (hours): 8\n" + "charges: 4\n" + "damage: 0\n" + "active: 1\n" + "burn: 0\n" + "tags: \n" + "age (turns): 28800\n" + "rot (turns): 0\n" + " max rot (turns): 3888000\n" + "last rot: 0\n" + "last temp: 0\n" + "Temp: 0\n" + "Spec ener: -10\n" + "Spec heat lq: 2200\n" + "Spec heat sld: 2200\n" + "latent heat: 20\n" + "Freeze point: 32\n" ); + } +} +