diff --git a/data/mods/TEST_DATA/items.json b/data/mods/TEST_DATA/items.json index 577adef2b978..fa4f7373aec9 100644 --- a/data/mods/TEST_DATA/items.json +++ b/data/mods/TEST_DATA/items.json @@ -21,6 +21,18 @@ "effects": [ "NEVER_MISFIRES", "NON-FOULING", "RECOVER_80" ], "qualities": [ [ "HAMMER", 1 ] ] }, + { + "type": "GENERIC", + "id": "test_1kg_cube", + "name": "TEST 1kg cube", + "description": "A tiny super-dense cube with nice round weight of 1 kg.", + "weight": "1 kg", + "color": "light_gray", + "symbol": "*", + "material": [ "steel" ], + "volume": "10 ml", + "category": "spare_parts" + }, { "id": "test_rag", "type": "TOOL", diff --git a/data/mods/TEST_DATA/relics.json b/data/mods/TEST_DATA/relics.json new file mode 100644 index 000000000000..bcc346e95252 --- /dev/null +++ b/data/mods/TEST_DATA/relics.json @@ -0,0 +1,112 @@ +[ + { + "type": "GENERIC", + "id": "test_relic_base", + "name": "TEST relic base", + "description": "A relic for test purposes", + "category": "spare_parts", + "weight": "1 kg", + "volume": "100 ml", + "material": [ "steel" ], + "color": "red", + "symbol": "*" + }, + { + "type": "GENERIC", + "id": "test_relic_gives_trait", + "copy-from": "test_relic_base", + "name": "TEST relic gives trait", + "relic_data": { "passive_effects": [ { "has": "HELD", "condition": "ALWAYS", "mutations": [ "CARNIVORE" ] } ] } + }, + { + "type": "GENERIC", + "id": "test_relic_mods_stats", + "copy-from": "test_relic_base", + "name": "TEST relic mods stats", + "relic_data": { + "passive_effects": [ + { + "has": "HELD", + "condition": "ALWAYS", + "values": [ + { "value": "STRENGTH", "add": 4, "multiply": 1 }, + { "value": "DEXTERITY", "add": -2 }, + { "value": "PERCEPTION", "add": 1, "multiply": -0.5 }, + { "value": "INTELLIGENCE", "add": -11 } + ] + } + ] + } + }, + { + "type": "GENERIC", + "id": "test_relic_mods_speed", + "copy-from": "test_relic_base", + "name": "TEST relic mods speed", + "relic_data": { + "passive_effects": [ { "has": "HELD", "condition": "ALWAYS", "values": [ { "value": "SPEED", "add": 25, "multiply": -0.5 } ] } ] + } + }, + { + "type": "GENERIC", + "id": "test_relic_mods_atk_cost", + "copy-from": "test_relic_base", + "name": "TEST relic mods attack cost", + "relic_data": { + "passive_effects": [ { "has": "HELD", "condition": "ALWAYS", "values": [ { "value": "ATTACK_COST", "multiply": -0.2 } ] } ] + } + }, + { + "type": "GENERIC", + "id": "test_relic_sword", + "name": "TEST relic sword", + "description": "A relic sword for test purposes", + "category": "spare_parts", + "weight": "1250 g", + "to_hit": 1, + "color": "dark_gray", + "symbol": "/", + "material": [ "steel" ], + "volume": "1 L", + "bashing": 32, + "cutting": 32, + "price": 7500, + "price_postapoc": 300, + "relic_data": { + "passive_effects": [ { "has": "HELD", "condition": "ALWAYS", "values": [ { "value": "ITEM_ATTACK_COST", "add": -15 } ] } ] + } + }, + { + "type": "GENERIC", + "id": "test_relic_mods_mv_cost", + "copy-from": "test_relic_base", + "name": "TEST relic mods movement cost", + "relic_data": { + "passive_effects": [ { "has": "HELD", "condition": "ALWAYS", "values": [ { "value": "MOVE_COST", "multiply": -0.1 } ] } ] + } + }, + { + "type": "GENERIC", + "id": "test_relic_mods_metabolism", + "copy-from": "test_relic_base", + "name": "TEST relic mods metabolic rate", + "relic_data": { + "passive_effects": [ { "has": "HELD", "condition": "ALWAYS", "values": [ { "value": "METABOLISM", "multiply": -0.1 } ] } ] + } + }, + { + "type": "GENERIC", + "id": "test_relic_mods_manapool", + "copy-from": "test_relic_base", + "name": "TEST relic mods mana pool", + "relic_data": { + "passive_effects": [ + { + "has": "HELD", + "condition": "ALWAYS", + "values": [ { "value": "MANA_CAP", "add": 100, "multiply": -0.3 }, { "value": "MANA_REGEN", "multiply": -0.3 } ] + } + ] + } + } +] diff --git a/doc/MAGIC.md b/doc/MAGIC.md index 9bd8061c8424..1d9b28072de8 100644 --- a/doc/MAGIC.md +++ b/doc/MAGIC.md @@ -330,7 +330,7 @@ Syntax: ``` ### values -(array) List of miscellaneous values to modify. +(array) List of miscellaneous character/item values to modify. Syntax for single entry: ```c++ @@ -338,40 +338,101 @@ Syntax for single entry: // (required) Value ID to modify, refer to list below. "value": "VALUE_ID_STRING" - // Additive bonus. Optional, default is 0. - "add": 13.37, + // Additive bonus. Optional integer number, default is 0. + // May be ignored for some values. + "add": 13, // Multiplicative bonus. Optional, default is 0. "multiply": -0.3, } ``` -Additive bonus is applied before multiplicative, like so: +Additive bonus is applied separately from multiplicative, like so: ```c++ bonus = add + base_value * multiply ``` Thus, a `multiply` value of -0.8 is -80%, and a `multiply` of 2.5 is +250%. +When modifying integer values, final bonus is rounded towards 0 (truncated). + +When multiple enchantments (e.g. one from an item and one from a bionic) modify the same value, +their bonuses are added together without rounding, then the sum is rounded (if necessary) +before being applied to the base value. + +Since there's no limit on number of enchantments the character can have at a time, +the final calculated values have hardcoded bounds to prevent unintended behavior. #### IDs of modifiable values +#### Character values + +##### STRENGTH +Strength stat. +`base_value` here is the base stat value. +The final value cannot go below 0. + +##### DEXTERITY +Dexterity stat. +`base_value` here is the base stat value. +The final value cannot go below 0. + +##### PERCEPTION +Perception stat. +`base_value` here is the base stat value. +The final value cannot go below 0. + +##### INTELLIGENCE +Intelligence stat. +`base_value` here is the base stat value. +The final value cannot go below 0. + +##### SPEED +Character speed. +`base_value` here is character speed including pain/hunger/weight penalties. +Final speed value cannot go below 25% of base speed. + +##### ATTACK_COST +Melee attack cost. The lower, the better. +`base_value` here is attack cost for given weapon including modifiers from stats and skills. +The final value cannot go below 25. + +##### MOVE_COST +Movement cost. +`base_value` here is tile movement cost including modifiers from clothing and traits. +The final value cannot go below 20. + +##### METABOLISM +Metabolic rate. +This modifier ignores `add` field. +`base_value` here is `PLAYER_HUNGER_RATE` modified by traits. +The final value cannot go below 0. + +##### MANA_CAP +Mana capacity. +`base_value` here is character's base mana capacity modified by traits. +The final value cannot go below 0. + +##### MANA_REGEN +Mana regeneration rate. +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. + +#### Item values + +##### ITEM_ATTACK_COST +Attack cost (melee or throwing) for this item. +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 -* STRENGTH -* DEXTERITY -* PERCEPTION -* INTELLIGENCE -* SPEED -* ATTACK_COST -* ATTACK_SPEED -* MOVE_COST -* METABOLISM -* MAX_MANA -* REGEN_MANA + * BIONIC_POWER * MAX_STAMINA * REGEN_STAMINA @@ -424,7 +485,6 @@ Effects for the item that has the enchantment: * ITEM_ENCUMBRANCE * ITEM_VOLUME * ITEM_COVERAGE -* ITEM_ATTACK_SPEED * ITEM_WET_PROTECTION ## Examples diff --git a/src/character.cpp b/src/character.cpp index 4ee3289d81ad..524f1ba2fafe 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -4550,7 +4550,7 @@ void Character::update_body( const time_point &from, const time_point &to ) update_stomach( from, to ); recalculate_enchantment_cache(); if( ticks_between( from, to, 3_minutes ) > 0 ) { - magic->update_mana( *this->as_player(), to_turns( 3_minutes ) ); + magic->update_mana( *this->as_player(), to_turns( 3_minutes ) ); } const int five_mins = ticks_between( from, to, 5_minutes ); if( five_mins > 0 ) { @@ -7701,15 +7701,9 @@ void Character::recalculate_enchantment_cache() } } -double Character::calculate_by_enchantment( double modify, enchant_vals::mod value, - bool round_output ) const +double Character::bonus_from_enchantments( double base, enchant_vals::mod value, bool round ) const { - modify += enchantment_cache->get_value_add( value ); - modify *= 1.0 + enchantment_cache->get_value_multiply( value ); - if( round_output ) { - modify = std::round( modify ); - } - return modify; + return enchantment_cache->calc_bonus( value, base, round ); } void Character::passive_absorb_hit( body_part bp, damage_unit &du ) const @@ -7749,28 +7743,28 @@ static void item_armor_enchantment_adjust( Character &guy, damage_unit &du, item { switch( du.type ) { case DT_ACID: - du.amount = armor.calculate_by_enchantment( guy, du.amount, enchant_vals::mod::ITEM_ARMOR_ACID ); + du.amount += armor.bonus_from_enchantments( guy, du.amount, enchant_vals::mod::ITEM_ARMOR_ACID ); break; case DT_BASH: - du.amount = armor.calculate_by_enchantment( guy, du.amount, enchant_vals::mod::ITEM_ARMOR_BASH ); + du.amount += armor.bonus_from_enchantments( guy, du.amount, enchant_vals::mod::ITEM_ARMOR_BASH ); break; case DT_BIOLOGICAL: - du.amount = armor.calculate_by_enchantment( guy, du.amount, enchant_vals::mod::ITEM_ARMOR_BIO ); + du.amount += armor.bonus_from_enchantments( guy, du.amount, enchant_vals::mod::ITEM_ARMOR_BIO ); break; case DT_COLD: - du.amount = armor.calculate_by_enchantment( guy, du.amount, enchant_vals::mod::ITEM_ARMOR_COLD ); + du.amount += armor.bonus_from_enchantments( guy, du.amount, enchant_vals::mod::ITEM_ARMOR_COLD ); break; case DT_CUT: - du.amount = armor.calculate_by_enchantment( guy, du.amount, enchant_vals::mod::ITEM_ARMOR_CUT ); + du.amount += armor.bonus_from_enchantments( guy, du.amount, enchant_vals::mod::ITEM_ARMOR_CUT ); break; case DT_ELECTRIC: - du.amount = armor.calculate_by_enchantment( guy, du.amount, enchant_vals::mod::ITEM_ARMOR_ELEC ); + du.amount += armor.bonus_from_enchantments( guy, du.amount, enchant_vals::mod::ITEM_ARMOR_ELEC ); break; case DT_HEAT: - du.amount = armor.calculate_by_enchantment( guy, du.amount, enchant_vals::mod::ITEM_ARMOR_HEAT ); + du.amount += armor.bonus_from_enchantments( guy, du.amount, enchant_vals::mod::ITEM_ARMOR_HEAT ); break; case DT_STAB: - du.amount = armor.calculate_by_enchantment( guy, du.amount, enchant_vals::mod::ITEM_ARMOR_STAB ); + du.amount += armor.bonus_from_enchantments( guy, du.amount, enchant_vals::mod::ITEM_ARMOR_STAB ); break; default: return; @@ -7784,28 +7778,28 @@ static void armor_enchantment_adjust( Character &guy, damage_unit &du ) { switch( du.type ) { case DT_ACID: - du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::ARMOR_ACID ); + du.amount += guy.bonus_from_enchantments( du.amount, enchant_vals::mod::ARMOR_ACID ); break; case DT_BASH: - du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::ARMOR_BASH ); + du.amount += guy.bonus_from_enchantments( du.amount, enchant_vals::mod::ARMOR_BASH ); break; case DT_BIOLOGICAL: - du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::ARMOR_BIO ); + du.amount += guy.bonus_from_enchantments( du.amount, enchant_vals::mod::ARMOR_BIO ); break; case DT_COLD: - du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::ARMOR_COLD ); + du.amount += guy.bonus_from_enchantments( du.amount, enchant_vals::mod::ARMOR_COLD ); break; case DT_CUT: - du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::ARMOR_CUT ); + du.amount += guy.bonus_from_enchantments( du.amount, enchant_vals::mod::ARMOR_CUT ); break; case DT_ELECTRIC: - du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::ARMOR_ELEC ); + du.amount += guy.bonus_from_enchantments( du.amount, enchant_vals::mod::ARMOR_ELEC ); break; case DT_HEAT: - du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::ARMOR_HEAT ); + du.amount += guy.bonus_from_enchantments( du.amount, enchant_vals::mod::ARMOR_HEAT ); break; case DT_STAB: - du.amount = guy.calculate_by_enchantment( du.amount, enchant_vals::mod::ARMOR_STAB ); + du.amount += guy.bonus_from_enchantments( du.amount, enchant_vals::mod::ARMOR_STAB ); break; default: return; @@ -9735,8 +9729,12 @@ int Character::run_cost( int base_cost, bool diag ) const movecost += 10 * footwear_factor(); } - movecost = calculate_by_enchantment( movecost, enchant_vals::mod::MOVE_COST ); + movecost += bonus_from_enchantments( movecost, enchant_vals::mod::MOVE_COST ); movecost /= stamina_move_cost_modifier(); + + if( movecost < 20.0 ) { + movecost = 20.0; + } } if( diag ) { diff --git a/src/character.h b/src/character.h index 220aaa8789f4..d14c22037a88 100644 --- a/src/character.h +++ b/src/character.h @@ -698,7 +698,7 @@ class Character : public Creature, public visitable /** Returns true if the player scores a critical hit */ bool scored_crit( float target_dodge, const item &weap ) const; /** Returns cost (in moves) of attacking with given item (no modifiers, like stuck) */ - int attack_speed( const item &weap ) const; + int attack_cost( const item &weap ) const; /** Gets melee accuracy component from weapon+skills */ float get_hit_weapon( const item &weap ) const; @@ -930,9 +930,11 @@ class Character : public Creature, public visitable public: // recalculates enchantment cache by iterating through all held, worn, and wielded items void recalculate_enchantment_cache(); - // gets add and mult value from enchantment cache - double calculate_by_enchantment( double modify, enchant_vals::mod value, - bool round_output = false ) const; + + /** + * Calculate bonus from enchantments for given base value. + */ + double bonus_from_enchantments( double base, enchant_vals::mod value, bool round = false ) const; /** Returns true if the player has any martial arts buffs attached */ bool has_mabuff( const mabuff_id &buff_id ) const; @@ -2256,9 +2258,6 @@ class Character : public Creature, public visitable */ bool is_visible_in_range( const Creature &critter, int range ) const; - // a cache of all active enchantment values. - // is recalculated every turn in Character::recalculate_enchantment_cache - pimpl enchantment_cache; player_activity destination_activity; // A unique ID number, assigned by the game class. Values should never be reused. character_id id; @@ -2293,6 +2292,10 @@ class Character : public Creature, public visitable inventory cached_crafting_inventory; protected: + // a cache of all active enchantment values. + // is recalculated every turn in Character::recalculate_enchantment_cache + pimpl enchantment_cache; + /** Amount of time the player has spent in each overmap tile. */ std::unordered_map overmap_time; diff --git a/src/consumption.cpp b/src/consumption.cpp index 22efa396ed11..370cfecc164d 100644 --- a/src/consumption.cpp +++ b/src/consumption.cpp @@ -18,6 +18,7 @@ #include "game.h" #include "item_contents.h" #include "itype.h" +#include "magic_enchantment.h" #include "map.h" #include "material.h" #include "messages.h" @@ -516,9 +517,14 @@ bool Character::vitamin_set( const vitamin_id &vit, int qty ) float Character::metabolic_rate_base() const { static const std::string hunger_rate_string( "PLAYER_HUNGER_RATE" ); - float hunger_rate = get_option< float >( hunger_rate_string ); static const std::string metabolism_modifier( "metabolism_modifier" ); - return hunger_rate * ( 1.0f + mutation_value( metabolism_modifier ) ); + + float hunger_rate = get_option< float >( hunger_rate_string ); + float mut_bonus = 1.0f + mutation_value( metabolism_modifier ); + float with_mut = hunger_rate * mut_bonus; + float ench_bonus = bonus_from_enchantments( with_mut, enchant_vals::mod::METABOLISM ); + + return std::max( 0.0f, with_mut + ench_bonus ); } // TODO: Make this less chaotic to let NPC retroactive catch up work here diff --git a/src/game_inventory.cpp b/src/game_inventory.cpp index 4a97762396af..2b8569dff3ba 100644 --- a/src/game_inventory.cpp +++ b/src/game_inventory.cpp @@ -1188,7 +1188,7 @@ class weapon_inventory_preset: public inventory_selector_preset append_cell( [ this ]( const item_location & loc ) { if( deals_melee_damage( *loc ) ) { - return string_format( "%d", this->p.attack_speed( *loc ) ); + return string_format( "%d", this->p.attack_cost( *loc ) ); } return std::string(); }, _( "MOVES" ) ); diff --git a/src/handle_action.cpp b/src/handle_action.cpp index 898c27ed4ccf..47fb98af5151 100644 --- a/src/handle_action.cpp +++ b/src/handle_action.cpp @@ -635,7 +635,7 @@ static void smash() } } } - const int move_cost = !u.is_armed() ? 80 : u.weapon.attack_time() * 0.8; + const int move_cost = !u.is_armed() ? 80 : u.weapon.attack_cost() * 0.8; bool didit = false; bool mech_smash = false; int smashskill; diff --git a/src/item.cpp b/src/item.cpp index bd554d21475f..8221e4097ec8 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -1297,7 +1297,7 @@ double item::effective_dps( const player &guy, const monster &mon ) const double crit_chance = guy.crit_chance( 0, 0, *this ); double num_low_hits = std::max( 0.0, num_all_hits - num_high_hits ); - double moves_per_attack = guy.attack_speed( *this ); + double moves_per_attack = guy.attack_cost( *this ); // attacks that miss do no damage but take time double total_moves = ( hit_trials - num_all_hits ) * moves_per_attack; double total_damage = 0.0; @@ -3214,7 +3214,7 @@ void item::combat_info( std::vector &info, const iteminfo_query *parts if( parts->test( iteminfo_parts::BASE_MOVES ) ) { info.push_back( iteminfo( "BASE", _( "Moves per attack: " ), "", - iteminfo::lower_is_better, attack_time() ) ); + iteminfo::lower_is_better, attack_cost() ) ); info.emplace_back( "BASE", _( "Typical damage per second:" ), "" ); const std::map &dps_data = dps( true, false ); std::string sep; @@ -3275,7 +3275,7 @@ void item::combat_info( std::vector &info, const iteminfo_query *parts g->u.roll_all_damage( false, non_crit, true, *this ); damage_instance crit; g->u.roll_all_damage( true, crit, true, *this ); - int attack_cost = g->u.attack_speed( *this ); + int attack_cost = g->u.attack_cost( *this ); insert_separation_line( info ); if( parts->test( iteminfo_parts::DESCRIPTION_MELEEDMG ) ) { info.push_back( iteminfo( "DESCRIPTION", _( "Average melee damage:" ) ) ); @@ -4817,12 +4817,11 @@ int item::lift_strength() const return std::max( mass / 10000, 1 ); } -int item::attack_time() const +int item::attack_cost() const { - int ret = 65 + ( volume() / 62.5_ml + weight() / 60_gram ) / count(); - ret = calculate_by_enchantment_wield( ret, enchant_vals::mod::ITEM_ATTACK_SPEED, - true ); - return ret; + int base = 65 + ( volume() / 62.5_ml + weight() / 60_gram ) / count(); + int bonus = bonus_from_enchantments_wielded( base, enchant_vals::mod::ITEM_ATTACK_COST, true ); + return std::max( 0, base + bonus ); } int item::damage_melee( damage_type dt ) const @@ -6498,42 +6497,42 @@ std::vector item::get_enchantments() const return relic_data->get_enchantments(); } -double item::calculate_by_enchantment( const Character &owner, double modify, - enchant_vals::mod value, bool round_value ) const +double item::bonus_from_enchantments( const Character &owner, double base, + enchant_vals::mod value, bool round ) const { - double add_value = 0.0; - double mult_value = 1.0; + double add = 0.0; + double mul = 0.0; for( const enchantment &ench : get_enchantments() ) { if( ench.is_active( owner, *this ) ) { - add_value += ench.get_value_add( value ); - mult_value += ench.get_value_multiply( value ); + add += ench.get_value_add( value ); + mul += ench.get_value_multiply( value ); } } - modify += add_value; - modify *= mult_value; - if( round_value ) { - modify = std::round( modify ); + // TODO: this part duplicates enchantment::calc_bonus() + double ret = add + base * mul; + if( round ) { + ret = trunc( ret ); } - return modify; + return ret; } -double item::calculate_by_enchantment_wield( double modify, enchant_vals::mod value, - bool round_value ) const +double item::bonus_from_enchantments_wielded( double base, enchant_vals::mod value, + bool round ) const { - double add_value = 0.0; - double mult_value = 1.0; + double add = 0.0; + double mul = 0.0; for( const enchantment &ench : get_enchantments() ) { - if( ench.active_wield() ) { - add_value += ench.get_value_add( value ); - mult_value += ench.get_value_multiply( value ); + if( ench.is_active_when_wielded() ) { + add += ench.get_value_add( value ); + mul += ench.get_value_multiply( value ); } } - modify += add_value; - modify *= mult_value; - if( round_value ) { - modify = std::round( modify ); + // TODO: this part duplicates enchantment::calc_bonus() + double ret = add + base * mul; + if( round ) { + ret = trunc( ret ); } - return modify; + return ret; } bool item::can_contain( const item &it ) const diff --git a/src/item.h b/src/item.h index e5fa7fe0d5b9..2f40ab3810f8 100644 --- a/src/item.h +++ b/src/item.h @@ -554,7 +554,7 @@ class item : public visitable * Base number of moves (@ref Creature::moves) that a single melee attack with this items * takes. The actual time depends heavily on the attacker, see melee.cpp. */ - int attack_time() const; + int attack_cost() const; /** Damage of given type caused when this item is used as melee weapon */ int damage_melee( damage_type dt ) const; @@ -2052,11 +2052,19 @@ class item : public visitable const std::vector> &get_cached_tool_selections() const; std::vector get_enchantments() const; - double calculate_by_enchantment( const Character &owner, double modify, enchant_vals::mod value, - bool round_value = false ) const; - // calculates the enchantment value as if this item were wielded. - double calculate_by_enchantment_wield( double modify, enchant_vals::mod value, - bool round_value = false ) const; + + /** + * Calculate bonus from enchantments that affect this item only. + */ + double bonus_from_enchantments( const Character &owner, double base, + enchant_vals::mod value, bool round = false ) const; + + /** + * Calculate bonus from enchantments that affect this item only, + * assume it's wielded and all enchantments' conditions are satisfied. + */ + double bonus_from_enchantments_wielded( double base, enchant_vals::mod value, + bool round = false ) const; private: bool use_amount_internal( const itype_id &it, int &quantity, std::list &used, diff --git a/src/magic.cpp b/src/magic.cpp index bc4d7cb5e27b..310d728a6f87 100644 --- a/src/magic.cpp +++ b/src/magic.cpp @@ -1408,20 +1408,33 @@ void known_magic::mod_mana( const Character &guy, int add_mana ) int known_magic::max_mana( const Character &guy ) const { - const float int_bonus = ( ( 0.2f + guy.get_int() * 0.1f ) - 1.0f ) * mana_base; - const float unaugmented_mana = std::max( 0.0f, - ( ( mana_base + int_bonus ) * guy.mutation_value( "mana_multiplier" ) ) + - guy.mutation_value( "mana_modifier" ) - units::to_kilojoule( guy.get_power_level() ) ); - return guy.calculate_by_enchantment( unaugmented_mana, enchant_vals::mod::MAX_MANA, true ); + float int_bonus = ( ( 0.2f + guy.get_int() * 0.1f ) - 1.0f ) * mana_base; + float mut_mul = guy.mutation_value( "mana_multiplier" ); + float mut_add = guy.mutation_value( "mana_modifier" ); + int natural_cap = std::max( 0.0f, ( ( mana_base + int_bonus ) * mut_mul ) + mut_add ); + + int bp_penalty = units::to_kilojoule( guy.get_power_level() ); + int ench_bonus = guy.bonus_from_enchantments( natural_cap, enchant_vals::mod::MANA_CAP, true ); + + return std::max( 0, natural_cap - bp_penalty + ench_bonus ); } -void known_magic::update_mana( const Character &guy, float turns ) +double known_magic::mana_regen_rate( const Character &guy ) const { // mana should replenish in 8 hours. - const float full_replenish = to_turns( 8_hours ); - const float ratio = turns / full_replenish; - mod_mana( guy, std::floor( ratio * guy.calculate_by_enchantment( max_mana( guy ) * - guy.mutation_value( "mana_regen_multiplier" ), enchant_vals::mod::REGEN_MANA ) ) ); + double full_replenish = to_turns( 8_hours ); + double capacity = max_mana( guy ); + double mut_mul = guy.mutation_value( "mana_regen_multiplier" ); + double natural_regen = std::max( 0.0, capacity * mut_mul / full_replenish ); + + double ench_bonus = guy.bonus_from_enchantments( natural_regen, enchant_vals::mod::MANA_REGEN ); + + return std::max( 0.0, natural_regen + ench_bonus ); +} + +void known_magic::update_mana( const Character &guy, double turns ) +{ + mod_mana( guy, mana_regen_rate( guy ) * turns ); } std::vector known_magic::spells() const diff --git a/src/magic.h b/src/magic.h index fb99f0bf7cc9..80bb0ada4a2a 100644 --- a/src/magic.h +++ b/src/magic.h @@ -483,7 +483,9 @@ class known_magic int max_mana( const Character &guy ) const; void mod_mana( const Character &guy, int add_mana ); void set_mana( int new_mana ); - void update_mana( const Character &guy, float turns ); + /** Mana regeneration rate (units per turn). */ + double mana_regen_rate( const Character &guy ) const; + void update_mana( const Character &guy, double turns ); // does the Character have enough energy to cast this spell? // not specific to mana bool has_enough_energy( const Character &guy, spell &sp ) const; diff --git a/src/magic_enchantment.cpp b/src/magic_enchantment.cpp index 7d21dad8c324..56b9dbbd7f2e 100644 --- a/src/magic_enchantment.cpp +++ b/src/magic_enchantment.cpp @@ -76,11 +76,10 @@ namespace io case enchant_vals::mod::INTELLIGENCE: return "INTELLIGENCE"; case enchant_vals::mod::SPEED: return "SPEED"; case enchant_vals::mod::ATTACK_COST: return "ATTACK_COST"; - case enchant_vals::mod::ATTACK_SPEED: return "ATTACK_SPEED"; case enchant_vals::mod::MOVE_COST: return "MOVE_COST"; case enchant_vals::mod::METABOLISM: return "METABOLISM"; - case enchant_vals::mod::MAX_MANA: return "MAX_MANA"; - case enchant_vals::mod::REGEN_MANA: return "REGEN_MANA"; + 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"; @@ -131,7 +130,7 @@ namespace io 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_SPEED: return "ITEM_ATTACK_SPEED"; + 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; } @@ -141,6 +140,19 @@ namespace io // *INDENT-ON* } // namespace io +static void migrate_ench_vals_enums( std::string &s ) +{ + if( s == "ITEM_ATTACK_SPEED" ) { + s = "ITEM_ATTACK_COST"; + } else if( s == "ATTACK_SPEED" ) { + s = "ATTACK_COST"; + } else if( s == "MAX_MANA" ) { + s = "MANA_CAP"; + } else if( s == "REGEN_MANA" ) { + s = "MANA_REGEN"; + } +} + namespace { generic_factory spell_factory( "enchantment" ); @@ -169,14 +181,11 @@ bool enchantment::is_active( const Character &guy, const item &parent ) const return false; } - if( active_conditions.first == has::HELD && - active_conditions.second == condition::ALWAYS ) { - return true; + if( active_conditions.first == has::WIELD && !guy.is_wielding( parent ) ) { + return false; } - if( !( active_conditions.first == has::HELD || - ( active_conditions.first == has::WIELD && guy.is_wielding( parent ) ) || - ( active_conditions.first == has::WORN && guy.is_worn( parent ) ) ) ) { + if( active_conditions.first == has::WORN && !guy.is_worn( parent ) ) { return false; } @@ -203,11 +212,6 @@ bool enchantment::is_active( const Character &guy, const bool active ) const return false; } -bool enchantment::active_wield() const -{ - return active_conditions.first == has::HELD || active_conditions.first == has::WIELD; -} - void enchantment::add_activation( const time_duration &freq, const fake_spell &fake ) { intermittent_activation[freq].emplace_back( fake ); @@ -253,15 +257,19 @@ void enchantment::load( const JsonObject &jo, const std::string & ) if( jo.has_array( "values" ) ) { for( const JsonObject value_obj : jo.get_array( "values" ) ) { - const enchant_vals::mod value = io::string_to_enum - ( value_obj.get_string( "value" ) ); + 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 ); + const int add = value_obj.get_int( "add", 0 ); const double mult = value_obj.get_float( "multiply", 0.0 ); if( add != 0 ) { values_add.emplace( value, add ); } if( mult != 0.0 ) { - values_multiply.emplace( value, mult ); + // Limit precision to minimize inconsistencies between platforms / compilers + const double mul = static_cast( std::round( mult * 100'000 ) ) / 100'000.0; + values_multiply.emplace( value, mul ); } } } @@ -414,6 +422,26 @@ double enchantment::get_value_multiply( const enchant_vals::mod value ) const return found->second; } +double enchantment::calc_bonus( enchant_vals::mod value, double base, bool round ) const +{ + bool use_add = true; + switch( value ) { + case enchant_vals::mod::METABOLISM: + case enchant_vals::mod::MANA_REGEN: + use_add = false; + break; + default: + break; + } + double add = use_add ? get_value_add( value ) : 0.0; + double mul = get_value_multiply( value ); + double ret = add + base * mul; + if( round ) { + ret = trunc( ret ); + } + return ret; +} + int enchantment::mult_bonus( enchant_vals::mod value_type, int base_value ) const { return get_value_multiply( value_type ) * base_value; @@ -421,23 +449,16 @@ int enchantment::mult_bonus( enchant_vals::mod value_type, int base_value ) cons void enchantment::activate_passive( Character &guy ) const { - guy.mod_str_bonus( get_value_add( enchant_vals::mod::STRENGTH ) ); - guy.mod_str_bonus( mult_bonus( enchant_vals::mod::STRENGTH, guy.get_str_base() ) ); - - guy.mod_dex_bonus( get_value_add( enchant_vals::mod::DEXTERITY ) ); - guy.mod_dex_bonus( mult_bonus( enchant_vals::mod::DEXTERITY, guy.get_dex_base() ) ); - - guy.mod_per_bonus( get_value_add( enchant_vals::mod::PERCEPTION ) ); - guy.mod_per_bonus( mult_bonus( enchant_vals::mod::PERCEPTION, guy.get_per_base() ) ); - - guy.mod_int_bonus( get_value_add( enchant_vals::mod::INTELLIGENCE ) ); - guy.mod_int_bonus( mult_bonus( enchant_vals::mod::INTELLIGENCE, guy.get_int_base() ) ); - - guy.mod_speed_bonus( get_value_add( enchant_vals::mod::SPEED ) ); - guy.mod_speed_bonus( mult_bonus( enchant_vals::mod::SPEED, guy.get_speed_base() ) ); - - guy.mod_num_dodges_bonus( get_value_add( enchant_vals::mod::BONUS_DODGE ) ); - guy.mod_num_dodges_bonus( mult_bonus( enchant_vals::mod::BONUS_DODGE, guy.get_num_dodges_base() ) ); + guy.mod_str_bonus( calc_bonus( enchant_vals::mod::STRENGTH, guy.get_str_base(), true ) ); + guy.mod_dex_bonus( calc_bonus( enchant_vals::mod::DEXTERITY, guy.get_dex_base(), true ) ); + guy.mod_per_bonus( calc_bonus( enchant_vals::mod::PERCEPTION, guy.get_per_base(), true ) ); + guy.mod_int_bonus( calc_bonus( enchant_vals::mod::INTELLIGENCE, guy.get_int_base(), true ) ); + + guy.mod_num_dodges_bonus( calc_bonus( + enchant_vals::mod::BONUS_DODGE, + guy.get_num_dodges_base(), + true + ) ); if( emitter ) { get_map().emit_field( guy.pos(), *emitter ); diff --git a/src/magic_enchantment.h b/src/magic_enchantment.h index 35b53c36fe78..04672abb37d5 100644 --- a/src/magic_enchantment.h +++ b/src/magic_enchantment.h @@ -29,19 +29,18 @@ enum class mod : int { INTELLIGENCE, SPEED, ATTACK_COST, - ATTACK_SPEED, // affects attack speed of item even if it's not the one you're wielding MOVE_COST, METABOLISM, - MAX_MANA, - REGEN_MANA, + MANA_CAP, + MANA_REGEN, BIONIC_POWER, MAX_STAMINA, REGEN_STAMINA, - MAX_HP, // for all limbs! use with caution + MAX_HP, REGEN_HP, - THIRST, // cost or regen over time - FATIGUE, // cost or regen over time - PAIN, // cost or regen over time + THIRST, + FATIGUE, + PAIN, BONUS_DODGE, BONUS_BLOCK, BONUS_DAMAGE, @@ -72,7 +71,7 @@ enum class mod : int { ITEM_DAMAGE_ELEC, ITEM_DAMAGE_ACID, ITEM_DAMAGE_BIO, - ITEM_DAMAGE_AP, // armor piercing + ITEM_DAMAGE_AP, ITEM_ARMOR_BASH, ITEM_ARMOR_CUT, ITEM_ARMOR_STAB, @@ -85,7 +84,7 @@ enum class mod : int { ITEM_ENCUMBRANCE, ITEM_VOLUME, ITEM_COVERAGE, - ITEM_ATTACK_SPEED, + ITEM_ATTACK_COST, ITEM_WET_PROTECTION, NUM_MOD }; @@ -125,15 +124,24 @@ class enchantment int get_value_add( enchant_vals::mod value ) const; double get_value_multiply( enchant_vals::mod value ) const; + /** + * Calculate bonus provided by this enchantment for given base value. + */ + double calc_bonus( enchant_vals::mod value, double base, bool round = false ) const; + // this enchantment has a valid condition and is in the right location bool is_active( const Character &guy, const item &parent ) const; // @active means the container for the enchantment is active, for comparison to active flag. bool is_active( const Character &guy, bool active ) const; - // this enchantment is active when wielded. - // shows total conditional values, so only use this when Character is not available - bool active_wield() const; + /** + * Whether this enchantment will be active if parent item is wielded. + * Assumes condition is satisfied. + */ + inline bool is_active_when_wielded() const { + return has::WIELD == active_conditions.first || has::HELD == active_conditions.first; + } // modifies character stats, or does other passive effects void activate_passive( Character &guy ) const; diff --git a/src/melee.cpp b/src/melee.cpp index 454a6dd25b5d..b718e9a871b9 100644 --- a/src/melee.cpp +++ b/src/melee.cpp @@ -419,12 +419,12 @@ void Character::melee_attack( Creature &t, bool allow_special, const matec_id &f } item &cur_weapon = allow_unarmed ? used_weapon() : weapon; - if( cur_weapon.attack_time() > attack_speed( cur_weapon ) * 20 ) { + if( cur_weapon.attack_cost() > attack_cost( cur_weapon ) * 20 ) { add_msg( m_bad, _( "This weapon is too unwieldy to attack with!" ) ); return; } - int move_cost = attack_speed( cur_weapon ); + int move_cost = attack_cost( cur_weapon ); if( hit_spread < 0 ) { int stumble_pen = stumble( *this, cur_weapon ); @@ -634,7 +634,7 @@ void player::reach_attack( const tripoint &p ) // Reset last target pos last_target_pos = cata::nullopt; - int move_cost = attack_speed( weapon ); + int move_cost = attack_cost( weapon ); int skill = std::min( 10, get_skill_level( skill_stabbing ) ); int t = 0; std::vector path = line_to( pos(), p, t, 0 ); @@ -2161,9 +2161,9 @@ void player_hit_message( Character *attacker, const std::string &message, attacker->add_msg_player_or_npc( msgtype, msg, msg, t.disp_name() ); } -int Character::attack_speed( const item &weap ) const +int Character::attack_cost( const item &weap ) const { - const int base_move_cost = weap.attack_time() / 2; + const int base_move_cost = weap.attack_cost() / 2; const int melee_skill = has_active_bionic( bionic_id( bio_cqb ) ) ? BIO_CQB_LEVEL : get_skill_level( skill_melee ); /** @EFFECT_MELEE increases melee attack speed */ @@ -2186,7 +2186,8 @@ int Character::attack_speed( const item &weap ) const move_cost += skill_cost; move_cost -= dexbonus; - move_cost = calculate_by_enchantment( move_cost, enchant_vals::mod::ATTACK_SPEED, true ); + move_cost += bonus_from_enchantments( move_cost, enchant_vals::mod::ATTACK_COST, true ); + // Martial arts last. Flat has to be after mult, because comments say so. move_cost *= ma_mult; move_cost += ma_move_cost; @@ -2281,7 +2282,7 @@ void player::disarm( npc &target ) int hitspread = target.deal_melee_attack( this, hit_roll() ); if( hitspread < 0 ) { add_msg( _( "You lunge for the %s, but miss!" ), it.tname() ); - mod_moves( -100 - stumble( *this, weapon ) - attack_speed( weapon ) ); + mod_moves( -100 - stumble( *this, weapon ) - attack_cost( weapon ) ); target.on_attacked( *this ); return; } @@ -2315,7 +2316,7 @@ void player::disarm( npc &target ) } // Make their weapon fall on floor if we've rolled enough. - mod_moves( -100 - attack_speed( weapon ) ); + mod_moves( -100 - attack_cost( weapon ) ); if( my_roll >= their_roll ) { add_msg( _( "You smash %s with all your might forcing their %s to drop down nearby!" ), target.name, it.tname() ); diff --git a/src/npcmove.cpp b/src/npcmove.cpp index 973f3810d9d5..3767a4e321b6 100644 --- a/src/npcmove.cpp +++ b/src/npcmove.cpp @@ -2370,7 +2370,7 @@ void npc::move_to( const tripoint &pt, bool no_bashing, std::set *nomo } } else if( !no_bashing && smash_ability() > 0 && g->m.is_bashable( p ) && g->m.bash_rating( smash_ability(), p ) > 0 ) { - moves -= !is_armed() ? 80 : weapon.attack_time() * 0.8; + moves -= !is_armed() ? 80 : weapon.attack_cost() * 0.8; g->m.bash( p, smash_ability() ); } else { if( attitude == NPCATT_MUG || diff --git a/src/player.cpp b/src/player.cpp index ceb177792d34..922360ff74f9 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -41,6 +41,7 @@ #include "itype.h" #include "lightmap.h" #include "line.h" +#include "magic_enchantment.h" #include "map.h" #include "map_iterator.h" #include "mapdata.h" @@ -546,6 +547,9 @@ void player::recalc_speed_bonus() set_speed_bonus( static_cast( get_speed() * 1.1 ) - get_speed_base() ); } + double ench_bonus = enchantment_cache->calc_bonus( enchant_vals::mod::SPEED, get_speed() ); + set_speed_bonus( get_speed() + ench_bonus - get_speed_base() ); + // Speed cannot be less than 25% of base speed, so minimal speed bonus is -75% base speed. const int min_speed_bonus = static_cast( -0.75 * get_speed_base() ); if( get_speed_bonus() < min_speed_bonus ) { @@ -4399,6 +4403,7 @@ void player::environmental_revert_effect() set_pain( 0 ); set_painkiller( 0 ); set_rad( 0 ); + set_sleep_deprivation( 0 ); recalc_sight_limits(); reset_encumbrance(); diff --git a/src/ranged.cpp b/src/ranged.cpp index 3274385c759e..4b78c7883611 100644 --- a/src/ranged.cpp +++ b/src/ranged.cpp @@ -571,12 +571,12 @@ int player::fire_gun( const tripoint &target, const int max_shots, item &gun ) int throw_cost( const player &c, const item &to_throw ); int throw_cost( const player &c, const item &to_throw ) { - // Very similar to player::attack_speed + // Very similar to player::attack_cost // TODO: Extract into a function? // Differences: // Dex is more (2x) important for throwing speed // At 10 skill, the cost is down to 0.75%, not 0.66% - const int base_move_cost = to_throw.attack_time() / 2; + const int base_move_cost = to_throw.attack_cost() / 2; const int throw_skill = std::min( MAX_SKILL, c.get_skill_level( skill_throw ) ); ///\EFFECT_THROW increases throwing speed const int skill_cost = static_cast( ( base_move_cost * ( 20 - throw_skill ) / 20 ) ); diff --git a/src/relic.cpp b/src/relic.cpp index ddf1750feaad..95dcc032d467 100644 --- a/src/relic.cpp +++ b/src/relic.cpp @@ -94,24 +94,6 @@ int relic::activate( Creature &caster, const tripoint &target ) const return charges_per_activation; } -int relic::modify_value( const enchant_vals::mod value_type, const int value ) const -{ - int add_modifier = 0; - double multiply_modifier = 0.0; - for( const enchantment &ench : passive_effects ) { - add_modifier += ench.get_value_add( value_type ); - multiply_modifier += ench.get_value_multiply( value_type ); - } - multiply_modifier = std::max( multiply_modifier + 1.0, 0.0 ); - int modified_value; - if( multiply_modifier < 1.0 ) { - modified_value = std::floor( multiply_modifier * value ); - } else { - modified_value = std::ceil( multiply_modifier * value ); - } - return modified_value + add_modifier; -} - bool relic::operator==( const relic &rhs ) const { return charges_per_activation == rhs.charges_per_activation && diff --git a/src/relic.h b/src/relic.h index 3de78fa17801..7f651a5c25c6 100644 --- a/src/relic.h +++ b/src/relic.h @@ -43,8 +43,6 @@ class relic void add_active_effect( const fake_spell &sp ); std::vector get_enchantments() const; - - int modify_value( enchant_vals::mod value_type, int value ) const; }; #endif // CATA_SRC_RELIC_H diff --git a/tests/enchantment_test.cpp b/tests/enchantment_test.cpp new file mode 100644 index 000000000000..49b3056db073 --- /dev/null +++ b/tests/enchantment_test.cpp @@ -0,0 +1,556 @@ +#include "catch/catch.hpp" + +#include "magic.h" +#include "magic_enchantment.h" +#include "map.h" +#include "map_helpers.h" +#include "item.h" +#include "options.h" +#include "player.h" +#include "player_helpers.h" + +static trait_id trait_CARNIVORE( "CARNIVORE" ); +static efftype_id effect_debug_clairvoyance( "debug_clairvoyance" ); + +static void advance_turn( Character &guy ) +{ + guy.process_turn(); + calendar::turn += 1_turns; +} + +static void give_item( Character &guy, const std::string &item_id ) +{ + guy.i_add( item( item_id ) ); + guy.recalculate_enchantment_cache(); +} + +static void clear_items( Character &guy ) +{ + guy.inv.clear(); + guy.recalculate_enchantment_cache(); +} + +TEST_CASE( "Enchantments grant mutations", "[magic][enchantment][trait][mutation]" ) +{ + clear_map(); + 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"; + + WHEN( "Character doesn't have trait" ) { + REQUIRE( !guy.has_trait( trait_CARNIVORE ) ); + AND_WHEN( "Character receives relic" ) { + give_item( guy, s_relic ); + THEN( "Character gains trait" ) { + CHECK( guy.has_trait( trait_CARNIVORE ) ); + } + AND_WHEN( "Turn passes" ) { + advance_turn( guy ); + THEN( "Character still has trait" ) { + CHECK( guy.has_trait( trait_CARNIVORE ) ); + } + AND_WHEN( "Character loses relic" ) { + clear_items( guy ); + THEN( "Character loses trait" ) { + CHECK_FALSE( guy.has_trait( trait_CARNIVORE ) ); + } + AND_WHEN( "Turn passes" ) { + advance_turn( guy ); + THEN( "Character still has no trait" ) { + CHECK_FALSE( guy.has_trait( trait_CARNIVORE ) ); + } + } + } + } + } + } + + WHEN( "Character has trait" ) { + guy.set_mutation( trait_CARNIVORE ); + REQUIRE( guy.has_trait( trait_CARNIVORE ) ); + AND_WHEN( "Character receives relic" ) { + give_item( guy, s_relic ); + THEN( "Nothing changes" ) { + CHECK( guy.has_trait( trait_CARNIVORE ) ); + } + AND_WHEN( "Turn passes" ) { + advance_turn( guy ); + THEN( "Nothing changes" ) { + CHECK( guy.has_trait( trait_CARNIVORE ) ); + } + AND_WHEN( "Character loses relic" ) { + clear_items( guy ); + THEN( "Nothing changes" ) { + CHECK( guy.has_trait( trait_CARNIVORE ) ); + } + AND_WHEN( "Turn passes" ) { + advance_turn( guy ); + THEN( "Nothing changes" ) { + CHECK( guy.has_trait( trait_CARNIVORE ) ); + } + } + } + } + } + } +} + +TEST_CASE( "Enchantments apply effects", "[magic][enchantment][effect]" ) +{ + clear_map(); + Character &guy = get_player_character(); + clear_character( *guy.as_player(), true ); + + guy.recalculate_enchantment_cache(); + advance_turn( guy ); + + std::string s_relic = "architect_cube"; + + // TODO: multiple enchantments apply same effect of + // same or different intensity + // TODO: enchantments apply effect while char already has effect of + // same, stronger or weaker intensity + + WHEN( "Character doesn't have effect" ) { + REQUIRE( !guy.has_effect( effect_debug_clairvoyance ) ); + AND_WHEN( "Character receives relic" ) { + give_item( guy, s_relic ); + THEN( "Character still doesn't have effect" ) { + CHECK_FALSE( guy.has_effect( effect_debug_clairvoyance ) ); + } + AND_WHEN( "Turn passes" ) { + advance_turn( guy ); + THEN( "Character receives effect" ) { + CHECK( guy.has_effect( effect_debug_clairvoyance ) ); + } + AND_WHEN( "Character loses relic" ) { + clear_items( guy ); + THEN( "Character still has effect" ) { + CHECK( guy.has_effect( effect_debug_clairvoyance ) ); + } + AND_WHEN( "Turn passes" ) { + advance_turn( guy ); + + // FIXME: effects should go away after 1 turn! + CHECK( guy.has_effect( effect_debug_clairvoyance ) ); + advance_turn( guy ); + + THEN( "Character loses effect" ) { + CHECK_FALSE( guy.has_effect( effect_debug_clairvoyance ) ); + } + } + } + } + } + } +} + +static void tests_stats( Character &guy, int s_base, int d_base, int p_base, int i_base, int s_exp, + int d_exp, int p_exp, int i_exp ) +{ + guy.str_max = s_base; + guy.dex_max = d_base; + 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 ) { + REQUIRE( guy.get_str_base() == s_base ); + REQUIRE( guy.get_dex_base() == d_base ); + REQUIRE( guy.get_per_base() == p_base ); + REQUIRE( guy.get_int_base() == i_base ); + + CHECK( guy.get_str() == s ); + CHECK( guy.get_dex() == d ); + CHECK( guy.get_per() == p ); + CHECK( guy.get_int() == i ); + }; + auto check_stats_base = [&]() { + check_stats( s_base, d_base, p_base, i_base ); + }; + + check_stats_base(); + + std::string s_relic = "test_relic_mods_stats"; + + WHEN( "Character receives relic" ) { + give_item( guy, s_relic ); + THEN( "Stats remain unchanged" ) { + check_stats_base(); + } + AND_WHEN( "Turn passes" ) { + advance_turn( guy ); + THEN( "Stats are modified and don't overflow" ) { + check_stats( s_exp, d_exp, p_exp, i_exp ); + } + AND_WHEN( "Character loses relic" ) { + clear_items( guy ); + THEN( "Stats remain unchanged" ) { + check_stats( s_exp, d_exp, p_exp, i_exp ); + } + AND_WHEN( "Turn passes" ) { + advance_turn( guy ); + THEN( "Stats return to normal" ) { + check_stats_base(); + } + } + } + } + } +} + +TEST_CASE( "Enchantments modify stats", "[magic][enchantment][character]" ) +{ + clear_map(); + Character &guy = get_player_character(); + clear_character( *guy.as_player(), true ); + + SECTION( "base stats 8" ) { + tests_stats( guy, 8, 8, 8, 8, 20, 6, 5, 0 ); + } + SECTION( "base stats 12" ) { + tests_stats( guy, 12, 12, 12, 12, 28, 10, 7, 1 ); + } + SECTION( "base stats 4" ) { + tests_stats( guy, 4, 4, 4, 4, 12, 2, 3, 0 ); + } +} + +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 ); + + guy.set_moves( 0 ); + advance_turn( guy ); + + std::string s_relic = "test_relic_mods_speed"; + + auto check_speed = [&]( int speed, int moves ) { + REQUIRE( guy.get_speed_base() == sp_base ); + REQUIRE( guy.get_speed() == speed ); + REQUIRE( guy.get_moves() == moves ); + }; + auto check_speed_is_base = [&] { + check_speed( sp_base, sp_base ); + }; + + WHEN( "Character has no relics" ) { + THEN( "Speed and moves gain equel base" ) { + check_speed_is_base(); + } + } + WHEN( "Character receives relic" ) { + give_item( guy, s_relic ); + THEN( "Nothing changes" ) { + check_speed_is_base(); + } + AND_WHEN( "Turn passes" ) { + guy.set_moves( 0 ); + advance_turn( guy ); + THEN( "Speed changes, moves gain changes" ) { + check_speed( sp_exp, sp_exp ); + } + AND_WHEN( "Character loses relic" ) { + clear_items( guy ); + THEN( "Nothing changes" ) { + check_speed( sp_exp, sp_exp ); + } + AND_WHEN( "Turn passes" ) { + guy.set_moves( 0 ); + advance_turn( guy ); + THEN( "Speed and moves gain return to normal" ) { + check_speed_is_base(); + } + } + } + } + } + WHEN( "Character receives 10 relics" ) { + for( int i = 0; i < 10; i++ ) { + give_item( guy, s_relic ); + } + THEN( "Nothing changes" ) { + check_speed_is_base(); + } + AND_WHEN( "Turn passes" ) { + guy.set_moves( 0 ); + advance_turn( guy ); + THEN( "Speed and moves gain do not fall below 25% of base" ) { + check_speed( sp_base / 4, sp_base / 4 ); + } + } + } +} + +TEST_CASE( "Enchantments modify speed", "[magic][enchantment][speed]" ) +{ + clear_map(); + Character &guy = get_player_character(); + clear_character( *guy.as_player(), true ); + + SECTION( "base = 100" ) { + tests_speed( guy, 100, 75 ); + } + SECTION( "base = 80" ) { + tests_speed( guy, 80, 65 ); + } + SECTION( "base = 120" ) { + tests_speed( guy, 120, 85 ); + } +} + +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 ); + REQUIRE( guy.attack_cost( weap ) == guy_atk_cost ); + + std::string s_relic = "test_relic_mods_atk_cost"; + + WHEN( "Character receives relic" ) { + give_item( guy, s_relic ); + THEN( "Attack cost changes" ) { + CHECK( guy.attack_cost( weap ) == exp_guy_atk_cost ); + } + AND_WHEN( "Character loses relic" ) { + clear_items( guy ); + THEN( "Attack cost returns to normal" ) { + CHECK( guy.attack_cost( weap ) == guy_atk_cost ); + } + } + } + WHEN( "Character receives 10 relics" ) { + for( int i = 0; i < 10; i++ ) { + give_item( guy, s_relic ); + } + THEN( "Attack cost does not drop below 25" ) { + CHECK( guy.attack_cost( weap ) == 25 ); + } + } +} + +TEST_CASE( "Enchantments modify attack cost", "[magic][enchantment][melee]" ) +{ + clear_map(); + Character &guy = get_player_character(); + clear_character( *guy.as_player(), true ); + + SECTION( "normal sword" ) { + tests_attack_cost( guy, item( "test_normal_sword" ), 101, 96, 77 ); + } + SECTION( "normal sword + ITEM_ATTACK_COST" ) { + tests_attack_cost( guy, item( "test_relic_sword" ), 86, 82, 66 ); + } +} + +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"; + + REQUIRE( guy.run_cost( tile_move_cost ) == move_cost ); + + WHEN( "Character receives relic" ) { + give_item( guy, s_relic ); + THEN( "Move cost changes" ) { + CHECK( guy.run_cost( tile_move_cost ) == exp_move_cost ); + } + AND_WHEN( "Character loses relic" ) { + clear_items( guy ); + THEN( "Move cost goes back to normal" ) { + CHECK( guy.run_cost( tile_move_cost ) == move_cost ); + } + } + } + WHEN( "Character receives 15 relics" ) { + for( int i = 0; i < 15; i++ ) { + give_item( guy, s_relic ); + } + THEN( "Move cost does not drop below 20" ) { + CHECK( guy.run_cost( tile_move_cost ) == 20 ); + } + } +} + +TEST_CASE( "Enchantments modify move cost", "[magic][enchantment][move]" ) +{ + clear_map(); + Character &guy = get_player_character(); + clear_character( *guy.as_player(), true ); + + SECTION( "Naked character" ) { + SECTION( "tile move cost = 100" ) { + tests_move_cost( guy, 100, 116, 104 ); + } + SECTION( "tile move cost = 120" ) { + tests_move_cost( guy, 120, 136, 122 ); + } + } + SECTION( "Naked character with PADDED_FEET" ) { + trait_id tr( "PADDED_FEET" ); + guy.set_mutation( tr ); + REQUIRE( guy.has_trait( tr ) ); + + SECTION( "tile move cost = 100" ) { + tests_move_cost( guy, 100, 90, 81 ); + } + SECTION( "tile move cost = 120" ) { + tests_move_cost( guy, 120, 108, 97 ); + } + } +} + +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"; + + REQUIRE( guy.metabolic_rate_base() == Approx( norm ) ); + + WHEN( "Character receives relic" ) { + give_item( guy, s_relic ); + THEN( "Metabolic rate changes" ) { + CHECK( guy.metabolic_rate_base() == Approx( exp ) ); + AND_WHEN( "Character loses relic" ) { + clear_items( guy ); + THEN( "Metabolic rate goes back to normal" ) { + CHECK( guy.metabolic_rate_base() == Approx( norm ) ); + } + } + } + } + WHEN( "Character receives 15 relics" ) { + for( int i = 0; i < 15; i++ ) { + give_item( guy, s_relic ); + } + THEN( "Metabolic rate does not go below 0" ) { + CHECK( guy.metabolic_rate_base() == Approx( 0.0f ) ); + } + } +} + +TEST_CASE( "Enchantments modify metabolic rate", "[magic][enchantment][metabolism]" ) +{ + clear_map(); + 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" ); + REQUIRE( guy.metabolic_rate_base() == normal_mr ); + REQUIRE( normal_mr == 1.0f ); + + SECTION( "Clean character" ) { + tests_metabolic_rate( guy, 1.0f, 0.9f ); + } + SECTION( "Character with HUNGER trait" ) { + trait_id tr( "HUNGER" ); + guy.set_mutation( tr ); + REQUIRE( guy.has_trait( tr ) ); + + tests_metabolic_rate( guy, 1.5f, 1.35f ); + } +} + +struct mana_test_case { + int idx; + int intellect; + units::energy power_level; + int norm_cap; + int exp_cap; + double norm_regen_amt_8h; + double exp_regen_amt_8h; +}; + +static const std::vector mana_test_data = {{ + {0, 8, 0_kJ, 1000, 800, 1000.0, 560.0}, + {1, 12, 0_kJ, 1400, 1080, 1400.0, 686.0}, + {2, 8, 250_kJ, 750, 450, 750.0, 385.0}, + {3, 12, 250_kJ, 1150, 830, 1150.0, 581.0}, + {4, 8, 1250_kJ, 0, 0, 0.0, 0.0}, + {5, 16, 1250_kJ, 550, 110, 550.0, 77.0}, + } +}; + +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 ); + REQUIRE( guy.get_max_power_level() == 2000_kJ ); + + guy.set_power_level( t.power_level ); + REQUIRE( guy.get_power_level() == t.power_level ); + + guy.int_max = t.intellect; + guy.int_cur = guy.int_max; + REQUIRE( guy.get_int() == t.intellect ); + + REQUIRE( guy.magic->max_mana( guy ) == t.norm_cap ); + REQUIRE( guy.magic->mana_regen_rate( guy ) == Approx( norm_regen_rate ) ); + + const std::string s_relic = "test_relic_mods_manapool"; + + WHEN( "Character receives relic" ) { + give_item( guy, s_relic ); + THEN( "Mana pool capacity and regen rate change" ) { + CHECK( guy.magic->max_mana( guy ) == t.exp_cap ); + CHECK( guy.magic->mana_regen_rate( guy ) == Approx( exp_regen_rate ) ); + AND_WHEN( "Character loses relic" ) { + clear_items( guy ); + THEN( "Mana pool capacity and regen rate go back to normal" ) { + REQUIRE( guy.magic->max_mana( guy ) == t.norm_cap ); + REQUIRE( guy.magic->mana_regen_rate( guy ) == Approx( norm_regen_rate ) ); + } + } + } + } + WHEN( "Character receives 10 relics" ) { + for( int i = 0; i < 10; i++ ) { + give_item( guy, s_relic ); + } + THEN( "Mana pool capacity and regen rate don't drop below 0" ) { + REQUIRE( guy.magic->max_mana( guy ) == 0 ); + REQUIRE( guy.magic->mana_regen_rate( guy ) == Approx( 0.0 ) ); + } + } +} + +static void tests_mana_pool_section( const mana_test_case &t ) +{ + CAPTURE( t.idx ); + + clear_map(); + Character &guy = get_player_character(); + clear_character( *guy.as_player(), true ); + + tests_mana_pool( guy, t ); +} + +TEST_CASE( "Mana pool", "[magic][enchantment][mana][bionic]" ) +{ + for( const mana_test_case &it : mana_test_data ) { + tests_mana_pool_section( it ); + } +} diff --git a/tests/player_helpers.cpp b/tests/player_helpers.cpp index a41a2a139a1f..c1a962319eea 100644 --- a/tests/player_helpers.cpp +++ b/tests/player_helpers.cpp @@ -65,6 +65,10 @@ void clear_character( player &dummy, bool debug_storage ) dummy.set_mutation( trait_id( "DEBUG_STORAGE" ) ); } + dummy.clear_bionics(); + dummy.set_power_level( 0_J ); + dummy.set_max_power_level( 0_J ); + // Clear stomach and then eat a nutritious meal to normalize stomach // contents (needs to happen before clear_morale). dummy.stomach.empty(); @@ -77,7 +81,6 @@ void clear_character( player &dummy, bool debug_storage ) dummy.empty_skills(); dummy.clear_morale(); - dummy.clear_bionics(); dummy.activity.set_to_null(); dummy.reset_chargen_attributes(); dummy.set_pain( 0 ); diff --git a/tests/speed_test.cpp b/tests/speed_test.cpp new file mode 100644 index 000000000000..870fc7dc477c --- /dev/null +++ b/tests/speed_test.cpp @@ -0,0 +1,125 @@ +#include "catch/catch.hpp" + +#include "avatar.h" +#include "player_helpers.h" +#include "map.h" +#include "map_helpers.h" + +static void advance_turn( Character &guy ) +{ + guy.process_turn(); + calendar::turn += 1_turns; +} + +static player &prepare_player() +{ + clear_map(); + player &guy = *get_player_character().as_player(); + clear_character( *guy.as_player(), true ); + guy.set_moves( 0 ); + + advance_turn( guy ); + + REQUIRE( guy.get_speed_base() == 100 ); + REQUIRE( guy.get_speed() == 100 ); + REQUIRE( guy.get_moves() == 100 ); + + guy.set_moves( 0 ); + + return guy; +} + +TEST_CASE( "Character regains moves each turn", "[speed]" ) +{ + player &guy = prepare_player(); + + advance_turn( guy ); + + CHECK( guy.get_moves() == 100 ); +} + +static void pain_penalty_test( player &guy, int pain, int speed_exp ) +{ + int penalty = 100 - speed_exp; + + guy.set_pain( pain ); + REQUIRE( guy.get_pain() == pain ); + guy.set_painkiller( 0 ); + REQUIRE( guy.get_painkiller() == 0 ); + REQUIRE( guy.get_perceived_pain() == pain ); + REQUIRE( guy.get_pain_penalty().speed == penalty ); + + advance_turn( guy ); + + CHECK( guy.get_speed_bonus() == -penalty ); + CHECK( guy.get_speed() == speed_exp ); + CHECK( guy.get_moves() == speed_exp ); +} + +TEST_CASE( "Character is slowed down by pain", "[speed][pain]" ) +{ + player &guy = prepare_player(); + + WHEN( "10 pain" ) { + pain_penalty_test( guy, 10, 95 ); + } + WHEN( "100 pain" ) { + pain_penalty_test( guy, 100, 75 ); + } + WHEN( "300 pain" ) { + pain_penalty_test( guy, 300, 70 ); + } +} + +static void carry_weight_test( Character &guy, int load_kg, int speed_exp ) +{ + item item_1kg( "test_1kg_cube" ); + REQUIRE( item_1kg.weight() == 1_kilogram ); + REQUIRE( item_1kg.volume() == 10_ml ); + + CAPTURE( load_kg, speed_exp ); + WHEN( "Character carries specified weight" ) { + for( int i = 0; i < load_kg; i++ ) { + guy.inv.add_item( item_1kg ); + } + THEN( "No effect on speed" ) { + CHECK( guy.get_speed() == 100 ); + AND_WHEN( "Turn passes" ) { + advance_turn( guy ); + REQUIRE( guy.weight_carried() == 1_kilogram * load_kg + 633_gram ); + REQUIRE( guy.weight_capacity() == 45_kilogram ); + THEN( "Speed matches expected value" ) { + CHECK( guy.get_speed() == speed_exp ); + } + } + } + } +} + +TEST_CASE( "Character is slowed down while overburdened", "[speed]" ) +{ + player &guy = prepare_player(); + + item backpack( "test_backpack" ); + REQUIRE( backpack.get_storage() == 15_liter ); + REQUIRE( backpack.weight() == 633_gram ); + + guy.clear_mutations(); + guy.weapon = backpack; + guy.wear( guy.weapon, false ); + REQUIRE( guy.weight_capacity() == 45_kilogram ); + REQUIRE( guy.volume_capacity() == 15_liter ); + + SECTION( "Carry weight under carry capacity" ) { + // 94%, no penalty + carry_weight_test( guy, 42, 100 ); + } + SECTION( "Carry weight over carry capacity" ) { + // 148% gives -12 speed (25 * 0.48) + carry_weight_test( guy, 66, 88 ); + } + SECTION( "Carry weight significantly over carry capacity" ) { + // 228% gives -32 speed (25 * 1.28) + carry_weight_test( guy, 102, 68 ); + } +}