diff --git a/doc/JSON_FLAGS.md b/doc/JSON_FLAGS.md index 081bc1d050f54..0f3febbe9fe0f 100644 --- a/doc/JSON_FLAGS.md +++ b/doc/JSON_FLAGS.md @@ -475,6 +475,7 @@ Some armor flags, such as `WATCH` and `ALARMCLOCK` are compatible with other ite - ```MELTS``` Provides half fun unless frozen. Edible when frozen. - ```MILLABLE``` Can be placed inside a mill, to turn into flour. - ```MYCUS_OK``` Can be eaten by post-threshold Mycus characters. Only applies to mycus fruits by default. +- ```NEGATIVE_MONOTONY_OK``` Allows ```negative_monotony``` property to lower comestible fun to negative values. - ```NO_INGEST``` Administered by some means other than oral intake. - ```PKILL_1``` Minor painkiller. - ```PKILL_2``` Moderate painkiller. diff --git a/doc/JSON_INFO.md b/doc/JSON_INFO.md index 9c5bd769c43c9..e64d487aba3ee 100644 --- a/doc/JSON_INFO.md +++ b/doc/JSON_INFO.md @@ -1367,6 +1367,8 @@ CBMs can be defined like this: "quench" : 0, // Thirst quenched "heal" : -2, // Health effects (used for sickness chances) "addiction_potential" : 80, // Ability to cause addictions +"monotony_penalty" : 0, // (Optional, default: 2) Fun is reduced by this number for each one you've consumed in the last 48 hours. + // Can't drop fun below 0, unless the comestible also has the "NEGATIVE_MONOTONY_OK" flag. "calories" : 0, // Hunger satisfied (in kcal) "nutrition" : 0, // Hunger satisfied (OBSOLETE) "tool" : "apparatus", // Tool required to be eaten/drank diff --git a/src/character.h b/src/character.h index 5bc38b28ddddd..13c595894311a 100644 --- a/src/character.h +++ b/src/character.h @@ -1240,6 +1240,7 @@ class Character : public Creature, public visitable stomach_contents stomach; stomach_contents guts; + std::list> consumption_history; int oxygen; int radiation; diff --git a/src/consumption.cpp b/src/consumption.cpp index 47e90619f2c05..93892da4a3664 100644 --- a/src/consumption.cpp +++ b/src/consumption.cpp @@ -204,6 +204,20 @@ std::pair Character::fun_for( const item &comest ) const } } + // Food is less enjoyable when eaten too often. + if( fun > 0 || comest.has_flag( "NEGATIVE_MONOTONY_OK" ) ) { + for( const std::pair &event : consumption_history ) { + if( event.first > calendar::turn - 2_days && event.second->typeId() == comest.typeId() ) { + fun -= comest.get_comestible()->monotony_penalty; + // This effect can't drop fun below 0, unless the food has the right flag. + if( fun <= 0 && !comest.has_flag( "NEGATIVE_MONOTONY_OK" ) ) { + fun = 0; + break; // 0 is the lowest we'll go, no need to keep looping. + } + } + } + } + float fun_max = fun < 0 ? fun * 6 : fun * 3; if( comest.has_flag( flag_EATEN_COLD ) && comest.has_flag( flag_COLD ) ) { if( fun > 0 ) { @@ -952,6 +966,13 @@ bool player::eat( item &food, bool force ) if( will_vomit ) { vomit(); } + + consumption_history.emplace_back( calendar::turn, &food ); + // Clean out consumption_history so it doesn't get bigger than needed. + while( consumption_history.front().first < calendar::turn - 2_days ) { + consumption_history.pop_front(); + } + return true; } diff --git a/src/item_factory.cpp b/src/item_factory.cpp index 1e1bb80649f28..cbd10e2af8a31 100644 --- a/src/item_factory.cpp +++ b/src/item_factory.cpp @@ -1684,12 +1684,13 @@ void Item_factory::load( islot_comestible &slot, JsonObject &jo, const std::stri assign( jo, "cooks_like", slot.cooks_like, strict ); assign( jo, "smoking_result", slot.smoking_result, strict ); + bool is_junkfood = false; if( jo.has_member( "primary_material" ) ) { - slot.specific_heat_solid = material_id( - jo.get_string( "primary_material" ) )->specific_heat_solid(); - slot.specific_heat_liquid = material_id( - jo.get_string( "primary_material" ) )->specific_heat_liquid(); - slot.latent_heat = material_id( jo.get_string( "primary_material" ) )->latent_heat(); + std::string mat = jo.get_string( "primary_material" ); + slot.specific_heat_solid = material_id( mat )->specific_heat_solid(); + slot.specific_heat_liquid = material_id( mat )->specific_heat_liquid(); + slot.latent_heat = material_id( mat )->latent_heat(); + is_junkfood = is_junkfood || mat == "junk"; } else if( jo.has_member( "material" ) ) { float specific_heat_solid = 0; float specific_heat_liquid = 0; @@ -1699,6 +1700,7 @@ void Item_factory::load( islot_comestible &slot, JsonObject &jo, const std::stri specific_heat_solid += material_id( m )->specific_heat_solid(); specific_heat_liquid += material_id( m )->specific_heat_liquid(); latent_heat += material_id( m )->latent_heat(); + is_junkfood = is_junkfood || m == "junk"; } // Average based on number of materials. slot.specific_heat_liquid = specific_heat_liquid / jo.get_tags( "material" ).size(); @@ -1706,6 +1708,11 @@ void Item_factory::load( islot_comestible &slot, JsonObject &jo, const std::stri slot.latent_heat = latent_heat / jo.get_tags( "material" ).size(); } + if( is_junkfood ) { // Junk food never gets old by default, but this can still be overriden. + slot.monotony_penalty = 0; + } + assign( jo, "monotony_penalty", slot.monotony_penalty, strict ); + if( jo.has_string( "addiction_type" ) ) { slot.add = addiction_type( jo.get_string( "addiction_type" ) ); } diff --git a/src/itype.h b/src/itype.h index 5ba5c92cb2328..77b5b76625504 100644 --- a/src/itype.h +++ b/src/itype.h @@ -159,6 +159,9 @@ struct islot_comestible { float specific_heat_solid = 2.108; float latent_heat = 333; + /** A penalty applied to fun for every time this food has been eaten in the last 48 hours */ + int monotony_penalty = 2; + /** vitamins potentially provided by this comestible (if any) */ std::map vitamins;