Skip to content

Commit

Permalink
Eating same food repeatedly gives less fun
Browse files Browse the repository at this point in the history
Introduces a new JSON comestible property, "monotony_penalty", that is
subtracted from a food item's fun score for every identical meal you've
eaten in the last 48 hours. Foods are considered identical if the have
the same id, and their components also have the same ids.

The penalty can't take a food's fun below 0
or reduce fun for foods that already had negative fun, unless
that food has the "NEGATIVE_MONOTONY_OK" flag.
Default "monotony_penalty" is 2, except for junk food where it's 0.
  • Loading branch information
Davi-DeGanne committed Dec 7, 2019
1 parent f04c6d5 commit 4d6a5da
Show file tree
Hide file tree
Showing 8 changed files with 74 additions and 5 deletions.
1 change: 1 addition & 0 deletions doc/JSON_FLAGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions doc/JSON_INFO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/character.h
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,19 @@ struct social_modifiers {
return *this;
}
};

struct consumption_event {
time_point time;
itype_id type_id;
uint64_t component_hash = 0;

consumption_event( const item &food ) {
time = calendar::turn;
type_id = food.typeId();
component_hash = food.make_component_hash();
}
};

inline social_modifiers operator+( social_modifiers lhs, const social_modifiers &rhs )
{
lhs += rhs;
Expand Down Expand Up @@ -1240,6 +1253,7 @@ class Character : public Creature, public visitable<Character>

stomach_contents stomach;
stomach_contents guts;
std::list<consumption_event> consumption_history;

int oxygen;
int radiation;
Expand Down
22 changes: 22 additions & 0 deletions src/consumption.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,21 @@ std::pair<int, int> 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 consumption_event &event : consumption_history ) {
if( event.time > calendar::turn - 2_days && event.type_id == comest.typeId() &&
event.component_hash == comest.make_component_hash() ) {
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 ) {
Expand Down Expand Up @@ -952,6 +967,13 @@ bool player::eat( item &food, bool force )
if( will_vomit ) {
vomit();
}

consumption_history.emplace_back( food );
// Clean out consumption_history so it doesn't get bigger than needed.
while( consumption_history.front().time < calendar::turn - 2_days ) {
consumption_history.pop_front();
}

return true;
}

Expand Down
17 changes: 17 additions & 0 deletions src/item.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8067,6 +8067,23 @@ std::string item::components_to_string() const
}, enumeration_conjunction::none );
}

uint64_t item::make_component_hash() const
{
// First we need to ensure all the IDs are unique, to ensure we don't XOR identical hashes later.
std::set<std::string> id_set;
for( const item &it : components ) {
id_set.insert( it.typeId() );
}

uint64_t component_hash = 0;
std::hash<std::string> hasher;
for( std::string id : id_set ) {
component_hash ^= hasher( id );
}

return component_hash;
}

bool item::needs_processing() const
{
return active || has_flag( "RADIO_ACTIVATION" ) || has_flag( "ETHEREAL_ITEM" ) ||
Expand Down
3 changes: 3 additions & 0 deletions src/item.h
Original file line number Diff line number Diff line change
Expand Up @@ -1204,6 +1204,9 @@ class item : public visitable<item>
* no components */
std::string components_to_string() const;

/** Creates a hash from the itype_ids of this item's @ref components. */
uint64_t make_component_hash() const;

/** return the unique identifier of the items underlying type */
itype_id typeId() const;

Expand Down
17 changes: 12 additions & 5 deletions src/item_factory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -1699,13 +1700,19 @@ 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();
slot.specific_heat_solid = specific_heat_solid / jo.get_tags( "material" ).size();
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" ) );
}
Expand Down
3 changes: 3 additions & 0 deletions src/itype.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<vitamin_id, int> vitamins;

Expand Down

0 comments on commit 4d6a5da

Please sign in to comment.