diff --git a/data/json/effects.json b/data/json/effects.json index 969cafe50464..d0923c3c9806 100644 --- a/data/json/effects.json +++ b/data/json/effects.json @@ -1268,15 +1268,26 @@ { "type": "effect_type", "id": "adrenaline", - "name": [ "Adrenaline Comedown", "Adrenaline Rush" ], - "desc": [ "You feel completely drained.", "You feel the rush of adrenaline in your body!" ], + "name": [ "Adrenaline Rush" ], + "desc": [ "You feel the rush of adrenaline in your body!" ], "apply_message": "You feel a surge of adrenaline!", "rating": "good", + "removes_effects": [ "winded" ], + "max_duration": "5 m", + "base_mods": { "speed_mod": [ 20 ], "str_mod": [ 2 ], "dex_mod": [ 2 ], "int_mod": [ -8 ], "per_mod": [ 1 ], "stamina_min": [ 2 ] }, + "blood_analysis_description": "Adrenaline Spike", + "effects_on_remove": [ { "allow_on_remove": true, "effect_type": "adrenaline_comedown", "duration": "1 m" } ] + }, + { + "type": "effect_type", + "id": "adrenaline_comedown", + "name": [ "Adrenaline Comedown" ], + "desc": [ "You feel completely drained." ], + "apply_message": "Your adrenaline rush wears off. You feel AWFUL!", + "rating": "bad", "decay_messages": [ [ "Your adrenaline rush wears off. You feel AWFUL!", "bad" ] ], "miss_messages": [ [ "Your comedown throws you off.", 1 ] ], - "max_intensity": 2, - "int_dur_factor": "150 s", - "removes_effects": [ "winded" ], + "max_duration": "5 m", "base_mods": { "speed_mod": [ -10 ], "str_mod": [ -2 ], @@ -1284,9 +1295,7 @@ "int_mod": [ -1 ], "per_mod": [ -1 ], "stamina_min": [ -2 ] - }, - "scaling_mods": { "speed_mod": [ 30 ], "str_mod": [ 4 ], "dex_mod": [ 4 ], "int_mod": [ -7 ], "per_mod": [ 2 ], "stamina_min": [ 4 ] }, - "blood_analysis_description": "Adrenaline Spike" + } }, { "type": "effect_type", diff --git a/data/mods/TEST_DATA/effects.json b/data/mods/TEST_DATA/effects.json index 89713dd21408..637afe4961d0 100644 --- a/data/mods/TEST_DATA/effects.json +++ b/data/mods/TEST_DATA/effects.json @@ -18,5 +18,25 @@ "id": "test_high", "morale": "morale_test", "base_mods": { "morale": [ 25 ] } + }, + { + "type": "effect_type", + "id": "test_juggling_l1", + "effects_on_remove": [ { "effect_type": "test_juggling_r1", "duration": "0 s", "body_part": "hand_r" } ] + }, + { + "type": "effect_type", + "id": "test_juggling_r1", + "effects_on_remove": [ { "effect_type": "test_juggling_r2", "duration": "0 s", "body_part": "hand_r" } ] + }, + { + "type": "effect_type", + "id": "test_juggling_r2", + "effects_on_remove": [ { "effect_type": "test_juggling_l2", "duration": "0 s", "body_part": "hand_l" } ] + }, + { + "type": "effect_type", + "id": "test_juggling_l2", + "effects_on_remove": [ { "effect_type": "test_juggling_l1", "duration": "0 s" } ] } ] diff --git a/doc/src/content/docs/en/mod/json/reference/creatures/effects_json.md b/doc/src/content/docs/en/mod/json/reference/creatures/effects_json.md index b164147b02b8..0d640645d788 100644 --- a/doc/src/content/docs/en/mod/json/reference/creatures/effects_json.md +++ b/doc/src/content/docs/en/mod/json/reference/creatures/effects_json.md @@ -371,6 +371,31 @@ effect can hurt the player. Type of morale effect provided. Mandatory if there is a morale effect, must not be specified otherwise. +### Other effects on removal + +```json +"effects_on_remove": [ + { + "intensity_requirement": 0, - Defaults to 0 + "effect_type": "cold", - (Mandatory) Effect that will be applied + "allow_on_decay": false, - Defaults to true + "allow_on_remove" true, - Defaults to false + "intensity": 5, - Defaults to 0 + "inherit_intensity": false, - Defaults to false + "duration": "10 s", - Defaults to 0 + "inherit_duration": true, - Defaults to true + "body_part": "hand_r, - Defaults to null + "inherit_body_part": false - Defaults to true + } +] +``` + +"intensity_requirement" will prevent adding the new effect if current effect has lower intensity. +"allow_on_decay" enables adding the effect if parent decayed (was removed due to 0 duration). +"allow_on_remove" enables adding the effect if parent was removed before 0 duration. +"inherit_duration", "inherit_intensity" and "inherit_body_part" cause the relevant variable to be +copied from parent effect. + ### Effect effects ```json diff --git a/src/bionics.cpp b/src/bionics.cpp index 9be34b96a4aa..685642e07d5f 100644 --- a/src/bionics.cpp +++ b/src/bionics.cpp @@ -774,6 +774,7 @@ bool Character::activate_bionic( bionic &bio, bool eff_only, bool *close_bionics } else if( bio.id == bio_blood_filter ) { add_msg_activate(); static const std::vector removable = {{ + effect_adrenaline, effect_fungus, effect_dermatik, effect_bloodworms, effect_poison, effect_stung, effect_badpoison, effect_pkill1, effect_pkill2, effect_pkill3, effect_pkill_l, @@ -788,7 +789,6 @@ bool Character::activate_bionic( bionic &bio, bool eff_only, bool *close_bionics remove_effect( eff ); } // Purging the substance won't remove the fatigue it caused - force_comedown( get_effect( effect_adrenaline ) ); force_comedown( get_effect( effect_meth ) ); set_painkiller( 0 ); set_stim( 0 ); @@ -826,7 +826,7 @@ bool Character::activate_bionic( bionic &bio, bool eff_only, bool *close_bionics return false; } else { add_msg_activate(); - add_effect( effect_adrenaline, 20_minutes ); + add_effect( effect_adrenaline, 3_minutes ); } } else if( bio.id == bio_emp ) { if( const std::optional pnt = choose_adjacent( _( "Create an EMP where?" ) ) ) { diff --git a/src/character.cpp b/src/character.cpp index 8e72e6f44c46..9718b7b33d5d 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -1140,7 +1140,7 @@ void Character::set_pain( int npain ) int Character::get_perceived_pain() const { - if( get_effect_int( effect_adrenaline ) > 1 ) { + if( has_effect( effect_adrenaline ) ) { return 0; } @@ -5514,10 +5514,6 @@ void Character::check_needs_extremes() } g->events().send( getID(), effect_jetinjector ); set_part_hp_cur( bodypart_id( "torso" ), 0 ); - } else if( get_effect_dur( effect_adrenaline ) > 50_minutes ) { - add_msg_if_player( m_bad, _( "Your heart spasms and stops." ) ); - g->events().send( getID(), effect_adrenaline ); - set_part_hp_cur( bodypart_id( "torso" ), 0 ); } else if( get_effect_int( effect_drunk ) > 4 ) { add_msg_if_player( m_bad, _( "Your breathing slows down to a stop." ) ); g->events().send( getID(), effect_drunk ); @@ -9205,7 +9201,7 @@ void Character::on_hurt( Creature *source, bool disturb /*= true*/ ) if( has_trait( trait_ADRENALINE ) && !has_effect( effect_adrenaline ) && ( get_part_hp_cur( bodypart_id( "head" ) ) < 25 || get_part_hp_cur( bodypart_id( "torso" ) ) < 15 ) ) { - add_effect( effect_adrenaline, 20_minutes ); + add_effect( effect_adrenaline, 3_minutes ); } if( disturb ) { @@ -10451,7 +10447,7 @@ void Character::on_item_takeoff( const item &it ) void Character::on_effect_int_change( const efftype_id &effect_type, int intensity, const bodypart_str_id &bp ) { - // Adrenaline can reduce perceived pain (or increase it when you enter comedown). + // Adrenaline can reduce perceived pain (or increase it when it times out). // See @ref get_perceived_pain() if( effect_type == effect_adrenaline ) { // Note that calling this does no harm if it wasn't changed. diff --git a/src/creature.cpp b/src/creature.cpp index 9870a7cdd4b7..08236e51b1ac 100644 --- a/src/creature.cpp +++ b/src/creature.cpp @@ -1361,6 +1361,17 @@ int Creature::get_effect_int( const efftype_id &eff_id, body_part bp ) const return 0; } + +struct removed_effect { + public: + removed_effect( efftype_id type, bodypart_str_id bp, bool is_decayed ) : + type( type ), bp( bp ), is_decayed( is_decayed ) + {} + efftype_id type; + bodypart_str_id bp; + bool is_decayed; +}; + void Creature::process_effects() { process_effects_internal(); @@ -1368,24 +1379,26 @@ void Creature::process_effects() // id's and body_part's of all effects to be removed. If we ever get player or // monster specific removals these will need to be moved down to that level and then // passed in to this function. - std::vector> to_remove; + std::vector to_remove; + + std::vector to_add; // Decay/removal of effects for( auto &elem : *effects ) { for( auto &_it : elem.second ) { if( _it.second.is_removed() ) { - to_remove.emplace_back( elem.first, _it.first ); + to_remove.emplace_back( elem.first, _it.first, false ); continue; } // Add any effects that others remove to the removal list for( const efftype_id &removed_effect : _it.second.get_removes_effects() ) { - to_remove.emplace_back( removed_effect, bodypart_str_id::NULL_ID() ); + to_remove.emplace_back( removed_effect, bodypart_str_id::NULL_ID(), false ); } effect &e = _it.second; const int prev_int = e.get_intensity(); // Run decay effects, marking effects for removal as necessary. if( e.decay( calendar::turn, is_player() ) ) { - to_remove.emplace_back( elem.first, _it.first ); + to_remove.emplace_back( elem.first, _it.first, true ); } if( e.get_intensity() != prev_int && e.get_duration() > 0_turns ) { @@ -1395,21 +1408,47 @@ void Creature::process_effects() } // Run the on-remove effects - for( const std::pair &r : to_remove ) { - remove_effect( r.first, r.second ); + for( const removed_effect &r : to_remove ) { + const auto &add_after = r.type->get_effects_on_remove(); + if( !add_after.empty() ) { + bool found = false; + // Copypasted from get_effect, but without check for `removed` flag + auto got_outer = effects->find( r.type ); + if( got_outer != effects->end() ) { + auto got_inner = got_outer->second.find( convert_bp( r.bp->token ) ); + if( got_inner != got_outer->second.end() ) { + const auto &parent = got_inner->second; + const auto &decay_effects = r.is_decayed ? + parent.create_decay_effects() : + parent.create_removal_effects(); + to_add.insert( to_add.end(), decay_effects.begin(), decay_effects.end() ); + found = true; + } + } + + if( !found ) { + debugmsg( "Couldn't find effect to remove %s", r.type.str() ); + } + } + + remove_effect( r.type, r.bp ); } // Actually remove effects. This should be the last thing done in process_effects(). - for( const std::pair &r : to_remove ) { - if( !r.second ) { - effects->erase( r.first ); + for( const removed_effect &r : to_remove ) { + if( !r.bp ) { + effects->erase( r.type ); } else { - ( *effects )[r.first].erase( r.second ); + ( *effects )[r.type].erase( r.bp ); // If there are no more effects of a given type remove the type map - if( ( *effects )[r.first].empty() ) { - effects->erase( r.first ); + if( ( *effects )[r.type].empty() ) { + effects->erase( r.type ); } } } + + for( const effect &eff : to_add ) { + add_effect( eff ); + } } bool Creature::resists_effect( const effect &e ) const diff --git a/src/effect.cpp b/src/effect.cpp index 53c94fc2d543..4fba4ebd253b 100644 --- a/src/effect.cpp +++ b/src/effect.cpp @@ -1420,6 +1420,14 @@ void load_effect_type( const JsonObject &jo ) } } + if( jo.has_array( "effects_on_remove" ) ) { + JsonArray jarr = jo.get_array( "effects_on_remove" ); + for( JsonObject jo_decay : jarr ) { + new_etype.effects_on_remove.emplace_back(); + new_etype.effects_on_remove.back().load_decay( jo_decay ); + } + } + effect_types[new_etype.id] = new_etype; } @@ -1515,3 +1523,70 @@ std::string texitify_healing_power( const int power ) } return ""; } + +void caused_effect::load_decay( const JsonObject &jo ) +{ + assign( jo, "allow_on_decay", allow_on_decay ); + assign( jo, "allow_on_remove", allow_on_remove ); + load( jo ); +} + +void caused_effect::load( const JsonObject &jo ) +{ + assign( jo, "effect_type", type ); + assign( jo, "intensity_requirement", intensity_requirement ); + + if( assign( jo, "duration", duration ) ) { + // In case of copy-from + inherit_duration = false; + } + assign( jo, "inherit_duration", inherit_duration ); + if( jo.has_member( "duration" ) && jo.has_member( "inherit_duration" ) ) { + jo.throw_error( R"("duration" and "inherit_duration" can't both be set at the same time.)" ); + } + + if( assign( jo, "intensity", intensity ) ) { + inherit_intensity = false; + } + assign( jo, "inherit_intensity", inherit_intensity ); + if( jo.has_member( "intensity" ) && jo.has_member( "inherit_intensity" ) ) { + jo.throw_error( R"("intensity" and "inherit_intensity" can't both be set at the same time.)" ); + } + + if( assign( jo, "body_part", bp ) ) { + inherit_body_part = false; + } + assign( jo, "inherit_body_part", inherit_body_part ); + if( jo.has_member( "intensity" ) && jo.has_member( "inherit_intensity" ) ) { + jo.throw_error( R"("body_part" and "inherit_body_part" can't both be set at the same time.)" ); + } +} + +std::vector effect::create_decay_effects() const +{ + return create_child_effects( true ); +} + +std::vector effect::create_removal_effects() const +{ + return create_child_effects( false ); +} + +std::vector effect::create_child_effects( bool decay ) const +{ + std::vector ret; + for( const auto &new_effect : eff_type->effects_on_remove ) { + if( this->intensity < new_effect.intensity_requirement || + ( decay && !new_effect.allow_on_decay ) || + ( !decay && !new_effect.allow_on_remove ) ) { + continue; + } + const effect_type *new_effect_type = &*new_effect.type; + time_duration dur = new_effect.inherit_duration ? this->duration : new_effect.duration; + int intensity = new_effect.inherit_intensity ? this->intensity : new_effect.intensity; + bodypart_str_id bp = new_effect.inherit_body_part ? convert_bp( this->bp ) : new_effect.bp; + effect e = effect( new_effect_type, dur, bp, intensity, calendar::turn ); + ret.emplace_back( e ); + } + return ret; +} diff --git a/src/effect.h b/src/effect.h index 3bf22ecf0c0d..127445ca72d1 100644 --- a/src/effect.h +++ b/src/effect.h @@ -23,6 +23,7 @@ enum game_message_type : int; class JsonIn; class JsonObject; class JsonOut; +class effect; /** Handles the large variety of weed messages. */ void weed_msg( player &p ); @@ -34,6 +35,50 @@ enum effect_rating { e_mixed // The effect has good and bad parts to the one who has it. }; +struct caused_effect { + public: + efftype_id type; + /** Minimum parent effect intensity to apply the new effect. */ + int intensity_requirement = 0; + /** If false, prevents application if the trigger was parent decaying to 0 duration. */ + bool allow_on_decay = true; + /** If false, prevents application if the trigger was parent being removed with at least 1 turn of duration left. */ + bool allow_on_remove = false; + + /** + * Duration of the new effect. + * If 0, type's max duration will be used instead. + * Should be left at 0 for permanent effects. + */ + time_duration duration = 0_turns; + /** + * If true, duration field is ignored and parent intensity is copied. + * If true and parent duration was <1, the new effect will not be applied. + */ + bool inherit_duration = false; + /** Intensity of the new effect. */ + int intensity = 0; + /** If true, intensity field is ignored and parent effect intensity is copied. */ + bool inherit_intensity = false; + + bodypart_str_id bp = bodypart_str_id::NULL_ID(); + bool inherit_body_part = true; + + void load_decay( const JsonObject &jo ); + + auto tie() const { + return std::tie( type, intensity_requirement, allow_on_decay, allow_on_remove, + duration, inherit_duration, intensity, inherit_intensity ); + } + + bool operator==( const caused_effect &rhs ) const { + return tie() == rhs.tie(); + } + private: + void load( const JsonObject &jo ); +}; + + class effect_type { friend void load_effect_type( const JsonObject &jo ); @@ -81,6 +126,10 @@ class effect_type /** Returns the id of morale type this effect produces. */ morale_type get_morale_type() const; + const std::vector &get_effects_on_remove() const { + return effects_on_remove; + } + bool is_show_in_info() const; /** Returns true if an effect is permanent, i.e. it's duration does not decrease over time. */ @@ -159,6 +208,8 @@ class effect_type morale_type morale; + std::vector effects_on_remove; + /** Key tuple order is:("base_mods"/"scaling_mods", reduced: bool, type of mod: "STR", desired argument: "tick") */ std::unordered_map < std::tuple, double, cata::tuple_hash > mod_data; @@ -308,6 +359,11 @@ class effect /** Returns if the effect is supposed to be handled in Creature::movement */ bool impairs_movement() const; + /** Create a set of effects that should replace this one when it decays to 0 duration. */ + std::vector create_decay_effects() const; + /** Create a set of effects that should replace this one when it is removed prematurely. */ + std::vector create_removal_effects() const; + /** Returns the effect's matching effect_type id. */ const efftype_id &get_id() const { return eff_type->id; @@ -316,6 +372,9 @@ class effect void serialize( JsonOut &json ) const; void deserialize( JsonIn &jsin ); + private: + std::vector create_child_effects( bool decay ) const; + protected: const effect_type *eff_type; time_duration duration; diff --git a/src/iuse.cpp b/src/iuse.cpp index 1c2bf5b356fd..b60159c73dec 100644 --- a/src/iuse.cpp +++ b/src/iuse.cpp @@ -5143,7 +5143,7 @@ int iuse::artifact( player *p, item *it, bool, const tripoint & ) case AEA_ADRENALINE: p->add_msg_if_player( m_good, _( "You're filled with a roaring energy!" ) ); - p->add_effect( effect_adrenaline, rng( 20_minutes, 25_minutes ) ); + p->add_effect( effect_adrenaline, rng( 2_minutes, 3_minutes ) ); break; case AEA_MAP: { @@ -5587,7 +5587,7 @@ int iuse::unfold_generic( player *p, item *it, bool, const tripoint & ) int iuse::adrenaline_injector( player *p, item *it, bool, const tripoint & ) { - if( p->is_npc() && p->get_effect_dur( effect_adrenaline ) >= 30_minutes ) { + if( p->is_npc() && p->get_effect_dur( effect_adrenaline ) >= 3_minutes ) { return 0; } @@ -5602,7 +5602,7 @@ int iuse::adrenaline_injector( player *p, item *it, bool, const tripoint & ) p->mod_healthy( -20 ); } - p->add_effect( effect_adrenaline, 20_minutes ); + p->add_effect( effect_adrenaline, 2_minutes ); return it->type->charges_to_use(); } diff --git a/src/memorial_logger.cpp b/src/memorial_logger.cpp index 3afd358bca7a..44ebdcdf3934 100644 --- a/src/memorial_logger.cpp +++ b/src/memorial_logger.cpp @@ -44,7 +44,6 @@ #include "type_id.h" #include "units.h" -static const efftype_id effect_adrenaline( "adrenaline" ); static const efftype_id effect_datura( "datura" ); static const efftype_id effect_drunk( "drunk" ); static const efftype_id effect_jetinjector( "jetinjector" ); @@ -712,9 +711,6 @@ void memorial_logger::notify( const cata::event &e ) } else if( effect == effect_jetinjector ) { add( pgettext( "memorial_male", "Died of a healing stimulant overdose." ), pgettext( "memorial_female", "Died of a healing stimulant overdose." ) ); - } else if( effect == effect_adrenaline ) { - add( pgettext( "memorial_male", "Died of adrenaline overdose." ), - pgettext( "memorial_female", "Died of adrenaline overdose." ) ); } else if( effect == effect_drunk ) { add( pgettext( "memorial_male", "Died of an alcohol overdose." ), pgettext( "memorial_female", "Died of an alcohol overdose." ) ); diff --git a/src/suffer.cpp b/src/suffer.cpp index 1e0c36a53b8c..7cc9c0eeb787 100644 --- a/src/suffer.cpp +++ b/src/suffer.cpp @@ -773,7 +773,7 @@ void Character::suffer_feral_kill_withdrawl() if( !one_in( 4 ) ) { add_msg_if_player( m_bad, _( "You jolt awake in a panic attack!" ) ); wake_up(); - add_effect( effect_adrenaline, rng( 3_minutes, 5_minutes ) ); + add_effect( effect_adrenaline, rng( 1_minutes, 2_minutes ) ); mod_stim( rng( 5, 10 ) ); add_morale( MORALE_FEELING_BAD, -5, -25, 10_minutes, 3_minutes, true ); } else { diff --git a/tests/effects_test.cpp b/tests/effects_test.cpp new file mode 100644 index 000000000000..1098ae16d722 --- /dev/null +++ b/tests/effects_test.cpp @@ -0,0 +1,71 @@ +#include "catch/catch.hpp" + +#include +#include +#include +#include + +#include "avatar.h" +#include "effect.h" + +static const efftype_id effect_adrenaline( "adrenaline" ); +static const efftype_id effect_adrenaline_comedown( "adrenaline_comedown" ); +static const efftype_id effect_test_juggling_l1( "test_juggling_l1" ); +static const efftype_id effect_test_juggling_l2( "test_juggling_l2" ); +static const efftype_id effect_test_juggling_r1( "test_juggling_r1" ); +static const efftype_id effect_test_juggling_r2( "test_juggling_r2" ); + +TEST_CASE( "Adrenaline decays into adrenaline comedown" ) +{ + REQUIRE( effect_adrenaline.is_valid() ); + REQUIRE( effect_adrenaline->get_effects_on_remove().size() == 1 ); + avatar dummy; + dummy.add_effect( effect_adrenaline, 0_turns ); + REQUIRE( dummy.has_effect( effect_adrenaline ) ); + dummy.process_effects(); + CHECK( !dummy.has_effect( effect_adrenaline ) ); + CHECK( dummy.has_effect( effect_adrenaline_comedown ) ); + const effect &e = dummy.get_effect( effect_adrenaline_comedown ); + auto on_remove = effect_adrenaline->get_effects_on_remove().front(); + CHECK( to_turns( e.get_duration() ) == to_turns( on_remove.duration ) ); +} + +TEST_CASE( "Removed adrenaline still triggers adrenaline comedown" ) +{ + REQUIRE( effect_adrenaline.is_valid() ); + REQUIRE( effect_adrenaline->get_effects_on_remove().size() == 1 ); + avatar dummy; + dummy.add_effect( effect_adrenaline, 100_turns ); + REQUIRE( dummy.has_effect( effect_adrenaline ) ); + dummy.remove_effect( effect_adrenaline ); + dummy.process_effects(); + CHECK( !dummy.has_effect( effect_adrenaline ) ); + REQUIRE( dummy.has_effect( effect_adrenaline_comedown ) ); + const effect &e = dummy.get_effect( effect_adrenaline_comedown ); + auto on_remove = effect_adrenaline->get_effects_on_remove().front(); + CHECK( to_turns( e.get_duration() ) == to_turns( on_remove.duration ) ); +} + +TEST_CASE( "Effect body part switching and inheritance on decay works as expected" ) +{ + REQUIRE( effect_test_juggling_l1.is_valid() ); + avatar dummy; + dummy.add_effect( effect_test_juggling_l1, 0_seconds, bp_hand_l ); + REQUIRE( dummy.has_effect( effect_test_juggling_l1 ) ); + + dummy.process_effects(); + CHECK( !dummy.has_effect( effect_test_juggling_l1 ) ); + CHECK( dummy.has_effect( effect_test_juggling_r1, body_part_hand_r->token ) ); + + dummy.process_effects(); + CHECK( !dummy.has_effect( effect_test_juggling_r1 ) ); + CHECK( dummy.has_effect( effect_test_juggling_r2, body_part_hand_r->token ) ); + + dummy.process_effects(); + CHECK( !dummy.has_effect( effect_test_juggling_r2 ) ); + CHECK( dummy.has_effect( effect_test_juggling_l2, body_part_hand_l->token ) ); + + dummy.process_effects(); + CHECK( !dummy.has_effect( effect_test_juggling_l2 ) ); + CHECK( dummy.has_effect( effect_test_juggling_l1, body_part_hand_l->token ) ); +}