diff --git a/data/json/items/tools.json b/data/json/items/tools.json index b42a8382f2f6f..a42f1e77b1dbf 100644 --- a/data/json/items/tools.json +++ b/data/json/items/tools.json @@ -295,7 +295,7 @@ "cotton", "leather", "wool", "fur", "nomex" ], "skill": "tailor", - "cost_scaling": 0, + "cost_scaling": 0.1, "tool_quality": 0, "move_cost": 1000 } @@ -327,7 +327,7 @@ "cotton", "leather", "wool", "fur", "nomex", "neoprene" ], "skill": "tailor", - "cost_scaling": 0, + "cost_scaling": 0.1, "tool_quality": 1, "move_cost": 800 }, @@ -1727,7 +1727,7 @@ "kevlar", "plastic", "iron", "steel", "hardsteel", "aluminum", "copper" ], "skill": "mechanics", - "cost_scaling": 0.25, + "cost_scaling": 0.1, "move_cost": 1500 }, { @@ -4643,7 +4643,7 @@ "kevlar", "plastic", "iron", "steel", "hardsteel", "aluminum", "copper" ], "skill": "mechanics", - "cost_scaling": 0.25, + "cost_scaling": 0.1, "move_cost": 500, "tool_quality": 3 }, @@ -6661,7 +6661,7 @@ "cotton", "leather", "wool", "fur", "nomex" ], "skill": "tailor", - "cost_scaling": 0, + "cost_scaling": 0.1, "tool_quality": -1, "move_cost": 1300 } @@ -6692,7 +6692,7 @@ "cotton", "leather", "wool", "fur", "nomex" ], "skill": "tailor", - "cost_scaling": 0, + "cost_scaling": 0.1, "tool_quality": -1, "move_cost": 1500 } @@ -7793,7 +7793,7 @@ "kevlar", "plastic", "iron", "steel", "hardsteel", "aluminum", "copper" ], "skill": "mechanics", - "cost_scaling": 0.25, + "cost_scaling": 0.1, "move_cost": 1000, "tool_quality": 3 }, @@ -11000,7 +11000,7 @@ "neoprene", "nomex" ], "skill": "tailor", - "cost_scaling": 0, + "cost_scaling": 0.1, "tool_quality": 0, "move_cost": 1200 } diff --git a/src/activity_handlers.cpp b/src/activity_handlers.cpp index 061537b7b7e1d..77f5d5dbe51b5 100644 --- a/src/activity_handlers.cpp +++ b/src/activity_handlers.cpp @@ -1387,10 +1387,10 @@ enum repeat_type : int { REPEAT_CANCEL // Stop repeating }; -repeat_type repeat_menu( repeat_type last_selection ) +repeat_type repeat_menu( const std::string &title, repeat_type last_selection ) { uimenu rmenu; - rmenu.text = _("Repeat repairing?"); + rmenu.text = title; rmenu.addentry( REPEAT_ONCE, true, '1', _("Repeat once") ); rmenu.addentry( REPEAT_FOREVER, true, '2', _("Repeat as long as you can") ); rmenu.addentry( REPEAT_FULL, true, '3', _("Repeat until fully repaired, but don't reinforce") ); @@ -1529,7 +1529,17 @@ void activity_handlers::repair_item_finish( player_activity *act, player *p ) if( need_input ) { g->draw(); - repeat_type answer = repeat_menu( repeat ); + auto action_type = actor->default_action( fix ); + const auto chance = actor->repair_chance( *p, fix, action_type ); + if( chance.first <= 0.0f ) { + action_type = repair_item_actor::RT_PRACTICE; + } + + const std::string title = string_format( + _("%s\nSuccess chance %.1f\nDamage chance %.1f"), + repair_item_actor::action_description( action_type ).c_str(), + 100.0f * chance.first, 100.0f * chance.second ); + repeat_type answer = repeat_menu( title, repeat ); if( answer == REPEAT_CANCEL ) { act->type = ACT_NULL; return; diff --git a/src/item.cpp b/src/item.cpp index 3de4fe6214a49..6d68f0088003a 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -2728,24 +2728,21 @@ int item::get_encumber() const if( item::item_tags.count("FIT") ) { encumber = std::max( encumber / 2, encumber - 10 ); } - // Good items to test this stuff on: - // Hoodies (3 thickness), jumpsuits (2 thickness, 3 encumbrance), - // Nomes socks (2 thickness, 0 encumbrance) - // When a common item has 90%+ coverage, 15/15 protection and <=5 encumbrance, - // it's a sure sign something has to be nerfed. + + const int thickness = get_thickness(); + const int coverage = get_coverage(); if( item::item_tags.count("wooled") ) { - encumber += 3; + encumber += 1 + 3 * coverage / 100; } if( item::item_tags.count("furred") ){ - encumber += 5; + encumber += 1 + 4 * coverage / 100; } - // Don't let dual-armor-modded items get below 10 encumbrance after fitting - // Also prevent 0 encumbrance armored underwear + if( item::item_tags.count("leather_padded") ) { - encumber = std::max( 15, encumber + 7 ); + encumber += thickness * coverage / 100 + 5; } if( item::item_tags.count("kevlar_padded") ) { - encumber = std::max( 13, encumber + 5 ); + encumber += thickness * coverage / 100 + 5; } return encumber; @@ -2796,13 +2793,15 @@ int item::get_warmth() const // it_armor::warmth is signed char int result = static_cast( t->warmth ); - if (item::item_tags.count("furred") > 0){ - fur_lined = 35 * (float(get_coverage()) / 100); + if( item::item_tags.count("furred") > 0 ) { + fur_lined = 35 * get_coverage() / 100; } - if (item::item_tags.count("wooled") > 0){ - wool_lined = 20 * (float(get_coverage()) / 100); + + if( item::item_tags.count("wooled") > 0 ) { + wool_lined = 20 * get_coverage() / 100; } - return result + fur_lined + wool_lined; + + return result + fur_lined + wool_lined; } @@ -2893,7 +2892,7 @@ long item::num_charges() return 0; } -int item::bash_resist(bool /*to_self*/) const +int item::bash_resist( bool to_self ) const { float resist = 0; float l_padding = 0; @@ -2922,7 +2921,9 @@ int item::bash_resist(bool /*to_self*/) const // Armor gets an additional multiplier. if (is_armor()) { // base resistance - eff_thickness = ((get_thickness() - damage <= 0) ? 1 : (get_thickness() - damage)); + // Don't give reinforced items +armor, just more resistance to ripping + const int eff_damage = std::max( to_self ? -1 : 0, damage ); + eff_thickness = ((get_thickness() - eff_damage <= 0) ? 1 : (get_thickness() - eff_damage)); } for (auto mat : mat_types) { @@ -2934,7 +2935,7 @@ int item::bash_resist(bool /*to_self*/) const return lround((resist * eff_thickness * adjustment) + l_padding + k_padding); } -int item::cut_resist(bool /*to_self*/) const +int item::cut_resist( bool to_self ) const { float resist = 0; float l_padding = 0; @@ -2964,7 +2965,9 @@ int item::cut_resist(bool /*to_self*/) const // Armor gets an additional multiplier. if (is_armor()) { // base resistance - eff_thickness = ((get_thickness() - damage <= 0) ? 1 : (get_thickness() - damage)); + // Don't give reinforced items +armor, just more resistance to ripping + const int eff_damage = std::max( to_self ? -1 : 0, damage ); + eff_thickness = ((get_thickness() - eff_damage <= 0) ? 1 : (get_thickness() - eff_damage)); } for (auto mat : mat_types) { diff --git a/src/iuse_actor.cpp b/src/iuse_actor.cpp index e37ac7a0e9bfb..2accd951083fb 100644 --- a/src/iuse_actor.cpp +++ b/src/iuse_actor.cpp @@ -21,6 +21,7 @@ #include "field.h" #include "weather.h" #include "pldata.h" +#include "recipe_dictionary.h" #include #include @@ -2044,18 +2045,14 @@ bool repair_item_actor::handle_components( player &pl, const item &fix, return false; } - // Repairing apparently doesn't always consume items; - // maybe it should just consume less or something? - // Anyway, don't ask for items if we won't need any. - if( !(fix.damage >= 3 || fix.damage == 0) ) { - return true; - } - const inventory &crafting_inv = pl.crafting_inventory(); // Repairing or modifying items requires at least 1 repair item, // otherwise number is related to size of item - const int items_needed = std::max( 1, ceil( fix.volume() * cost_scaling ) ); + // Round up if checking, but roll if actually consuming + const int items_needed = std::max( 1, just_check ? + ceil( fix.volume() * cost_scaling ) : + divide_roll_remainder( fix.volume() * cost_scaling, 1.0f ) ); // Go through all discovered repair items and see if we have any of them available for( const auto &entry : valid_entries ) { @@ -2093,6 +2090,36 @@ bool repair_item_actor::handle_components( player &pl, const item &fix, return true; } + +// Returns the level of the lowest level recipe that results in item of `fix`'s type +// If the recipe is not known by the player, +1 to difficulty +// If player doesn't meet the requirements of the recipe, +1 to difficulty +// If the recipe doesn't exist, difficulty is 10 +int repair_item_actor::repair_recipe_difficulty( const player &pl, + const item &fix, bool training ) const +{ + const auto &type = fix.typeId(); + int min = 5; + for( const auto *cur_recipe : recipe_dict ) { + if( type != cur_recipe->result ) { + continue; + } + + int cur_difficulty = cur_recipe->difficulty; + if( !training && !pl.knows_recipe( cur_recipe ) ) { + cur_difficulty++; + } + + if( !training && !pl.has_recipe_requirements( cur_recipe ) ) { + cur_difficulty++; + } + + min = std::min( cur_difficulty, min ); + } + + return min; +} + bool repair_item_actor::can_repair( player &pl, const item &tool, const item &fix, bool print_msg ) const { if( !could_repair( pl, tool, print_msg ) ) { @@ -2133,21 +2160,91 @@ bool repair_item_actor::can_repair( player &pl, const item &tool, const item &fi return false; } - if( fix.damage == 0 && fix.has_flag("PRIMITIVE_RANGED_WEAPON") ) { + if( fix.has_flag("VARSIZE") && !fix.has_flag("FIT") ) { + return true; + } + + if( fix.damage > 0 ) { + return true; + } + + if( fix.damage < 0 ) { + if( print_msg ) { + pl.add_msg_if_player( m_info, _("Your %s is already enhanced."), fix.tname().c_str() ); + } + return false; + } + + if( fix.has_flag("PRIMITIVE_RANGED_WEAPON") ) { if( print_msg ) { pl.add_msg_if_player( m_info, _("You cannot improve your %s any more this way."), fix.tname().c_str()); } return false; } - if( fix.damage >= 0 || (fix.has_flag("VARSIZE") && !fix.has_flag("FIT")) ) { - return true; + return true; +} + +std::pair repair_item_actor::repair_chance( + const player &pl, const item &fix, repair_item_actor::repair_type action_type ) const +{ + ///\EFFECT_TAILOR randomly improves clothing repair efforts + ///\EFFECT_MECHANICS randomly improves metal repair efforts + const int skill = pl.get_skill_level( used_skill ); + const int recipe_difficulty = repair_recipe_difficulty( pl, fix ); + int action_difficulty = 0; + switch( action_type ) { + case RT_REPAIR: + action_difficulty = fix.damage; + break; + case RT_REFIT: + // Let's make refitting as hard as recovering an almost-wrecked item + action_difficulty = MAX_ITEM_DAMAGE; + break; + case RT_REINFORCE: + // Reinforcing is at least as hard as refitting + action_difficulty = std::max( MAX_ITEM_DAMAGE, recipe_difficulty ); + break; + default: + std::make_pair( 0.0f, 0.0f ); + } + + const int difficulty = recipe_difficulty + action_difficulty; + // Sample numbers: + // Item | Damage | Skill | Dex | Success | Failure + // Hoodie | 2 | 3 | 10 | 6% | 0% + // Hazmat | 1 | 10 | 10 | 8% | 0% + // Hazmat | 1 | 5 | 20 | 0% | 2% + // t-shirt| 4 | 1 | 5 | 2% | 3% + // Duster | 2 | 5 | 5 | 10% | 0% + // Duster | 2 | 2 | 10 | 4% | 1% + // Duster | Refit | 2 | 10 | 0% | N/A + float success_chance = (10 + 2 * skill - 2 * difficulty) / 100.0f; + ///\EFFECT_DEX randomly reduces the chances of damaging an item when repairing + float damage_chance = (difficulty - skill - (tool_quality + pl.dex_cur) / 5.0f) / 100.0f; + + damage_chance = std::max( 0.0f, std::min( 1.0f, damage_chance ) ); + success_chance = std::max( 0.0f, std::min( 1.0f - damage_chance, success_chance ) ); + + + return std::make_pair( success_chance, damage_chance ); +} + +repair_item_actor::repair_type repair_item_actor::default_action( const item &fix ) const +{ + if( fix.damage > 0 ) { + return RT_REPAIR; } - if( print_msg ) { - pl.add_msg_if_player( m_info, _("Your %s is already enhanced."), fix.tname().c_str() ); + if( fix.has_flag("VARSIZE") && !fix.has_flag("FIT") ) { + return RT_REFIT; } - return false; + + if( fix.damage == 0 ) { + return RT_REINFORCE; + } + + return RT_NOTHING; } repair_item_actor::attempt_hint repair_item_actor::repair( player &pl, item &tool, item &fix ) const @@ -2156,15 +2253,10 @@ repair_item_actor::attempt_hint repair_item_actor::repair( player &pl, item &too return AS_CANT; } - pl.practice( used_skill, 8 ); - ///\EFFECT_TAILOR randomly improves clothing repair efforts - ///\EFFECT_MECHANICS randomly improves metal repair efforts - // Let's make refitting/reinforcing as hard as recovering an almost-wrecked item - // TODO: Make difficulty depend on the item type (for example, on recipe's difficulty) - const int difficulty = fix.damage == 0 ? 4 : fix.damage; - float repair_chance = (5 + pl.get_skill_level( used_skill ) - difficulty) / 100.0f; - ///\EFFECT_DEX randomly reduces the chances of damaging an item when repairing - float damage_chance = (5 - (pl.dex_cur + tool_quality) / 5.0f) / 100.0f; + const auto action = default_action( fix ); + const auto chance = repair_chance( pl, fix, action ); + const int practice_amount = repair_recipe_difficulty( pl, fix, true ); + pl.practice( used_skill, practice_amount ); float roll_value = rng_float( 0.0, 1.0 ); enum roll_result { SUCCESS, @@ -2172,15 +2264,15 @@ repair_item_actor::attempt_hint repair_item_actor::repair( player &pl, item &too NEUTRAL } roll; - if( roll_value > 1.0f - damage_chance ) { + if( roll_value > 1.0f - chance.second ) { roll = FAILURE; - } else if( roll_value < repair_chance ) { + } else if( roll_value < chance.first ) { roll = SUCCESS; } else { roll = NEUTRAL; } - if( fix.damage > 0 ) { + if( action == RT_REPAIR ) { if( roll == FAILURE ) { pl.add_msg_if_player(m_bad, _("You damage your %s further!"), fix.tname().c_str()); fix.damage++; @@ -2210,19 +2302,14 @@ repair_item_actor::attempt_hint repair_item_actor::repair( player &pl, item &too return AS_RETRY; } - if( fix.damage == 0 && fix.has_flag("PRIMITIVE_RANGED_WEAPON") ) { - pl.add_msg_if_player(m_info, _("You cannot improve your %s any more this way."), fix.tname().c_str()); - return AS_CANT; - } - - if( fix.damage == 0 || (fix.has_flag("VARSIZE") && !fix.has_flag("FIT")) ) { + if( action == RT_REFIT ) { if( roll == FAILURE ) { pl.add_msg_if_player(m_bad, _("You damage your %s!"), fix.tname().c_str()); fix.damage++; return AS_FAILURE; } - if( roll == SUCCESS && fix.has_flag("VARSIZE") && !fix.has_flag("FIT") ) { + if( roll == SUCCESS ) { pl.add_msg_if_player(m_good, _("You take your %s in, improving the fit."), fix.tname().c_str()); fix.item_tags.insert("FIT"); @@ -2230,7 +2317,16 @@ repair_item_actor::attempt_hint repair_item_actor::repair( player &pl, item &too return AS_SUCCESS; } - if( roll == SUCCESS && (fix.has_flag("FIT") || !fix.has_flag("VARSIZE")) ) { + return AS_RETRY; + } + + if( action == RT_REINFORCE ) { + if( fix.has_flag("PRIMITIVE_RANGED_WEAPON") ) { + pl.add_msg_if_player( m_info, _("You cannot improve your %s any more this way."), fix.tname().c_str() ); + return AS_CANT; + } + + if( roll == SUCCESS ) { pl.add_msg_if_player(m_good, _("You make your %s extra sturdy."), fix.tname().c_str()); fix.damage--; handle_components( pl, fix, false, false ); @@ -2240,10 +2336,23 @@ repair_item_actor::attempt_hint repair_item_actor::repair( player &pl, item &too return AS_RETRY; } - pl.add_msg_if_player(m_info, _("Your %s is already enhanced."), fix.tname().c_str()); + pl.add_msg_if_player( m_info, _("Your %s is already enhanced."), fix.tname().c_str() ); return AS_CANT; } +const std::string &repair_item_actor::action_description( repair_item_actor::repair_type rt ) +{ + static const std::array arr = {{ + _("Nothing"), + _("Repairing"), + _("Refiting"), + _("Reinforcing"), + _("Practicing") + }}; + + return arr[rt]; +} + void heal_actor::load( JsonObject &obj ) { // Mandatory diff --git a/src/iuse_actor.h b/src/iuse_actor.h index 343c4329dbb5c..de9f9f8be4d82 100644 --- a/src/iuse_actor.h +++ b/src/iuse_actor.h @@ -661,7 +661,17 @@ class repair_item_actor : public iuse_actor AS_RETRY, // Failed, but can retry AS_FAILURE, // Failed hard, don't retry AS_DESTROYED, // Failed and destroyed item - AS_CANT // Couldn't attempt + AS_CANT, // Couldn't attempt + AS_CANT_YET // Skill too low + }; + + enum repair_type : int { + RT_NOTHING = 0, + RT_REPAIR, // Just repairing damage + RT_REFIT, // Adding (fits) tag + RT_REINFORCE, // Getting damage below 0 + RT_PRACTICE, // Wanted to reinforce, but can't + NUM_REPAIR_TYPES }; /** Attempts to repair target item with selected tool */ @@ -671,6 +681,19 @@ class repair_item_actor : public iuse_actor bool can_repair( player &pl, const item &tool, const item &target, bool print_msg ) const; /** Returns if components are available. Consumes them if `just_check` is false. */ bool handle_components( player &pl, const item &fix, bool print_msg, bool just_check ) const; + /** Returns the chance to repair and to damage an item. */ + std::pair repair_chance( + const player &pl, const item &fix, repair_type action_type ) const; + /** What are we most likely trying to do with this item? */ + repair_type default_action( const item &fix ) const; + /** + * Calculates the difficulty to repair an item + * based on recipes to craft it and player's knowledge of them. + * If `training` is true, player's lacking knowledge and skills are not used to increase difficulty. + */ + int repair_recipe_difficulty( const player &pl, const item &fix, bool training = false ) const; + /** Describes members of `repair_type` enum */ + static const std::string &action_description( repair_type ); repair_item_actor() : iuse_actor() { } virtual ~repair_item_actor() { }