diff --git a/data/mods/Magiclysm/items/bionics.json b/data/mods/Magiclysm/items/bionics.json index 163b2b6ca2c0..bc4738dafe17 100644 --- a/data/mods/Magiclysm/items/bionics.json +++ b/data/mods/Magiclysm/items/bionics.json @@ -12,7 +12,7 @@ { "type": "enchantment", "id": "ench_bio_sneeze_beam", - "values": [ { "value": "MAX_MANA", "add": 500 } ] + "values": [ { "value": "MANA_CAP", "add": 500 } ] }, { "id": "bio_sneeze_beam", diff --git a/data/mods/Magiclysm/items/enchanted.json b/data/mods/Magiclysm/items/enchanted.json index 5b353bdbb637..37951145520c 100644 --- a/data/mods/Magiclysm/items/enchanted.json +++ b/data/mods/Magiclysm/items/enchanted.json @@ -26,7 +26,7 @@ "material": "wood", "flags": [ "SHEATH_SPEAR", "ALWAYS_TWOHAND", "FRAGILE_MELEE", "MAGIC_FOCUS" ], "relic_data": { - "passive_effects": [ { "has": "WIELD", "condition": "ALWAYS", "values": [ { "value": "MAX_MANA", "multiply": 0.2, "add": 750 } ] } ] + "passive_effects": [ { "has": "WIELD", "condition": "ALWAYS", "values": [ { "value": "MANA_CAP", "multiply": 0.2, "add": 750 } ] } ] }, "weight": "1400 g", "volume": "3 L", @@ -52,7 +52,7 @@ "material_thickness": 2, "environmental_protection": 2, "relic_data": { - "passive_effects": [ { "has": "WORN", "condition": "ALWAYS", "values": [ { "value": "REGEN_MANA", "multiply": 1 } ] } ] + "passive_effects": [ { "has": "WORN", "condition": "ALWAYS", "values": [ { "value": "MANA_REGEN", "multiply": 1 } ] } ] }, "flags": [ "VARSIZE" ] }, diff --git a/data/mods/TEST_DATA/relics.json b/data/mods/TEST_DATA/relics.json index bcc346e95252..50f4b17d35ef 100644 --- a/data/mods/TEST_DATA/relics.json +++ b/data/mods/TEST_DATA/relics.json @@ -108,5 +108,97 @@ } ] } + }, + { + "type": "GENERIC", + "id": "test_relic_mods_stamina", + "copy-from": "test_relic_base", + "name": "TEST relic mods stamina rate", + "relic_data": { + "passive_effects": [ + { "has": "HELD", "condition": "ALWAYS", "values": [ { "value": "STAMINA_CAP", "multiply": -0.1 } ] }, + { "has": "HELD", "condition": "ALWAYS", "values": [ { "value": "STAMINA_REGEN", "multiply": -0.1 } ] } + ] + } + }, + { + "type": "GENERIC", + "id": "test_relic_mods_thirst", + "copy-from": "test_relic_base", + "name": "TEST relic mods thirst rate", + "relic_data": { + "passive_effects": [ { "has": "HELD", "condition": "ALWAYS", "values": [ { "value": "THIRST", "multiply": -0.1 } ] } ] + } + }, + { + "type": "GENERIC", + "id": "test_relic_mods_fatigue", + "copy-from": "test_relic_base", + "name": "TEST relic mods fatigue rate", + "relic_data": { + "passive_effects": [ { "has": "HELD", "condition": "ALWAYS", "values": [ { "value": "FATIGUE", "multiply": -0.1 } ] } ] + } + }, + { + "type": "GENERIC", + "id": "test_relic_mods_dodges", + "copy-from": "test_relic_base", + "name": "TEST relic mods dodges", + "relic_data": { "passive_effects": [ { "has": "HELD", "condition": "ALWAYS", "values": [ { "value": "BONUS_DODGE", "add": 1 } ] } ] } + }, + { + "type": "GENERIC", + "id": "test_relic_mods_cut_dmg", + "copy-from": "test_balanced_sword", + "name": "TEST relic mods cut dmg", + "relic_data": { + "passive_effects": [ { "has": "HELD", "condition": "ALWAYS", "values": [ { "value": "ITEM_DAMAGE_CUT", "multiply": -0.5, "add": 1 } ] } ] + } + }, + { + "type": "GENERIC", + "id": "test_relic_mods_stab_dmg", + "copy-from": "test_screwdriver", + "name": "TEST relic mods stab dmg", + "relic_data": { + "passive_effects": [ { "has": "HELD", "condition": "ALWAYS", "values": [ { "value": "ITEM_DAMAGE_STAB", "multiply": -0.5, "add": 1 } ] } ] + } + }, + { + "type": "GENERIC", + "id": "test_relic_mods_bash_dmg", + "copy-from": "test_halligan", + "name": "TEST relic mods bash dmg", + "relic_data": { + "passive_effects": [ { "has": "HELD", "condition": "ALWAYS", "values": [ { "value": "ITEM_DAMAGE_BASH", "multiply": -0.5, "add": 1 } ] } ] + } + }, + { + "type": "GENERIC", + "id": "test_relic_item_armor_mod", + "copy-from": "test_hazmat_suit", + "name": "TEST relic item armor mod", + "relic_data": { + "passive_effects": [ + { "has": "WORN", "condition": "ALWAYS", "values": [ { "value": "ITEM_ARMOR_CUT", "multiply": -0.5, "add": 3 } ] }, + { + "has": "WORN", + "condition": "ALWAYS", + "values": [ { "value": "ITEM_ARMOR_BASH", "multiply": 0.5, "add": -3 } ] + } + ] + } + }, + { + "type": "GENERIC", + "id": "test_relic_char_armor_mod", + "copy-from": "test_socks", + "name": "TEST relic character armor mod", + "relic_data": { + "passive_effects": [ + { "has": "WORN", "condition": "ALWAYS", "values": [ { "value": "ARMOR_CUT", "multiply": -0.5, "add": -2 } ] }, + { "has": "WORN", "condition": "ALWAYS", "values": [ { "value": "ARMOR_STAB", "multiply": -0.1, "add": -3 } ] } + ] + } } ] diff --git a/doc/MAGIC.md b/doc/MAGIC.md index 1d9b28072de8..225a2cb8b09f 100644 --- a/doc/MAGIC.md +++ b/doc/MAGIC.md @@ -1,13 +1,45 @@ # Spells, enchantments and other custom effects -- [Spells](#Spells) - * [Currently Implemented Effects and special rules](#Currently-Implemented-Effects-and-special-rules) - * [Spells that level up](#Spells-that-level-up) - * [Learning spells](#Learning-spells) - * [Spells in professions and NPC classes](#Spells-in-professions-and-NPC-classes) - * [Spells in monsters](#Spells-in-monsters) -- [Enchantments](#Enchantments) - * [Fields](#Fields) - * [Examples](#Examples) +- [Spells](#spells) + - [Currently Implemented Effects and special rules](#currently-implemented-effects-and-special-rules) + - [Spells that level up](#spells-that-level-up) + - [Learning Spells](#learning-spells) + - [Spells in professions and NPC classes](#spells-in-professions-and-npc-classes) + - [Spells in monsters](#spells-in-monsters) +- [Enchantments](#enchantments) + - [id](#id) + - [has](#has) + - [condition](#condition) + - [emitter](#emitter) + - [ench\_effects](#ench_effects) + - [hit\_you\_effect](#hit_you_effect) + - [hit\_me\_effect](#hit_me_effect) + - [mutations](#mutations) + - [intermittent\_activation](#intermittent_activation) + - [values](#values) + - [IDs of modifiable values](#ids-of-modifiable-values) + - [Character values](#character-values) + - [STRENGTH](#strength) + - [DEXTERITY](#dexterity) + - [PERCEPTION](#perception) + - [INTELLIGENCE](#intelligence) + - [SPEED](#speed) + - [ATTACK\_COST](#attack_cost) + - [MOVE\_COST](#move_cost) + - [METABOLISM](#metabolism) + - [MANA\_CAP](#mana_cap) + - [MANA\_REGEN](#mana_regen) + - [STAMINA\_CAP](#stamina_cap) + - [STAMINA\_REGEN](#stamina_regen) + - [THIRST](#thirst) + - [FATIGUE](#fatigue) + - [BONUS\_DODGE](#bonus_dodge) + - [ARMOR\_X](#armor_x) + - [Item values](#item-values) + - [ITEM\_ATTACK\_COST](#item_attack_cost) + - [ITEM\_DAMAGE\_X](#item_damage_x) + - [ITEM\_ARMOR\_X](#item_armor_x) + - [Examples](#examples) + # Spells @@ -418,6 +450,51 @@ This modifier ignores `add` field. `base_value` here is character's base mana gain rate modified by traits. The final value cannot go below 0. +##### STAMINA_CAP +Stamina capacity. +This modifier ignores `add` field. +`base_value` here is character's base stamina capacity modified by traits. +The final value cannot go below 10% of `PLAYER_MAX_STAMINA`. + +##### STAMINA_REGEN +Stamina regeneration rate. +This modifier ignores `add` field. +`base_value` here is character's base stamina gain rate modified by mouth encumbrance. +The final value cannot go below 0. + +##### THIRST +Thirst gain rate. +This modifier ignores `add` field. +`base_value` here is character's base thirst gain rate. +The final value cannot go below 0. + +##### FATIGUE +Fatigue gain rate. +This modifier ignores `add` field. +`base_value` here is character's base fatigue gain rate. +The final value cannot go below 0. + +##### BONUS_DODGE +Additional dodges per turn before dodge penalty kicks in. +`base_value` here is character's base dodges per turn before penalty (usually 1). +The final value can go below 0, which results in penalty to dodge roll. + +##### ARMOR_X +Incoming damage modifier. +Applied after Active Defense System bionic but before the damage is absorbed by items. +Note that `base_value` here is incoming damage value of corresponding type, +so positive `add` and greater than 1 `mul` will **increase** damage received by the character. +Each damage type has its own enchant value: +* `ARMOR_ACID` +* `ARMOR_BASH` +* `ARMOR_BIO` +* `ARMOR_BULLET` +* `ARMOR_COLD` +* `ARMOR_CUT` +* `ARMOR_ELEC` +* `ARMOR_HEAT` +* `ARMOR_STAB` + #### Item values ##### ITEM_ATTACK_COST @@ -426,66 +503,31 @@ Ignores condition / location, and is always active. `base_value` here is base item attack cost. Note that the final value cannot go below 0. -##### TODO - -TODO: docs for each - -TODO: some of these are broken/unimplemented - - -* BIONIC_POWER -* MAX_STAMINA -* REGEN_STAMINA -* MAX_HP -* REGEN_HP -* THIRST -* FATIGUE -* PAIN -* BONUS_DODGE -* BONUS_BLOCK -* BONUS_DAMAGE -* ATTACK_NOISE -* SPELL_NOISE -* SHOUT_NOISE -* FOOTSTEP_NOISE -* SIGHT_RANGE -* CARRY_WEIGHT -* CARRY_VOLUME -* SOCIAL_LIE -* SOCIAL_PERSUADE -* SOCIAL_INTIMIDATE -* ARMOR_BASH -* ARMOR_CUT -* ARMOR_STAB -* ARMOR_HEAT -* ARMOR_COLD -* ARMOR_ELEC -* ARMOR_ACID -* ARMOR_BIO - -Effects for the item that has the enchantment: -* ITEM_DAMAGE_BASH -* ITEM_DAMAGE_CUT -* ITEM_DAMAGE_STAB -* ITEM_DAMAGE_HEAT -* ITEM_DAMAGE_COLD -* ITEM_DAMAGE_ELEC -* ITEM_DAMAGE_ACID -* ITEM_DAMAGE_BIO -* ITEM_DAMAGE_AP -* ITEM_ARMOR_BASH -* ITEM_ARMOR_CUT -* ITEM_ARMOR_STAB -* ITEM_ARMOR_HEAT -* ITEM_ARMOR_COLD -* ITEM_ARMOR_ELEC -* ITEM_ARMOR_ACID -* ITEM_ARMOR_BIO -* ITEM_WEIGHT -* ITEM_ENCUMBRANCE -* ITEM_VOLUME -* ITEM_COVERAGE -* ITEM_WET_PROTECTION +##### ITEM_DAMAGE_X +Melee damage of this item. +Ignores condition / location, and is always active. +`base_value` here is base item damage of corresponding type. +Note that the final value cannot go below 0. +Only some damage types are supported: +* `ITEM_DAMAGE_BASH` +* `ITEM_DAMAGE_CUT` +* `ITEM_DAMAGE_STAB` + +##### ITEM_ARMOR_X +Incoming damage modifier for this item, applied before the damage is absorbed by the item. +Note that `base_value` here is incoming damage value of corresponding type, +so positive `add` and greater than 1 `mul` will **increase** damage received by the character. +Each damage type has its own enchant value: +* `ITEM_ARMOR_ACID` +* `ITEM_ARMOR_BASH` +* `ITEM_ARMOR_BIO` +* `ITEM_ARMOR_BULLET` +* `ITEM_ARMOR_COLD` +* `ITEM_ARMOR_CUT` +* `ITEM_ARMOR_ELEC` +* `ITEM_ARMOR_HEAT` +* `ITEM_ARMOR_STAB` + ## Examples ```json diff --git a/src/character.cpp b/src/character.cpp index 8aadd44ebe79..b20623a8a33c 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -4917,7 +4917,8 @@ needs_rates Character::calc_needs_rates() const static const std::string player_thirst_rate( "PLAYER_THIRST_RATE" ); rates.thirst = get_option< float >( player_thirst_rate ); static const std::string thirst_modifier( "thirst_modifier" ); - rates.thirst *= 1.0f + mutation_value( thirst_modifier ); + rates.thirst *= 1.0f + mutation_value( thirst_modifier ) + + bonus_from_enchantments( 1.0, enchant_vals::mod::THIRST ); static const std::string slows_thirst( "SLOWS_THIRST" ); if( worn_with_flag( slows_thirst ) ) { rates.thirst *= 0.7f; @@ -4926,7 +4927,8 @@ needs_rates Character::calc_needs_rates() const static const std::string player_fatigue_rate( "PLAYER_FATIGUE_RATE" ); rates.fatigue = get_option< float >( player_fatigue_rate ); static const std::string fatigue_modifier( "fatigue_modifier" ); - rates.fatigue *= 1.0f + mutation_value( fatigue_modifier ); + rates.fatigue *= 1.0f + mutation_value( fatigue_modifier ) + + bonus_from_enchantments( 1.0, enchant_vals::mod::FATIGUE ); // Note: intentionally not in metabolic rate if( has_recycler ) { @@ -4971,6 +4973,11 @@ needs_rates Character::calc_needs_rates() const rates.thirst *= 0.25f; } + rates.thirst = std::max( rates.thirst, 0.0f ); + rates.hunger = std::max( rates.hunger, 0.0f ); + rates.fatigue = std::max( rates.fatigue, 0.0f ); + rates.recovery = std::max( rates.recovery, 0.0f ); + return rates; } @@ -7112,9 +7119,11 @@ int Character::get_stamina_max() const { static const std::string player_max_stamina( "PLAYER_MAX_STAMINA" ); static const std::string max_stamina_modifier( "max_stamina_modifier" ); - int maxStamina = get_option< int >( player_max_stamina ); + const int baseMaxStamina = get_option< int >( player_max_stamina ); + int maxStamina = baseMaxStamina; maxStamina *= Character::mutation_value( max_stamina_modifier ); - return maxStamina; + maxStamina += bonus_from_enchantments( maxStamina, enchant_vals::mod::STAMINA_CAP ); + return std::max( baseMaxStamina / 10, maxStamina ); } void Character::set_stamina( int new_stamina ) @@ -7191,7 +7200,8 @@ void Character::update_stamina( int turns ) // Recover some stamina every turn. // max stamina modifers from mutation also affect stamina multi float stamina_multiplier = 1.0f + mutation_value( stamina_regen_modifier ) + - ( mutation_value( "max_stamina_modifier" ) - 1.0f ); + ( mutation_value( "max_stamina_modifier" ) - 1.0f ) + + bonus_from_enchantments( 1.0, enchant_vals::mod::STAMINA_REGEN ); // But mouth encumbrance interferes, even with mutated stamina. stamina_recovery += stamina_multiplier * std::max( 1.0f, base_regen_rate - ( encumb( bp_mouth ) / 5.0f ) ); @@ -7208,8 +7218,10 @@ void Character::update_stamina( int turns ) // At -100 stim it inflicts -20 malus to regen at 100% stamina, // effectivly countering stamina gain of default 20, // at 50% stamina its -10 (50%), cuts by 25% at 25% stamina + // FIXME: this formula is only suitable for advancing by 1 turn stamina_recovery += current_stim / 5.0f * get_stamina() / get_stamina_max(); } + stamina_recovery = std::max( 0.0f, stamina_recovery ); const int max_stam = get_stamina_max(); if( get_power_level() >= 3_kJ && has_active_bionic( bio_gills ) ) { @@ -7987,7 +7999,9 @@ static void destroyed_armor_msg( Character &who, const std::string &pre_damage_n pre_damage_name ); } -static void item_armor_enchantment_adjust( Character &guy, damage_unit &du, item &armor ) +static void item_armor_enchantment_adjust( + const Character &guy, damage_unit &du, const item &armor +) { switch( du.type ) { case DT_ACID: @@ -8025,7 +8039,7 @@ static void item_armor_enchantment_adjust( Character &guy, damage_unit &du, item // adjusts damage unit depending on type by enchantments. // the ITEM_ enchantments only affect the damage resistance for that one item, while the others affect all of them -static void armor_enchantment_adjust( Character &guy, damage_unit &du ) +static void armor_enchantment_adjust( const Character &guy, damage_unit &du ) { switch( du.type ) { case DT_ACID: diff --git a/src/item.cpp b/src/item.cpp index a52f50b3cba3..ee9853ae360f 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -5162,6 +5162,20 @@ int item::damage_melee( damage_type dt ) const } + switch( dt ) { + case DT_BASH: + res += bonus_from_enchantments_wielded( res, enchant_vals::mod::ITEM_DAMAGE_BASH, true ); + break; + case DT_CUT: + res += bonus_from_enchantments_wielded( res, enchant_vals::mod::ITEM_DAMAGE_CUT, true ); + break; + case DT_STAB: + res += bonus_from_enchantments_wielded( res, enchant_vals::mod::ITEM_DAMAGE_STAB, true ); + break; + default: + break; + } + return std::max( res, 0 ); } diff --git a/src/magic_enchantment.cpp b/src/magic_enchantment.cpp index b1e6b5990830..02c2eadf5bd7 100644 --- a/src/magic_enchantment.cpp +++ b/src/magic_enchantment.cpp @@ -81,27 +81,11 @@ namespace io case enchant_vals::mod::METABOLISM: return "METABOLISM"; case enchant_vals::mod::MANA_CAP: return "MANA_CAP"; case enchant_vals::mod::MANA_REGEN: return "MANA_REGEN"; - case enchant_vals::mod::BIONIC_POWER: return "BIONIC_POWER"; - case enchant_vals::mod::MAX_STAMINA: return "MAX_STAMINA"; - case enchant_vals::mod::REGEN_STAMINA: return "REGEN_STAMINA"; - case enchant_vals::mod::MAX_HP: return "MAX_HP"; - case enchant_vals::mod::REGEN_HP: return "REGEN_HP"; + case enchant_vals::mod::STAMINA_CAP: return "STAMINA_CAP"; + case enchant_vals::mod::STAMINA_REGEN: return "STAMINA_REGEN"; case enchant_vals::mod::THIRST: return "THIRST"; case enchant_vals::mod::FATIGUE: return "FATIGUE"; - case enchant_vals::mod::PAIN: return "PAIN"; - case enchant_vals::mod::BONUS_DAMAGE: return "BONUS_DAMAGE"; - case enchant_vals::mod::BONUS_BLOCK: return "BONUS_BLOCK"; case enchant_vals::mod::BONUS_DODGE: return "BONUS_DODGE"; - case enchant_vals::mod::ATTACK_NOISE: return "ATTACK_NOISE"; - case enchant_vals::mod::SPELL_NOISE: return "SPELL_NOISE"; - case enchant_vals::mod::SHOUT_NOISE: return "SHOUT_NOISE"; - case enchant_vals::mod::FOOTSTEP_NOISE: return "FOOTSTEP_NOISE"; - case enchant_vals::mod::SIGHT_RANGE: return "SIGHT_RANGE"; - case enchant_vals::mod::CARRY_WEIGHT: return "CARRY_WEIGHT"; - case enchant_vals::mod::CARRY_VOLUME: return "CARRY_VOLUME"; - case enchant_vals::mod::SOCIAL_LIE: return "SOCIAL_LIE"; - case enchant_vals::mod::SOCIAL_PERSUADE: return "SOCIAL_PERSUADE"; - case enchant_vals::mod::SOCIAL_INTIMIDATE: return "SOCIAL_INTIMIDATE"; case enchant_vals::mod::ARMOR_ACID: return "ARMOR_ACID"; case enchant_vals::mod::ARMOR_BASH: return "ARMOR_BASH"; case enchant_vals::mod::ARMOR_BIO: return "ARMOR_BIO"; @@ -114,13 +98,6 @@ namespace io case enchant_vals::mod::ITEM_DAMAGE_BASH: return "ITEM_DAMAGE_BASH"; case enchant_vals::mod::ITEM_DAMAGE_CUT: return "ITEM_DAMAGE_CUT"; case enchant_vals::mod::ITEM_DAMAGE_STAB: return "ITEM_DAMAGE_STAB"; - case enchant_vals::mod::ITEM_DAMAGE_BULLET: return "ITEM_DAMAGE_BULLET"; - case enchant_vals::mod::ITEM_DAMAGE_HEAT: return "ITEM_DAMAGE_HEAT"; - case enchant_vals::mod::ITEM_DAMAGE_COLD: return "ITEM_DAMAGE_COLD"; - case enchant_vals::mod::ITEM_DAMAGE_ELEC: return "ITEM_DAMAGE_ELEC"; - case enchant_vals::mod::ITEM_DAMAGE_ACID: return "ITEM_DAMAGE_ACID"; - case enchant_vals::mod::ITEM_DAMAGE_BIO: return "ITEM_DAMAGE_BIO"; - case enchant_vals::mod::ITEM_DAMAGE_AP: return "ITEM_DAMAGE_AP"; case enchant_vals::mod::ITEM_ARMOR_BASH: return "ITEM_ARMOR_BASH"; case enchant_vals::mod::ITEM_ARMOR_CUT: return "ITEM_ARMOR_CUT"; case enchant_vals::mod::ITEM_ARMOR_STAB: return "ITEM_ARMOR_STAB"; @@ -130,12 +107,7 @@ namespace io case enchant_vals::mod::ITEM_ARMOR_ELEC: return "ITEM_ARMOR_ELEC"; case enchant_vals::mod::ITEM_ARMOR_ACID: return "ITEM_ARMOR_ACID"; case enchant_vals::mod::ITEM_ARMOR_BIO: return "ITEM_ARMOR_BIO"; - case enchant_vals::mod::ITEM_WEIGHT: return "ITEM_WEIGHT"; - case enchant_vals::mod::ITEM_ENCUMBRANCE: return "ITEM_ENCUMBRANCE"; - case enchant_vals::mod::ITEM_VOLUME: return "ITEM_VOLUME"; - case enchant_vals::mod::ITEM_COVERAGE: return "ITEM_COVERAGE"; case enchant_vals::mod::ITEM_ATTACK_COST: return "ITEM_ATTACK_COST"; - case enchant_vals::mod::ITEM_WET_PROTECTION: return "ITEM_WET_PROTECTION"; case enchant_vals::mod::NUM_MOD: break; } debugmsg( "Invalid enchant_vals::mod" ); @@ -144,17 +116,22 @@ namespace io // *INDENT-ON* } // namespace io -static void migrate_ench_vals_enums( std::string &s ) +static std::string migrate_ench_vals_enums( const std::string &s ) { if( s == "ITEM_ATTACK_SPEED" ) { - s = "ITEM_ATTACK_COST"; + return "ITEM_ATTACK_COST"; } else if( s == "ATTACK_SPEED" ) { - s = "ATTACK_COST"; + return "ATTACK_COST"; } else if( s == "MAX_MANA" ) { - s = "MANA_CAP"; + return "MANA_CAP"; } else if( s == "REGEN_MANA" ) { - s = "MANA_REGEN"; + return "MANA_REGEN"; + } else if( s == "MAX_STAMINA" ) { + return "STAMINA_CAP"; + } else if( s == "REGEN_STAMINA" ) { + return "STAMINA_REGEN"; } + return s; } namespace @@ -267,8 +244,15 @@ void enchantment::load( const JsonObject &jo, const std::string & ) if( jo.has_array( "values" ) ) { for( const JsonObject value_obj : jo.get_array( "values" ) ) { std::string value_raw = value_obj.get_string( "value" ); - migrate_ench_vals_enums( value_raw ); - const enchant_vals::mod value = io::string_to_enum( value_raw ); + std::string value_new = migrate_ench_vals_enums( value_raw ); + if( json_report_strict && value_new != value_raw ) { + try { + value_obj.throw_error( string_format( "%s has been renamed to %s", value_raw, value_new ) ); + } catch( const std::exception &e ) { + debugmsg( "%s", e.what() ); + } + } + const enchant_vals::mod value = io::string_to_enum( value_new ); const int add = value_obj.get_int( "add", 0 ); const double mult = value_obj.get_float( "multiply", 0.0 ); @@ -437,6 +421,10 @@ double enchantment::calc_bonus( enchant_vals::mod value, double base, bool round switch( value ) { case enchant_vals::mod::METABOLISM: case enchant_vals::mod::MANA_REGEN: + case enchant_vals::mod::STAMINA_CAP: + case enchant_vals::mod::STAMINA_REGEN: + case enchant_vals::mod::THIRST: + case enchant_vals::mod::FATIGUE: use_add = false; break; default: diff --git a/src/magic_enchantment.h b/src/magic_enchantment.h index f0963d3fa7d5..49c7999c9617 100644 --- a/src/magic_enchantment.h +++ b/src/magic_enchantment.h @@ -35,27 +35,11 @@ enum class mod : int { METABOLISM, MANA_CAP, MANA_REGEN, - BIONIC_POWER, - MAX_STAMINA, - REGEN_STAMINA, - MAX_HP, - REGEN_HP, + STAMINA_CAP, + STAMINA_REGEN, THIRST, FATIGUE, - PAIN, BONUS_DODGE, - BONUS_BLOCK, - BONUS_DAMAGE, - ATTACK_NOISE, - SPELL_NOISE, - SHOUT_NOISE, - FOOTSTEP_NOISE, - SIGHT_RANGE, - CARRY_WEIGHT, - CARRY_VOLUME, - SOCIAL_LIE, - SOCIAL_PERSUADE, - SOCIAL_INTIMIDATE, ARMOR_BASH, ARMOR_CUT, ARMOR_STAB, @@ -69,13 +53,6 @@ enum class mod : int { ITEM_DAMAGE_BASH, ITEM_DAMAGE_CUT, ITEM_DAMAGE_STAB, - ITEM_DAMAGE_BULLET, - ITEM_DAMAGE_HEAT, - ITEM_DAMAGE_COLD, - ITEM_DAMAGE_ELEC, - ITEM_DAMAGE_ACID, - ITEM_DAMAGE_BIO, - ITEM_DAMAGE_AP, ITEM_ARMOR_BASH, ITEM_ARMOR_CUT, ITEM_ARMOR_STAB, @@ -85,12 +62,7 @@ enum class mod : int { ITEM_ARMOR_ELEC, ITEM_ARMOR_ACID, ITEM_ARMOR_BIO, - ITEM_WEIGHT, - ITEM_ENCUMBRANCE, - ITEM_VOLUME, - ITEM_COVERAGE, ITEM_ATTACK_COST, - ITEM_WET_PROTECTION, NUM_MOD }; } // namespace enchant_vals diff --git a/tests/enchantment_test.cpp b/tests/enchantment_test.cpp index 32a80149a286..1612a592542c 100644 --- a/tests/enchantment_test.cpp +++ b/tests/enchantment_test.cpp @@ -19,10 +19,19 @@ static void advance_turn( Character &guy ) calendar::turn += 1_turns; } -static void give_item( Character &guy, const std::string &item_id ) +static item &give_item( Character &guy, const std::string &item_id ) { - guy.i_add( item( item_id ) ); + item &ret = guy.i_add( item( item_id ) ); guy.recalculate_enchantment_cache(); + return ret; +} + +static item &wear_item( Character &guy, const std::string &item_id ) +{ + item &ret = guy.i_add( item( item_id ) ); + guy.wear_item( ret, false ); + guy.recalculate_enchantment_cache(); + return ret; } static void clear_items( Character &guy ) @@ -37,7 +46,6 @@ TEST_CASE( "Enchantments grant mutations", "[magic][enchantment][trait][mutation Character &guy = get_player_character(); clear_character( *guy.as_player(), true ); - guy.recalculate_enchantment_cache(); advance_turn( guy ); std::string s_relic = "test_relic_gives_trait"; @@ -106,7 +114,6 @@ TEST_CASE( "Enchantments apply effects", "[magic][enchantment][effect]" ) Character &guy = get_player_character(); clear_character( *guy.as_player(), true ); - guy.recalculate_enchantment_cache(); advance_turn( guy ); std::string s_relic = "architect_cube"; @@ -158,7 +165,6 @@ static void tests_stats( Character &guy, int s_base, int d_base, int p_base, int guy.per_max = p_base; guy.int_max = i_base; - guy.recalculate_enchantment_cache(); advance_turn( guy ); auto check_stats = [&]( int s, int d, int p, int i ) { @@ -225,7 +231,6 @@ TEST_CASE( "Enchantments modify stats", "[magic][enchantment][character]" ) static void tests_speed( Character &guy, int sp_base, int sp_exp ) { - guy.recalculate_enchantment_cache(); guy.set_speed_base( sp_base ); guy.set_speed_bonus( 0 ); @@ -311,7 +316,6 @@ TEST_CASE( "Enchantments modify speed", "[magic][enchantment][speed]" ) static void tests_attack_cost( Character &guy, const item &weap, int item_atk_cost, int guy_atk_cost, int exp_guy_atk_cost ) { - guy.recalculate_enchantment_cache(); advance_turn( guy ); REQUIRE( weap.attack_cost() == item_atk_cost ); @@ -357,7 +361,6 @@ TEST_CASE( "Enchantments modify attack cost", "[magic][enchantment][melee]" ) static void tests_move_cost( Character &guy, int tile_move_cost, int move_cost, int exp_move_cost ) { - guy.recalculate_enchantment_cache(); advance_turn( guy ); std::string s_relic = "test_relic_mods_mv_cost"; @@ -416,7 +419,6 @@ TEST_CASE( "Enchantments modify move cost", "[magic][enchantment][move]" ) static void tests_metabolic_rate( Character &guy, float norm, float exp ) { - guy.recalculate_enchantment_cache(); advance_turn( guy ); std::string s_relic = "test_relic_mods_metabolism"; @@ -451,7 +453,6 @@ TEST_CASE( "Enchantments modify metabolic rate", "[magic][enchantment][metabolis Character &guy = get_player_character(); clear_character( *guy.as_player(), true ); - guy.recalculate_enchantment_cache(); advance_turn( guy ); const float normal_mr = get_option( "PLAYER_HUNGER_RATE" ); @@ -495,7 +496,6 @@ static void tests_mana_pool( Character &guy, const mana_test_case &t ) double norm_regen_rate = t.norm_regen_amt_8h / to_turns( time_duration::from_hours( 8 ) ); double exp_regen_rate = t.exp_regen_amt_8h / to_turns( time_duration::from_hours( 8 ) ); - guy.recalculate_enchantment_cache(); advance_turn( guy ); guy.set_max_power_level( 2000_kJ ); @@ -555,3 +555,394 @@ TEST_CASE( "Mana pool", "[magic][enchantment][mana][bionic]" ) tests_mana_pool_section( it ); } } + +static float measure_stamina_gain_rate( Character &guy ) +{ + int gained_total = 0; + // Stamina regen rate is supposed to decrease over time as character gains stamina, + // so we measure 100 times on same level instead of doing update_stamina( 100 ) + for( int i = 0; i < 100; i++ ) { + guy.set_stamina( 0 ); + if( guy.get_stamina() != 0 ) { + // Hide this behind an if check to avoid spamming check counter + REQUIRE( guy.get_stamina() == 0 ); + } + guy.update_stamina( 1 ); + gained_total += guy.get_stamina(); + } + return gained_total / 100.0f; +} + +static void tests_stamina( Character &guy, + int cap_norm, int cap_exp, + float rate_norm, float rate_exp + ) +{ + advance_turn( guy ); + + std::string s_relic = "test_relic_mods_stamina"; + + REQUIRE( guy.get_stamina_max() == cap_norm ); + REQUIRE( measure_stamina_gain_rate( guy ) == Approx( rate_norm ) ); + + WHEN( "Character receives relic" ) { + give_item( guy, s_relic ); + THEN( "Stamina cap changes" ) { + CHECK( guy.get_stamina_max() == cap_exp ); + AND_WHEN( "Character loses relic" ) { + clear_items( guy ); + THEN( "Stamina cap goes back to normal" ) { + CHECK( guy.get_stamina_max() == cap_norm ); + } + } + } + THEN( "Stamina gain rate changes" ) { + CHECK( measure_stamina_gain_rate( guy ) == Approx( rate_exp ) ); + AND_WHEN( "Character loses relic" ) { + clear_items( guy ); + THEN( "Stamina gain rate goes back to normal" ) { + CHECK( measure_stamina_gain_rate( guy ) == Approx( rate_norm ) ); + } + } + } + } + WHEN( "Character receives 15 relics" ) { + for( int i = 0; i < 15; i++ ) { + give_item( guy, s_relic ); + } + THEN( "Stamina cap does not go below 0.1 of PLAYER_MAX_STAMINA" ) { + const int base_cap = get_option( "PLAYER_MAX_STAMINA" ); + CHECK( guy.get_stamina_max() == ( base_cap / 10 ) ); + } + THEN( "Stamina gain rate does not go below 0" ) { + CHECK( measure_stamina_gain_rate( guy ) == Approx( 0.0f ) ); + } + } +} + +TEST_CASE( "Enchantments modify stamina", "[magic][enchantment][stamina]" ) +{ + clear_all_state(); + Character &guy = get_player_character(); + clear_character( *guy.as_player(), true ); + + advance_turn( guy ); + + REQUIRE( guy.get_stim() == 0 ); + + const int normal_cap = get_option( "PLAYER_MAX_STAMINA" ); + REQUIRE( normal_cap == 10000 ); + REQUIRE( guy.get_stamina_max() == normal_cap ); + + const float normal_rate = get_option( "PLAYER_BASE_STAMINA_REGEN_RATE" ); + REQUIRE( normal_rate == Approx( 20.0f ) ); + REQUIRE( measure_stamina_gain_rate( guy ) == Approx( normal_rate ) ); + + guy.set_stamina( 0 ); + REQUIRE( guy.get_stamina() == 0 ); + + SECTION( "Clean character" ) { + tests_stamina( guy, 10000, 9000, 20.0f, 18.0f ); + } + SECTION( "Character with GOODCARDIO trait" ) { + trait_id tr( "GOODCARDIO" ); + guy.set_mutation( tr ); + REQUIRE( guy.has_trait( tr ) ); + + tests_stamina( guy, 12500, 11250, 25.0f, 23.0f ); + } + SECTION( "Character with PERSISTENCE_HUNTER trait" ) { + trait_id tr( "PERSISTENCE_HUNTER" ); + guy.set_mutation( tr ); + REQUIRE( guy.has_trait( tr ) ); + + tests_stamina( guy, 10000, 9000, 22.0f, 20.0f ); + } + SECTION( "Character with GOODCARDIO and PERSISTENCE_HUNTER traits" ) { + { + trait_id tr( "GOODCARDIO" ); + guy.set_mutation( tr ); + REQUIRE( guy.has_trait( tr ) ); + } + { + trait_id tr( "PERSISTENCE_HUNTER" ); + guy.set_mutation( tr ); + REQUIRE( guy.has_trait( tr ) ); + } + tests_stamina( guy, 12500, 11250, 27.0f, 25.0f ); + } +} + +template +static void tests_need_rate( Character &guy, const std::string &s_relic, float norm, float exp, + F getter ) +{ + advance_turn( guy ); + + REQUIRE( getter( guy ) == Approx( norm ) ); + + WHEN( "Character receives relic" ) { + give_item( guy, s_relic ); + THEN( "Need rate changes" ) { + CHECK( getter( guy ) == Approx( exp ) ); + AND_WHEN( "Character loses relic" ) { + clear_items( guy ); + THEN( "Need rate goes back to normal" ) { + CHECK( getter( guy ) == Approx( norm ) ); + } + } + } + } + WHEN( "Character receives 15 relics" ) { + for( int i = 0; i < 15; i++ ) { + give_item( guy, s_relic ); + } + THEN( "Need rate does not go below 0" ) { + CHECK( getter( guy ) == Approx( 0.0f ) ); + } + } +} + +TEST_CASE( "Enchantments modify thirst rate", "[magic][enchantment][thirst]" ) +{ + clear_all_state(); + Character &guy = get_player_character(); + clear_character( *guy.as_player(), true ); + + advance_turn( guy ); + + std::string s_relic = "test_relic_mods_thirst"; + const auto getter = []( const Character & guy ) -> float { + return guy.calc_needs_rates().thirst; + }; + + const float normal_rate = get_option( "PLAYER_THIRST_RATE" ); + REQUIRE( normal_rate == Approx( 1.0f ) ); + REQUIRE( getter( guy ) == Approx( normal_rate ) ); + + SECTION( "Clean character" ) { + tests_need_rate( guy, s_relic, 1.0f, 0.9f, getter ); + } + SECTION( "Character with THIRST trait" ) { + trait_id tr( "THIRST" ); + guy.set_mutation( tr ); + REQUIRE( guy.has_trait( tr ) ); + + tests_need_rate( guy, s_relic, 1.5f, 1.4f, getter ); + } +} + +TEST_CASE( "Enchantments modify fatigue rate", "[magic][enchantment][fatigue]" ) +{ + clear_all_state(); + Character &guy = get_player_character(); + clear_character( *guy.as_player(), true ); + + advance_turn( guy ); + + std::string s_relic = "test_relic_mods_fatigue"; + const auto getter = []( const Character & guy ) -> float { + return guy.calc_needs_rates().fatigue; + }; + + const float normal_rate = get_option( "PLAYER_THIRST_RATE" ); + REQUIRE( normal_rate == Approx( 1.0f ) ); + REQUIRE( getter( guy ) == Approx( normal_rate ) ); + + SECTION( "Clean character" ) { + tests_need_rate( guy, s_relic, 1.0f, 0.9f, getter ); + } + SECTION( "Character with WAKEFUL trait" ) { + trait_id tr( "WAKEFUL" ); + guy.set_mutation( tr ); + REQUIRE( guy.has_trait( tr ) ); + + tests_need_rate( guy, s_relic, 0.85f, 0.75f, getter ); + } +} + +static void check_num_dodges( const Character &guy, int num ) +{ + CHECK( guy.get_num_dodges() == num ); + CHECK( guy.dodges_left == num ); +} + +static void tests_num_dodges( Character &guy ) +{ + // Must have some moves to gain dodges + guy.moves = 1; + guy.dodges_left = 0; + + advance_turn( guy ); + + REQUIRE( guy.get_num_dodges_base() == 1 ); + REQUIRE( guy.get_num_dodges_bonus() == 0 ); + REQUIRE( guy.get_num_dodges() == 1 ); + REQUIRE( guy.dodges_left == 1 ); + + std::string s_relic = "test_relic_mods_dodges"; + + WHEN( "Character has no relics" ) { + THEN( "Dodges bonus remain unaffected" ) { + guy.moves = 1; + guy.dodges_left = 0; + advance_turn( guy ); + check_num_dodges( guy, 1 ); + } + } + WHEN( "Character receives relic" ) { + give_item( guy, s_relic ); + THEN( "Nothing changes" ) { + check_num_dodges( guy, 1 ); + } + AND_WHEN( "Turn passes" ) { + guy.moves = 1; + guy.dodges_left = 0; + advance_turn( guy ); + THEN( "Dodge bonus changes, dodges increase" ) { + check_num_dodges( guy, 2 ); + } + AND_WHEN( "Character loses relic" ) { + clear_items( guy ); + THEN( "Nothing changes" ) { + check_num_dodges( guy, 2 ); + } + AND_WHEN( "Turn passes" ) { + guy.moves = 1; + guy.dodges_left = 0; + advance_turn( guy ); + THEN( "Dodge bonus and dodge gain return to normal" ) { + check_num_dodges( guy, 1 ); + } + } + } + } + } + WHEN( "Character receives 10 relics" ) { + for( int i = 0; i < 10; i++ ) { + give_item( guy, s_relic ); + } + THEN( "Nothing changes" ) { + check_num_dodges( guy, 1 ); + } + AND_WHEN( "Turn passes" ) { + guy.moves = 1; + guy.dodges_left = 0; + advance_turn( guy ); + THEN( "Dodge bonus and dodge gain increase by 10" ) { + check_num_dodges( guy, 11 ); + } + } + } +} + +TEST_CASE( "Enchantments grant bonus dodges", "[magic][enchantment][dodge]" ) +{ + clear_all_state(); + Character &guy = get_player_character(); + clear_character( *guy.as_player(), true ); + + tests_num_dodges( guy ); +} + +TEST_CASE( "Item enchantments modify item damage", "[magic][enchantment]" ) +{ + clear_all_state(); + Character &guy = get_player_character(); + clear_character( *guy.as_player(), true ); + + SECTION( "Cut damage" ) { + item &base = give_item( guy, "test_balanced_sword" ); + item &impr = give_item( guy, "test_relic_mods_cut_dmg" ); + + REQUIRE( base.damage_melee( damage_type::DT_CUT ) == 32 ); + CHECK( impr.damage_melee( damage_type::DT_CUT ) == 17 ); + } + SECTION( "Stab damage" ) { + item &base = give_item( guy, "test_screwdriver" ); + item &impr = give_item( guy, "test_relic_mods_stab_dmg" ); + + REQUIRE( base.damage_melee( damage_type::DT_STAB ) == 6 ); + CHECK( impr.damage_melee( damage_type::DT_STAB ) == 4 ); + } + SECTION( "Bash damage" ) { + item &base = give_item( guy, "test_halligan" ); + item &impr = give_item( guy, "test_relic_mods_bash_dmg" ); + + REQUIRE( base.damage_melee( damage_type::DT_BASH ) == 20 ); + CHECK( impr.damage_melee( damage_type::DT_BASH ) == 11 ); + } +} + +static int calc_damage_absorb( Character &guy, damage_type dt, int amount ) +{ + static const bodypart_id torso( "torso" ); + damage_instance dmg( dt, amount ); + guy.absorb_hit( torso, dmg ); + assert( dmg.damage_units.size() == 1 ); + return amount - dmg.damage_units[0].amount; +} + +TEST_CASE( "Armor enchantments", "[magic][enchantment][armor]" ) +{ + clear_all_state(); + Character &guy = get_player_character(); + clear_character( *guy.as_player(), true ); + + REQUIRE( calc_damage_absorb( guy, damage_type::DT_CUT, 10 ) == 0 ); + REQUIRE( calc_damage_absorb( guy, damage_type::DT_BASH, 10 ) == 0 ); + REQUIRE( calc_damage_absorb( guy, damage_type::DT_STAB, 10 ) == 0 ); + + SECTION( "Armor item with no enchantments" ) { + wear_item( guy, "test_hazmat_suit" ); + + SECTION( "Cut" ) { + // 10 (incoming) - 4 (base item cut armor) = 6 (4 absorbed) + CHECK( calc_damage_absorb( guy, damage_type::DT_CUT, 10 ) == 4 ); + } + SECTION( "Bash" ) { + // 10 (incoming) - 4 (base item bash armor) = 6 (4 absorbed) + CHECK( calc_damage_absorb( guy, damage_type::DT_BASH, 10 ) == 4 ); + } + SECTION( "Stab" ) { + // 10 (incoming) - 3 (base item stab armor) = 7 (3 absorbed) + CHECK( calc_damage_absorb( guy, damage_type::DT_STAB, 10 ) == 3 ); + } + } + + SECTION( "Armor item with enchantment that trades bash armor for cut armor" ) { + wear_item( guy, "test_relic_item_armor_mod" ); + + SECTION( "Cut" ) { + // 10 (incoming) + (10 * -0.5 + 3) (enchantment) - 4 (base item cut armor) = 4 (6 absorbed) + CHECK( calc_damage_absorb( guy, damage_type::DT_CUT, 10 ) == 6 ); + } + SECTION( "Bash" ) { + // 10 (incoming) + (10 * 0.5 - 3) (enchantment) - 4 (base item bash armor) = 8 (2 absorbed) + CHECK( calc_damage_absorb( guy, damage_type::DT_BASH, 10 ) == 2 ); + } + SECTION( "Stab" ) { + // 10 (incoming) - 3 (base item stab armor) = 7 (3 absorbed) + CHECK( calc_damage_absorb( guy, damage_type::DT_STAB, 10 ) == 3 ); + } + } + + SECTION( "Armor item with no enchantments + socks of protection" ) { + wear_item( guy, "test_hazmat_suit" ); + // The socks provide character-wide protection regardless of what body parts they cover + wear_item( guy, "test_relic_char_armor_mod" ); + + SECTION( "Cut" ) { + // 10 (incoming) + (10 * -0.5 - 2) (enchantment) - 4 (base item cut armor) = -1 (10 absorbed) + CHECK( calc_damage_absorb( guy, damage_type::DT_CUT, 10 ) == 10 ); + } + SECTION( "Bash" ) { + // 10 (incoming) - 4 (base item bash armor) = 6 (4 absorbed) + CHECK( calc_damage_absorb( guy, damage_type::DT_BASH, 10 ) == 4 ); + } + SECTION( "Stab" ) { + // 10 (incoming) + (10 * -0.1 - 3) (enchantment) - 3 (base item stab armor) = 3 (7 absorbed) + CHECK( calc_damage_absorb( guy, damage_type::DT_STAB, 10 ) == 7 ); + } + } +} diff --git a/tests/player_helpers.cpp b/tests/player_helpers.cpp index 06f0d5911bdf..2521193611fd 100644 --- a/tests/player_helpers.cpp +++ b/tests/player_helpers.cpp @@ -73,6 +73,8 @@ void clear_character( player &dummy, bool debug_storage ) dummy.set_power_level( 0_J ); dummy.set_max_power_level( 0_J ); + dummy.recalculate_enchantment_cache(); + // Clear stomach and then eat a nutritious meal to normalize stomach // contents (needs to happen before clear_morale). dummy.stomach.empty(); @@ -115,6 +117,7 @@ void clear_character( player &dummy, bool debug_storage ) dummy.set_all_parts_hp_to_max(); dummy.cash = 0; + dummy.dodges_left = 1; const tripoint spot( 60, 60, 0 ); dummy.setpos( spot );