diff --git a/data/mods/TEST_DATA/effects.json b/data/mods/TEST_DATA/effects.json index b6fd71d706341..0069a87d459bf 100644 --- a/data/mods/TEST_DATA/effects.json +++ b/data/mods/TEST_DATA/effects.json @@ -10,6 +10,7 @@ "rating": "good", "max_duration": "1 h", "max_intensity": 10, + "int_decay_tick": 2, "base_mods": { "str_mod": [ 1 ], "dex_mod": [ 2 ], @@ -76,6 +77,16 @@ "max_intensity": 3, "chance_kill": [ [ 0, 1 ], [ 1, 10 ], [ 1, 1 ] ] }, + { + "type": "effect_type", + "id": "test_int_remove", + "name": [ "Protected from removal" ], + "desc": [ "Don't worry, you can't run out of steam!" ], + "int_decay_remove": false, + "max_intensity": 4, + "int_decay_step": -2, + "int_decay_tick": 1 + }, { "type": "effect_type", "id": "max_effected", diff --git a/doc/EFFECTS_JSON.md b/doc/EFFECTS_JSON.md index ce56042a2d6c1..0860f8e847f29 100644 --- a/doc/EFFECTS_JSON.md +++ b/doc/EFFECTS_JSON.md @@ -217,8 +217,8 @@ in "removes_effects" are automatically added to "blocks_effects", no need for ma ### Effect limiters ```C++ - "max_duration": 100, - "dur_add_perc": 150 - Defaults to 100% + "max_duration": 100, - Time duration string, defaults to 365 days + "dur_add_perc": 150 - Defaults to 100(%) ``` These are utilized when adding to currently existing effects. "max_duration" limits the overall duration of the effect. "dur_add_perc" is the percentage value of the normal duration for adding to an existing. An example: @@ -230,11 +230,12 @@ future applications decreasing the overall time left. ### Intensities Intensities are used to control effect effects, names, and descriptions. They are defined with: -```C++ - "int_add_val": 2 - Defaults to 0! This means future applications will not increase intensity unless changed! - and/or - "int_decay_step": -2, - Defaults to -1 - "int_decay_tick": 10 +```JSON + "int_add_val": 2 - Int, defaults to 0 meaning future applications will not increase intensity + + "int_decay_step": -2, - Int, default -1, intensity levels removed every decay tick + "int_decay_tick": 10 - Int, seconds between intensity decay (no decay at the default of 0) + "int_decay_remove": true - Bool, default true, removes the intensity if decay would decrease it to zero or "int_dur_factor": 700 ``` @@ -245,7 +246,8 @@ Because "int_add_val" = 2, the second addition will change the effect intensity NOTE: You must have at least one of the 3 intensity data sets for intensity to do anything! "int_decay_step" and "int_decay_tick" require one another to do anything. If both exist then the game will automatically -increment the current effect intensities by "int_decay_step" every "int_decay_tick" ticks, capping the result at [1, "max_intensity"]. +increment the current effect intensities by "int_decay_step" every "int_decay_tick" ticks, capping the result at [0, "max_intensity"] +and removing effects if the intensity reaches zero and `int_decay_remove` is true. This can be used to make effects automatically increase or decrease in intensity over time. "int_dur_factor" overrides the other three intensities fields, and forces the intensity to be a number defined as diff --git a/src/creature.cpp b/src/creature.cpp index b84975405034b..0e06f7a8c991e 100644 --- a/src/creature.cpp +++ b/src/creature.cpp @@ -1233,7 +1233,7 @@ void Creature::add_effect( const effect_source &source, const efftype_id &eff_id // If we do, mod the duration, factoring in the mod value e.mod_duration( dur * e.get_dur_add_perc() / 100 ); // Limit to max duration - if( e.get_max_duration() > 0_turns && e.get_duration() > e.get_max_duration() ) { + if( e.get_duration() > e.get_max_duration() ) { e.set_duration( e.get_max_duration() ); } // Adding a permanent effect makes it permanent @@ -1282,7 +1282,7 @@ void Creature::add_effect( const effect_source &source, const efftype_id &eff_id // Now we can make the new effect for application effect e( effect_source( source ), &type, dur, bp.id(), permanent, intensity, calendar::turn ); // Bound to max duration - if( e.get_max_duration() > 0_turns && e.get_duration() > e.get_max_duration() ) { + if( e.get_duration() > e.get_max_duration() ) { e.set_duration( e.get_max_duration() ); } diff --git a/src/effect.cpp b/src/effect.cpp index 08e47fd973e31..358abe16ebcc8 100644 --- a/src/effect.cpp +++ b/src/effect.cpp @@ -818,12 +818,15 @@ std::string effect::disp_short_desc( bool reduced ) const void effect::decay( std::vector &rem_ids, std::vector &rem_bps, const time_point &time, const bool player ) { - // Decay intensity if supposed to do so - // TODO: Remove effects that would decay to 0 intensity? - if( intensity > 1 && eff_type->int_decay_tick != 0 && + // Decay intensity if supposed to do so, removing effects at zero intensity + if( intensity > 0 && eff_type->int_decay_tick != 0 && to_turn( time ) % eff_type->int_decay_tick == 0 && get_max_duration() > get_duration() ) { set_intensity( intensity + eff_type->int_decay_step, player ); + if( intensity <= 0 ) { + rem_ids.push_back( get_id() ); + rem_bps.push_back( bp.id() ); + } } // Add to removal list if duration is <= 0 @@ -852,8 +855,8 @@ time_duration effect::get_max_duration() const void effect::set_duration( const time_duration &dur, bool alert ) { duration = dur; - // Cap to max_duration if it exists - if( eff_type->max_duration > 0_turns && duration > eff_type->max_duration ) { + // Cap to max_duration + if( duration > eff_type->max_duration ) { duration = eff_type->max_duration; } @@ -989,17 +992,23 @@ int effect::set_intensity( int val, bool alert ) intensity = 1; } - val = std::max( std::min( val, eff_type->max_intensity ), 1 ); + val = std::max( std::min( val, eff_type->max_intensity ), 0 ); if( val == intensity ) { // Nothing to change return intensity; } - if( alert && val < intensity && val - 1 < static_cast( eff_type->decay_msgs.size() ) ) { + // Filter out intensity falling to zero (the effect will be removed later) + if( alert && val < intensity && val != 0 && + val - 1 < static_cast( eff_type->decay_msgs.size() ) ) { add_msg( eff_type->decay_msgs[ val - 1 ].second, eff_type->decay_msgs[ val - 1 ].first.translated() ); } + if( val == 0 && !eff_type->int_decay_remove ) { + val = 1; + } + intensity = val; return intensity; @@ -1333,6 +1342,21 @@ int effect::get_int_add_val() const return eff_type->int_add_val; } +int effect::get_int_decay_step() const +{ + return eff_type->int_decay_step; +} + +int effect::get_int_decay_tick() const +{ + return eff_type->int_decay_tick; +} + +bool effect::get_int_decay_remove() const +{ + return eff_type->int_decay_remove; +} + const std::vector> &effect::get_miss_msgs() const { return eff_type->miss_msgs; @@ -1449,13 +1473,7 @@ void load_effect_type( const JsonObject &jo ) for( auto &&f : jo.get_string_array( "blocks_effects" ) ) { // *NOPAD* new_etype.blocks_effects.emplace_back( f ); } - - if( jo.has_string( "max_duration" ) ) { - new_etype.max_duration = read_from_json_string( jo.get_member( "max_duration" ), - time_duration::units ); - } else { - new_etype.max_duration = time_duration::from_turns( jo.get_int( "max_duration", 0 ) ); - } + optional( jo, false, "max_duration", new_etype.max_duration, 365_days ); if( jo.has_string( "int_dur_factor" ) ) { new_etype.int_dur_factor = read_from_json_string( jo.get_member( "int_dur_factor" ), @@ -1476,6 +1494,7 @@ void load_effect_type( const JsonObject &jo ) new_etype.int_add_val = jo.get_int( "int_add_val", 0 ); new_etype.int_decay_step = jo.get_int( "int_decay_step", -1 ); new_etype.int_decay_tick = jo.get_int( "int_decay_tick", 0 ); + optional( jo, false, "int_decay_remove", new_etype.int_decay_remove, true ); new_etype.load_miss_msgs( jo, "miss_messages" ); new_etype.load_decay_msgs( jo, "decay_messages" ); diff --git a/src/effect.h b/src/effect.h index 1fe950606f7e1..2bd714b4cae68 100644 --- a/src/effect.h +++ b/src/effect.h @@ -131,7 +131,7 @@ class effect_type protected: int max_intensity = 0; int max_effective_intensity = 0; - time_duration max_duration = 0_turns; + time_duration max_duration = 365_days; int dur_add_perc = 0; int int_add_val = 0; @@ -139,6 +139,7 @@ class effect_type int int_decay_step = 0; int int_decay_tick = 0 ; time_duration int_dur_factor = 0_turns; + bool int_decay_remove = true; std::set flags; @@ -244,11 +245,11 @@ class effect time_duration get_duration() const; /** Returns the maximum duration of an effect. */ time_duration get_max_duration() const; - /** Sets the duration, capping at max_duration if it exists. */ + /** Sets the duration, capping at max duration. */ void set_duration( const time_duration &dur, bool alert = false ); - /** Mods the duration, capping at max_duration if it exists. */ + /** Mods the duration, capping at max_duration. */ void mod_duration( const time_duration &dur, bool alert = false ); - /** Multiplies the duration, capping at max_duration if it exists. */ + /** Multiplies the duration, capping at max_duration. */ void mult_duration( double dur, bool alert = false ); std::vector vit_effects( bool reduced ) const; @@ -338,6 +339,12 @@ class effect time_duration get_int_dur_factor() const; /** Returns the amount an already existing effect intensity is modified by further applications of the same effect. */ int get_int_add_val() const; + /** Returns the step of intensity decay */ + int get_int_decay_step() const; + /** Returns the number of ticks between intensity changes */ + int get_int_decay_tick() const; + /** Returns if the effect is not protected from intensity decay-based removal */ + bool get_int_decay_remove() const; /** Returns a vector of the miss message messages and chances for use in add_miss_reason() while the effect is in effect. */ const std::vector> &get_miss_msgs() const; @@ -348,6 +355,7 @@ class effect /** Returns if the effect is supposed to be handed in Creature::movement */ bool impairs_movement() const; + /** Returns the effect's matching effect_type id. */ const efftype_id &get_id() const { return eff_type->id; diff --git a/src/martialarts.cpp b/src/martialarts.cpp index e216d871593e1..d777194c7f512 100644 --- a/src/martialarts.cpp +++ b/src/martialarts.cpp @@ -392,6 +392,7 @@ class ma_buff_effect_type : public effect_type int_decay_step = -1; int_decay_tick = 1; int_dur_factor = 0_turns; + int_decay_remove = false; name.push_back( buff.name ); desc.push_back( buff.description ); rating = e_good; diff --git a/tests/effect_test.cpp b/tests/effect_test.cpp index 7b692eafc458f..8eabc17412c28 100644 --- a/tests/effect_test.cpp +++ b/tests/effect_test.cpp @@ -148,11 +148,11 @@ TEST_CASE( "effect intensity", "[effect][intensity]" ) REQUIRE( eff_debugged.get_intensity() == 1 ); REQUIRE( eff_debugged.get_max_intensity() == 10 ); - SECTION( "intensity cannot be set less than 1" ) { + SECTION( "intensity cannot be set less than 0" ) { eff_debugged.set_intensity( 0 ); - CHECK( eff_debugged.get_intensity() == 1 ); + CHECK( eff_debugged.get_intensity() == 0 ); eff_debugged.set_intensity( -1 ); - CHECK( eff_debugged.get_intensity() == 1 ); + CHECK( eff_debugged.get_intensity() == 0 ); } SECTION( "intensity cannot be set greater than maximum" ) { @@ -166,6 +166,24 @@ TEST_CASE( "effect intensity", "[effect][intensity]" ) } } +TEST_CASE( "effect intensity removal", "[effect][intensity]" ) +{ + + const efftype_id eff_id( "test_int_remove" ); + effect eff_test_int_remove( effect_source::empty(), &eff_id.obj(), 3_turns, + bodypart_str_id( "bp_null" ), + false, 1, calendar::turn ); + + REQUIRE( eff_test_int_remove.get_intensity() == 1 ); + REQUIRE( eff_test_int_remove.get_int_decay_remove() == false ); + + SECTION( "effects protected from intensity-based removal can't be set to less than 1 intensity" ) { + eff_test_int_remove.set_intensity( 0 ); + CHECK( eff_test_int_remove.get_intensity() == 1 ); + } + +} + TEST_CASE( "max effective intensity", "[effect][max][intensity]" ) { const efftype_id eff_id( "max_effected" ); @@ -210,13 +228,14 @@ TEST_CASE( "max effective intensity", "[effect][max][intensity]" ) TEST_CASE( "effect decay", "[effect][decay]" ) { const efftype_id eff_id( "debugged" ); + const efftype_id eff_id2( "test_int_remove" ); std::vector rem_ids; std::vector rem_bps; - SECTION( "decay reduces effect duration by 1 turn" ) { + SECTION( "decay reduces effect duration by 1 turn and triggers intensity decay" ) { effect eff_debugged( effect_source::empty(), &eff_id.obj(), 2_turns, bodypart_str_id( "bp_null" ), - false, 1, calendar::turn ); + false, 5, calendar::turn ); // Ensure it will last 2 turns, and is not permanent/paused REQUIRE( to_turns( eff_debugged.get_duration() ) == 2 ); REQUIRE_FALSE( eff_debugged.is_permanent() ); @@ -260,13 +279,123 @@ TEST_CASE( "effect decay", "[effect][decay]" ) CHECK( to_turns( eff_debugged.get_duration() ) == 2 ); } - // TODO: - // When intensity > 1 - // - and duration < max_duration - // - and int_decay_tick is set (from effect JSON) - // - and time % decay_tick == 0 - // Then: - // - add int_decay_step to intensity (default -1) + SECTION( "intensity decay triggers on the appropriate turns" ) { + effect eff_debugged( effect_source::empty(), &eff_id.obj(), 1_hours, bodypart_str_id( "bp_null" ), + false, 10, calendar::turn ); + // Ensure it has a decay tick of 2 turns and a decay step of -1, and int removal is allwed + // Also check max duration + REQUIRE( eff_debugged.get_max_duration() == 1_hours ); + REQUIRE( eff_debugged.get_intensity() == 10 ); + REQUIRE( eff_debugged.get_int_decay_step() == -1 ); + REQUIRE( eff_debugged.get_int_decay_tick() == 2 ); + REQUIRE( eff_debugged.get_int_decay_remove() == true ); + // Reset time + calendar::turn = calendar::start_of_cataclysm; + + // First decay - at max duration, no intensity decay + CHECK( eff_debugged.get_duration() == eff_debugged.get_max_duration() ); + eff_debugged.decay( rem_ids, rem_bps, calendar::turn, false ); + CHECK( eff_debugged.get_intensity() == 10 ); + // Effect not removed + CHECK( rem_ids.empty() ); + CHECK( rem_bps.empty() ); + + // Progress turns, checking for the appropriate intensity and no removal + + // Turn 1, no intensity change + calendar::turn += 1_turns; + eff_debugged.decay( rem_ids, rem_bps, calendar::turn, false ); + CHECK( eff_debugged.get_intensity() == 10 ); + // Effect not removed + CHECK( rem_ids.empty() ); + CHECK( rem_bps.empty() ); + + // Turn 2, intensity decays + calendar::turn += 1_turns; + eff_debugged.decay( rem_ids, rem_bps, calendar::turn, false ); + CHECK( eff_debugged.get_intensity() == 9 ); + // Effect not removed + CHECK( rem_ids.empty() ); + CHECK( rem_bps.empty() ); + + // Turn 3, no intensity change + calendar::turn += 1_turns; + eff_debugged.decay( rem_ids, rem_bps, calendar::turn, false ); + CHECK( eff_debugged.get_intensity() == 9 ); + // Effect not removed + CHECK( rem_ids.empty() ); + CHECK( rem_bps.empty() ); + + // Skip the intermediate levels, set intensity to 1 manually before proceeding + eff_debugged.set_intensity( 1 ); + calendar::turn += 1_turns; + eff_debugged.decay( rem_ids, rem_bps, calendar::turn, false ); + CHECK( eff_debugged.get_intensity() == 0 ); + // Effect is removed + CHECK( rem_ids.size() == 1 ); + CHECK( rem_bps.size() == 1 ); + // Effect ID and body part are pushed to the arrays + REQUIRE( !rem_ids.empty() ); + CHECK( rem_ids.front() == eff_debugged.get_id() ); + REQUIRE( !rem_ids.empty() ); + CHECK( rem_bps.front() == bodypart_id( "bp_null" ) ); + + } + + SECTION( "int_decay_remove == false protects an effect from removal" ) { + effect eff_test_int_remove( effect_source::empty(), &eff_id2.obj(), 3_turns, + bodypart_str_id( "bp_null" ), + false, 3, calendar::turn ); + // Ensure it has the -2 int decay step, is protected from removal and has a decay tick of 1 + REQUIRE( eff_test_int_remove.get_int_decay_step() == -2 ); + REQUIRE( eff_test_int_remove.get_int_decay_tick() == 1 ); + REQUIRE( eff_test_int_remove.get_int_decay_remove() == false ); + // Ensure it will last 3 turns, and is not permanent/paused + REQUIRE( to_turns( eff_test_int_remove.get_duration() ) == 3 ); + REQUIRE_FALSE( eff_test_int_remove.is_permanent() ); + + // First decay - 2 turns left, intensity decay procs + eff_test_int_remove.decay( rem_ids, rem_bps, calendar::turn, false ); + CHECK( to_turns( eff_test_int_remove.get_duration() ) == 2 ); + CHECK( eff_test_int_remove.get_intensity() == 1 ); + // Effect not removed + CHECK( rem_ids.empty() ); + CHECK( rem_bps.empty() ); + + // Second decay - 1 turns left, intensity stays 1 (but wouldn't proc anyway) + calendar::turn += 1_turns; + eff_test_int_remove.decay( rem_ids, rem_bps, calendar::turn, false ); + CHECK( to_turns( eff_test_int_remove.get_duration() ) == 1 ); + CHECK( eff_test_int_remove.get_intensity() == 1 ); + // Effect not removed + CHECK( rem_ids.empty() ); + CHECK( rem_bps.empty() ); + + // Third decay - 0 turn left, intensity decay procs but intensity is capped to 1 + calendar::turn += 1_turns; + eff_test_int_remove.decay( rem_ids, rem_bps, calendar::turn, false ); + CHECK( to_turns( eff_test_int_remove.get_duration() ) == 0 ); + CHECK( eff_test_int_remove.get_intensity() == 1 ); + // Effect not removed + CHECK( rem_ids.empty() ); + CHECK( rem_bps.empty() ); + + // Fourth decay - 0 turns left, intensity stays 1 (but wouldn't proc anyway) + calendar::turn += 1_turns; + eff_test_int_remove.decay( rem_ids, rem_bps, calendar::turn, false ); + CHECK( to_turns( eff_test_int_remove.get_duration() ) == 0 ); + CHECK( eff_test_int_remove.get_intensity() == 1 ); + // Effect is removed + CHECK( rem_ids.size() == 1 ); + CHECK( rem_bps.size() == 1 ); + // Effect ID and body part are pushed to the arrays + REQUIRE( !rem_ids.empty() ); + CHECK( rem_ids.front() == eff_test_int_remove.get_id() ); + REQUIRE( !rem_ids.empty() ); + CHECK( rem_bps.front() == bodypart_id( "bp_null" ) ); + + + } } // Effect description