1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
|
#include "calendar.h"
#include "cata_catch.h"
#include "character.h"
#include "item.h"
#include "item_location.h"
#include "map.h"
#include "map_helpers.h"
#include "player_helpers.h"
#include "recipe_dictionary.h"
// Tests for disassembling items from an "uncraft" recipe.
//
// Covers the Character::complete_disassemble function, including:
//
// - Uncraft recipe "difficulty" affects component recovery success using character skill
// - Damage to the original item reduces the chance of successful component recovery
static const itype_id itype_cotton_patchwork( "cotton_patchwork" );
static const itype_id itype_debug_backpack( "debug_backpack" );
static const itype_id itype_string_6( "string_6" );
static const itype_id itype_test_knotted_string_ball( "test_knotted_string_ball" );
static const itype_id itype_test_multitool( "test_multitool" );
static const itype_id itype_test_rag_bundle( "test_rag_bundle" );
static time_point midday = calendar::turn_zero + 12_hours;
// Prepare for crafting with storage, tools, and light, and return the player character
static Character &setup_uncraft_character()
{
Character &they = get_player_character();
clear_avatar();
clear_map();
set_time( midday );
// Backpack for storage, and multi-tool for qualities
they.worn.wear_item( they, item( itype_debug_backpack ), false, false );
they.i_add( item( itype_test_multitool ) );
return they;
}
// Repeatedly disassemble an item of a given type according to the given recipe, and return
// a tally of all of the yielded item types.
static std::map<itype_id, int> repeat_uncraft( Character &they, const itype_id &dis_itype,
const recipe &dis_recip, int repeat = 1, int damage = 0 )
{
// Accumulate all items from recipe disassembly
std::map<itype_id, int> yield_items;
// Locations for real item and disassembly item
item_location it_loc;
item_location it_dis_loc;
for( int rep = 0; rep < repeat; ++rep ) {
// Add another instance of the disassembly item
item_location it = they.i_add( item( dis_itype ) );
if( damage > 0 ) {
it->set_damage( damage );
}
it_dis_loc = they.create_in_progress_disassembly( it );
// Clear away bits
clear_map();
// Get lit
set_time( midday );
// Disassemble it
they.complete_disassemble( it_dis_loc, dis_recip );
// Count how many of each type of item are here
for( item &it : get_map().i_at( they.pos() ) ) {
if( yield_items.count( it.typeId() ) == 0 ) {
yield_items[it.typeId()] = it.count();
} else {
yield_items[it.typeId()] += it.count();
}
}
}
return yield_items;
}
// Return the number of part_itype items yielded by character with the given skill level
// when disassembling whole_itype items the given number of times.
static int uncraft_yield( Character &they, const itype_id whole_itype, const itype_id part_itype,
const int difficulty, const int skill_level )
{
recipe uncraft_recipe = recipe_dictionary::get_uncraft( whole_itype );
uncraft_recipe.difficulty = difficulty;
they.set_skill_level( uncraft_recipe.skill_used, skill_level );
std::map<itype_id, int> yield = repeat_uncraft( they, whole_itype, uncraft_recipe, 1 );
return yield[part_itype];
}
// When a "recipe" or "uncraft" has a "difficulty" and "skill_used", recovery of components when
// disassembling from that recipe/uncraft may fail, depending on the skill level of the character
// doing disassembly.
//
// For each of the recipe or uncraft "components", two dice rolls are made:
//
// Difficulty roll: #dice = difficulty #sides = 24
// Character skill roll: #dice = 4*SKILL+2 #sides = INT+16
//
// For example, if recipe difficulty is 1, character skill is 0, and INT is the normal 8, the
// character is at an advantage in the dice rolls:
//
// Difficulty roll: 1 d 24 (difficulty# dice, 24 sides)
// Character skill roll: 2 d 24 (4*0+2 dice, 8+16 sides)
//
// For a more difficult recipe, say difficulty 4, with character still having 0 skill, and 8 INT,
// they are at a disadvantage:
//
// Difficulty roll: 4 d 24 (difficulty# dice, 24 sides)
// Character skill roll: 2 d 24 (4*0+2 dice, 8+16 sides)
//
// The character can increase their dice by increasing skill level; each level gives 4 additional
// dice to the character, so if they can increase their skill to 1, then attempt the same recipe,
// they will be at a slight advantage again:
//
// Difficulty roll: 4 d 24 (difficulty# dice, 24 sides)
// Character skill roll: 6 d 24 (4*1+2 dice, 8+16 sides)
//
// For each component, if the character skill roll *exceeds* the difficulty roll, recovery succeeds.
// Recipes with 0 difficulty do not require any skill to succeed; all components are recovered.
//
// Recovery may still fail if the original item was damaged; for that, see the [damage] test.
//
TEST_CASE( "uncraft_difficulty_and_character_skill", "[uncraft][difficulty][skill]" )
{
Character &they = setup_uncraft_character();
// The knotted string ball yields 1,000 short strings when successfully desconstructed.
const itype_id decon_it = itype_test_knotted_string_ball;
const itype_id part_it = itype_string_6;
std::map<itype_id, int> yield;
SECTION( "uncraft recipe with difficulty 0" ) {
// Full yield at any skill level
CHECK( uncraft_yield( they, decon_it, part_it, 0, 0 ) == 1000 );
CHECK( uncraft_yield( they, decon_it, part_it, 0, 1 ) == 1000 );
CHECK( uncraft_yield( they, decon_it, part_it, 0, 2 ) == 1000 );
CHECK( uncraft_yield( they, decon_it, part_it, 0, 3 ) == 1000 );
CHECK( uncraft_yield( they, decon_it, part_it, 0, 4 ) == 1000 );
}
constexpr int margin = 55;
SECTION( "uncraft recipe with difficulty 1" ) {
CHECK( uncraft_yield( they, decon_it, part_it, 1, 0 ) == Approx( 830 ).margin( margin ) );
CHECK( uncraft_yield( they, decon_it, part_it, 1, 1 ) == Approx( 1000 ).margin( margin ) );
CHECK( uncraft_yield( they, decon_it, part_it, 1, 2 ) == Approx( 1000 ).margin( margin ) );
CHECK( uncraft_yield( they, decon_it, part_it, 1, 3 ) == Approx( 1000 ).margin( margin ) );
CHECK( uncraft_yield( they, decon_it, part_it, 1, 4 ) == Approx( 1000 ).margin( margin ) );
}
SECTION( "uncraft recipe with difficulty 5" ) {
CHECK( uncraft_yield( they, decon_it, part_it, 5, 0 ) == Approx( 20 ).margin( margin ) );
CHECK( uncraft_yield( they, decon_it, part_it, 5, 1 ) == Approx( 700 ).margin( margin ) );
CHECK( uncraft_yield( they, decon_it, part_it, 5, 2 ) == Approx( 990 ).margin( margin ) );
CHECK( uncraft_yield( they, decon_it, part_it, 5, 3 ) == Approx( 1000 ).margin( margin ) );
CHECK( uncraft_yield( they, decon_it, part_it, 5, 4 ) == Approx( 1000 ).margin( margin ) );
}
SECTION( "uncraft recipe with difficulty 10" ) {
CHECK( uncraft_yield( they, decon_it, part_it, 10, 0 ) == Approx( 0 ).margin( margin ) );
CHECK( uncraft_yield( they, decon_it, part_it, 10, 1 ) == Approx( 30 ).margin( margin ) );
CHECK( uncraft_yield( they, decon_it, part_it, 10, 2 ) == Approx( 500 ).margin( margin ) );
CHECK( uncraft_yield( they, decon_it, part_it, 10, 3 ) == Approx( 930 ).margin( margin ) );
CHECK( uncraft_yield( they, decon_it, part_it, 10, 4 ) == Approx( 1000 ).margin( margin ) );
}
}
// Item damage_level (0-5) affects chance of successfully recovering components from disassembly.
TEST_CASE( "uncraft_from_a_damaged_item", "[uncraft][damage]" )
{
Character &they = setup_uncraft_character();
recipe uncraft_rags = recipe_dictionary::get_uncraft( itype_test_rag_bundle );
REQUIRE( uncraft_rags.difficulty == 0 ); // 0 difficulty makes sure it can't fail because of skills
struct uncraft_test_preset {
const int damage; // value to assign as item's damage
const int damage_level; // expected item damage level
const int yield; // expected approximate yield
const int margin; // expected approximate yield margin
};
// yield is based on pow( 0.8, damage_level ) roll, e.g. level 2 will get 0.8^2 = ~64% yield
// uncraft repeated 10 times and the bundle yields 100 each = 1000 yield total for intact item
// margin value of 55 derived from running 1000 iterations of this, add 5 for rng outliers
const int margin = 60;
const std::vector<uncraft_test_preset> presets = {
{ 0, 0, 1000, 0 }, // expect full yield from undamaged
{ 999, 1, 800, margin }, // ~80% from damage level 1
{ 1999, 2, 640, margin }, // ~64% from damage level 2
{ 2999, 3, 512, margin }, // ~51% from damage level 3
{ 3999, 4, 410, margin }, // ~41% from damage level 4
{ 4000, 5, 328, margin }, // ~33% from damage level 5 (XX)
};
for( const uncraft_test_preset &preset : presets ) {
CAPTURE( preset.damage, preset.damage_level, preset.yield, preset.margin );
item test_item( itype_test_rag_bundle, calendar::turn );
test_item.set_damage( preset.damage );
CHECK( test_item.damage_level() == preset.damage_level );
const int yield = repeat_uncraft( they, itype_test_rag_bundle, uncraft_rags, 10, preset.damage )
.at( itype_cotton_patchwork );
CHECK( yield == Approx( preset.yield ).margin( preset.margin ) );
}
}
|