diff --git a/CataclysmWin.cbp b/CataclysmWin.cbp index bd1dec92b8c18..8bfb3614860fd 100644 --- a/CataclysmWin.cbp +++ b/CataclysmWin.cbp @@ -535,7 +535,7 @@ - + diff --git a/astyled_whitelist b/astyled_whitelist index dac2f26bd6c5e..c0fb912b31ead 100644 --- a/astyled_whitelist +++ b/astyled_whitelist @@ -93,8 +93,8 @@ src/debug.h src/dependency_tree.h src/drawing_primitives.h src/editmap.h -src/explosion.h src/event.h +src/explosion.h src/faction.h src/filesystem.h src/game_constants.h @@ -132,6 +132,7 @@ src/mondefense.h src/monfaction.h src/mongroup.h src/morale.h +src/morale_types.h src/mutation.h src/name.h src/npc_favor.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 64799399ccc66..f72fced6c40ba 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -244,6 +244,7 @@ SET (CATACLYSM_DDA_HEADERS ${CMAKE_SOURCE_DIR}/src/computer.h ${CMAKE_SOURCE_DIR}/src/veh_interact.h ${CMAKE_SOURCE_DIR}/src/morale.h + ${CMAKE_SOURCE_DIR}/src/morale_types.h ${CMAKE_SOURCE_DIR}/src/game.h ${CMAKE_SOURCE_DIR}/src/generic_factory.h ${CMAKE_SOURCE_DIR}/src/help.h diff --git a/src/activity_handlers.cpp b/src/activity_handlers.cpp index 9b28bf16acd8b..d7b1bf7b173e5 100644 --- a/src/activity_handlers.cpp +++ b/src/activity_handlers.cpp @@ -11,7 +11,7 @@ #include "iuse_actor.h" #include "rng.h" #include "mongroup.h" -#include "morale.h" +#include "morale_types.h" #include "messages.h" #include "martialarts.h" #include "itype.h" diff --git a/src/addiction.cpp b/src/addiction.cpp index e5ea4147b4ec7..5b436357e3de6 100644 --- a/src/addiction.cpp +++ b/src/addiction.cpp @@ -2,7 +2,7 @@ #include "debug.h" #include "pldata.h" #include "player.h" -#include "morale.h" +#include "morale_types.h" #include "rng.h" #include "translations.h" diff --git a/src/catalua.cpp b/src/catalua.cpp index a1477b7e02a1d..91dfbdfe38d6c 100644 --- a/src/catalua.cpp +++ b/src/catalua.cpp @@ -24,7 +24,7 @@ #include "ui.h" #include "mongroup.h" #include "itype.h" -#include "morale.h" +#include "morale_types.h" #include "trap.h" #include "overmap.h" #include "mtype.h" diff --git a/src/character.h b/src/character.h index 587079d72c3cd..5254fe91ab496 100644 --- a/src/character.h +++ b/src/character.h @@ -511,6 +511,12 @@ class Character : public Creature, public visitable std::vector my_bionics; + protected: + virtual void on_mutation_gain( const std::string & ) {}; + virtual void on_mutation_loss( const std::string & ) {}; + virtual void on_item_wear( const item & ) {}; + virtual void on_item_takeoff( const item & ) {}; + protected: Character(); Character(const Character &) = default; diff --git a/src/crafting.cpp b/src/crafting.cpp index 7ff6ba0ecffbf..72de92e4a16e0 100644 --- a/src/crafting.cpp +++ b/src/crafting.cpp @@ -10,7 +10,6 @@ #include "json.h" #include "map.h" #include "messages.h" -#include "morale.h" #include "npc.h" #include "options.h" #include "output.h" @@ -205,7 +204,7 @@ bool player::crafting_allowed( const std::string &rec_name ) bool player::crafting_allowed( const recipe &rec ) { - if( !has_morale_to_craft() ) { // See morale.h + if( !has_morale_to_craft() ) { add_msg( m_info, _( "Your morale is too low to craft..." ) ); return false; } diff --git a/src/creature.cpp b/src/creature.cpp index b99449feddc2d..9f448f01ec617 100644 --- a/src/creature.cpp +++ b/src/creature.cpp @@ -807,6 +807,7 @@ void Creature::add_effect( const efftype_id &eff_id, int dur, body_part bp, if (found_effect != bodyparts.end()) { found = true; effect &e = found_effect->second; + const int prev_int = e.get_intensity(); // 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 @@ -832,6 +833,9 @@ void Creature::add_effect( const efftype_id &eff_id, int dur, body_part bp, } else if (e.get_intensity() > e.get_max_intensity()) { e.set_intensity(e.get_max_intensity()); } + if( e.get_intensity() != prev_int ) { + on_effect_int_change( eff_id, e.get_intensity(), bp ); + } } } @@ -881,6 +885,7 @@ void Creature::add_effect( const efftype_id &eff_id, int dur, body_part bp, pgettext("memorial_female", type.get_apply_memorial_log().c_str())); } + on_effect_int_change( eff_id, e.get_intensity(), bp ); // Perform any effect addition effects. bool reduced = resists_effect(e); add_eff_effects(e, reduced); @@ -904,6 +909,12 @@ bool Creature::add_env_effect( const efftype_id &eff_id, body_part vector, int s } void Creature::clear_effects() { + for( auto &elem : effects ) { + for( auto &_effect_it : elem.second ) { + const effect &e = _effect_it.second; + on_effect_int_change( e.get_id(), 0, e.get_bp() ); + } + } effects.clear(); } bool Creature::remove_effect( const efftype_id &eff_id, body_part bp ) @@ -928,9 +939,13 @@ bool Creature::remove_effect( const efftype_id &eff_id, body_part bp ) // num_bp means remove all of a given effect id if (bp == num_bp) { + for( auto &it : effects[eff_id] ) { + on_effect_int_change( eff_id, 0, it.first ); + } effects.erase(eff_id); } else { effects[eff_id].erase(bp); + on_effect_int_change( eff_id, 0, bp ); // If there are no more effects of a given type remove the type map if (effects[eff_id].empty()) { effects.erase(eff_id); @@ -1005,8 +1020,14 @@ void Creature::process_effects() rem_ids.push_back( removed_effect ); rem_bps.push_back(num_bp); } + effect &e = _it.second; + const int prev_int = e.get_intensity(); // Run decay effects, marking effects for removal as necessary. - _it.second.decay( rem_ids, rem_bps, calendar::turn, is_player() ); + e.decay( rem_ids, rem_bps, calendar::turn, is_player() ); + + if( e.get_intensity() != prev_int && e.get_duration() > 0 ) { + on_effect_int_change( e.get_id(), e.get_intensity(), e.get_bp() ); + } } } diff --git a/src/creature.h b/src/creature.h index 2b8226dc34ac1..12dc2d4192bdd 100644 --- a/src/creature.h +++ b/src/creature.h @@ -504,6 +504,9 @@ class Creature Creature &operator=(const Creature &) = default; Creature &operator=(Creature &&) = default; + protected: + virtual void on_effect_int_change( const efftype_id &, int, body_part ) {}; + public: body_part select_body_part(Creature *source, int hit_roll) const; protected: diff --git a/src/editmap.cpp b/src/editmap.cpp index ead9c7ac2bbfc..bd62988fdcb77 100644 --- a/src/editmap.cpp +++ b/src/editmap.cpp @@ -19,7 +19,6 @@ #include "overmapbuffer.h" #include "compatibility.h" #include "translations.h" -#include "morale.h" #include "coordinates.h" #include "npc.h" #include "vehicle.h" diff --git a/src/event.cpp b/src/event.cpp index 3e5d9f4fecdde..e4fc5d5a68d8e 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -9,7 +9,7 @@ #include "overmapbuffer.h" #include "messages.h" #include "sounds.h" -#include "morale.h" +#include "morale_types.h" #include "mapdata.h" #include diff --git a/src/game.cpp b/src/game.cpp index f0774300d0da7..2f5388e57fb98 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -62,7 +62,7 @@ #include "mission.h" #include "compatibility.h" #include "mongroup.h" -#include "morale.h" +#include "morale_types.h" #include "worldfactory.h" #include "material.h" #include "martialarts.h" diff --git a/src/inventory_ui.cpp b/src/inventory_ui.cpp index e22381da1d6c9..84a96433aba2b 100644 --- a/src/inventory_ui.cpp +++ b/src/inventory_ui.cpp @@ -7,7 +7,6 @@ #include "translations.h" #include "options.h" #include "messages.h" -#include "morale.h" #include "input.h" #include "catacharset.h" #include "item_location.h" diff --git a/src/item.cpp b/src/item.cpp index 1b16d130da713..aaac7950a54ea 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -29,7 +29,6 @@ #include "mtype.h" #include "field.h" #include "weather.h" -#include "morale.h" #include "catacharset.h" #include "cata_utility.h" #include "input.h" @@ -1628,7 +1627,7 @@ std::string item::info( bool showtext, std::vector &info ) const } } } - + if( is_gun() && has_flag( "FIRE_TWOHAND" ) ) { info.push_back( iteminfo( "DESCRIPTION", _( "* This weapon needs two free hands to fire." ) ) ); @@ -2024,11 +2023,13 @@ void item::on_wear( player &p ) if( &p == &g->u && type->artifact ) { g->add_artifact_messages( type->artifact->effects_worn ); } + + p.on_item_wear( *this ); } void item::on_takeoff (player &p) { - (void) p; // suppress unused variable warning + p.on_item_takeoff( *this ); if (is_sided()) { set_side(BOTH); diff --git a/src/iuse.cpp b/src/iuse.cpp index a6db051c2ef72..d1cc8875fff10 100644 --- a/src/iuse.cpp +++ b/src/iuse.cpp @@ -27,7 +27,7 @@ #include "iuse_actor.h" // For firestarter #include "mongroup.h" #include "translations.h" -#include "morale.h" +#include "morale_types.h" #include "input.h" #include "npc.h" #include "event.h" @@ -1000,7 +1000,6 @@ int iuse::prozac(player *p, item *it, bool, const tripoint& ) { if( !p->has_effect( effect_took_prozac) && p->get_morale_level() < 0 ) { p->add_effect( effect_took_prozac, 7200); - p->invalidate_morale_level(); } else { p->stim += 3; } @@ -8098,7 +8097,7 @@ int iuse::multicooker(player *p, item *it, bool t, const tripoint &pos) if (mc_upgrade == choice) { - if( !p->has_morale_to_craft() ) { // See morale.h + if( !p->has_morale_to_craft() ) { add_msg(m_info, _("Your morale is too low to craft...")); return false; } diff --git a/src/iuse_actor.cpp b/src/iuse_actor.cpp index f6b2e12d56067..90e893d0fd058 100644 --- a/src/iuse_actor.cpp +++ b/src/iuse_actor.cpp @@ -8,7 +8,7 @@ #include "overmapbuffer.h" #include "sounds.h" #include "translations.h" -#include "morale.h" +#include "morale_types.h" #include "messages.h" #include "material.h" #include "event.h" diff --git a/src/main_menu.cpp b/src/main_menu.cpp index 43be2ce17c747..2c7c05a340f45 100644 --- a/src/main_menu.cpp +++ b/src/main_menu.cpp @@ -14,7 +14,6 @@ #include "filesystem.h" #include "path_info.h" #include "mapsharing.h" -#include "morale.h" #include "sounds.h" #include diff --git a/src/mission_companion.cpp b/src/mission_companion.cpp index cfc28a5d75dcb..bb6103ef0c079 100644 --- a/src/mission_companion.cpp +++ b/src/mission_companion.cpp @@ -9,7 +9,6 @@ #include "catacharset.h" #include "messages.h" #include "mission.h" -#include "morale.h" #include "ammo.h" #include "overmapbuffer.h" #include "json.h" @@ -1254,7 +1253,7 @@ bool talk_function::forage_return(npc *p) } else { popup(_("%s was caught unaware and was forced to fight the creature at close range!"), comp->name.c_str()); // the following doxygen aliases do not yet exist. this is marked for future reference - + ///\EFFECT_MELEE_NPC affects forage mission results ///\EFFECT_SURVIVAL_NPC affects forage mission results diff --git a/src/monattack.cpp b/src/monattack.cpp index b4070e3707150..6d0debe460e2d 100644 --- a/src/monattack.cpp +++ b/src/monattack.cpp @@ -17,7 +17,7 @@ #include "weighted_list.h" #include "mongroup.h" #include "translations.h" -#include "morale.h" +#include "morale_types.h" #include "npc.h" #include "event.h" #include "ui.h" @@ -3851,9 +3851,9 @@ bool mattack::longswipe(monster *z) !z->sees( *target ) ) { return false; // Out of range } - + z->moves -= 150; - + if (target->uncanny_dodge()) { return true; } @@ -3893,7 +3893,7 @@ bool mattack::longswipe(monster *z) // Can we dodge the attack? Uses player dodge function % chance (melee.cpp) if (dodge_check(z, target)) { - target->add_msg_player_or_npc( _("The %s slashes at your neck! You duck!"), + target->add_msg_player_or_npc( _("The %s slashes at your neck! You duck!"), _("The %s slashes at 's neck! They duck!"), z->name().c_str() ); target->on_dodge( z, z->type->melee_skill * 2 ); return true; diff --git a/src/mondeath.cpp b/src/mondeath.cpp index 3ad8fe869c0c6..0e0a3e9d16fa4 100644 --- a/src/mondeath.cpp +++ b/src/mondeath.cpp @@ -10,7 +10,7 @@ #include "mondeath.h" #include "iuse_actor.h" #include "translations.h" -#include "morale.h" +#include "morale_types.h" #include "event.h" #include "itype.h" #include "mtype.h" diff --git a/src/morale.cpp b/src/morale.cpp index 0f17196ba5fe2..371180c0b44e3 100644 --- a/src/morale.cpp +++ b/src/morale.cpp @@ -1,11 +1,22 @@ #include "morale.h" +#include "morale_types.h" #include "cata_utility.h" #include "debug.h" +#include "item.h" #include "itype.h" #include "output.h" +#include "bodypart.h" +#include "catacharset.h" +#include "game.h" +#include "weather.h" #include +#include + +static const efftype_id effect_cold( "cold" ); +static const efftype_id effect_hot( "hot" ); +static const efftype_id effect_took_prozac( "took_prozac" ); namespace { @@ -88,7 +99,62 @@ const std::string &get_morale_data( const morale_type id ) } } // namespace -std::string morale_point::get_name() const +// Morale multiplier +struct morale_mult { + morale_mult(): good( 1.0 ), bad( 1.0 ) {} + morale_mult( double good, double bad ): good( good ), bad( bad ) {} + morale_mult( double both ): good( both ), bad( both ) {} + + double good; // For good morale + double bad; // For bad morale + + morale_mult operator * ( const morale_mult &rhs ) const { + return morale_mult( *this ) *= rhs; + } + + morale_mult &operator *= ( const morale_mult &rhs ) { + good *= rhs.good; + bad *= rhs.bad; + return *this; + } +}; + +inline double operator * ( double morale, const morale_mult &mult ) +{ + return morale * ( ( morale >= 0.0 ) ? mult.good : mult.bad ); +} + +inline double operator * ( const morale_mult &mult, double morale ) +{ + return morale * mult; +} + +inline double operator *= ( double &morale, const morale_mult &mult ) +{ + morale = morale * mult; + return morale; +} + +inline int operator *= ( int &morale, const morale_mult &mult ) +{ + morale = morale * mult; + return morale; +} + +// Commonly used morale multipliers +namespace morale_mults +{ +// Optimistic characters focus on the good things in life, +// and downplay the bad things. +static const morale_mult optimist( 1.25, 0.75 ); +// Again, those grouchy Bad-Tempered folks always focus on the negative. +// They can't handle positive things as well. They're No Fun. D: +static const morale_mult badtemper( 0.75, 1.25 ); +// Prozac reduces overall negative morale by 75%. +static const morale_mult prozac( 1.0, 0.25 ); +} + +std::string player_morale::morale_point::get_name() const { std::string name = get_morale_data( type ); @@ -102,10 +168,40 @@ std::string morale_point::get_name() const return name; } -void morale_point::add( int new_bonus, int new_max_bonus, int new_duration, int new_decay_start, - bool new_cap ) +int player_morale::morale_point::get_net_bonus() const { - if( new_cap ) { + return bonus * ( ( !is_permanent() && age > decay_start ) ? + logarithmic_range( decay_start, duration, age ) : 1 ); +} + +int player_morale::morale_point::get_net_bonus( const morale_mult &mult ) const +{ + return get_net_bonus() * mult; +} + +bool player_morale::morale_point::is_expired() const +{ + // Zero morale bonuses will be shown occasionally anyway + return ( !is_permanent() && age >= duration ) || bonus == 0; +} + +bool player_morale::morale_point::is_permanent() const +{ + return ( duration == 0 ); +} + +bool player_morale::morale_point::matches( morale_type _type, const itype *_item_type ) const +{ + return ( type == _type ) && ( item_type == _item_type ); +} + +void player_morale::morale_point::add( int new_bonus, int new_max_bonus, int new_duration, + int new_decay_start, + bool new_cap ) +{ + new_duration = std::max( 0, new_duration ); + + if( new_cap || new_duration == 0 ) { duration = new_duration; decay_start = new_decay_start; } else { @@ -115,21 +211,22 @@ void morale_point::add( int new_bonus, int new_max_bonus, int new_duration, int decay_start = pick_time( decay_start, new_decay_start, same_sign ); } - age = 0; // Brand new. Don't move above pick_time()'s as they use current age - bonus += new_bonus; + bonus = get_net_bonus() + new_bonus; if( abs( bonus ) > abs( new_max_bonus ) && ( new_max_bonus != 0 || new_cap ) ) { bonus = new_max_bonus; } + + age = 0; // Brand new. The assignment should stay below get_net_bonus() and pick_time(). } -int morale_point::pick_time( int current_time, int new_time, bool same_sign ) const +int player_morale::morale_point::pick_time( int current_time, int new_time, bool same_sign ) const { const int remaining_time = current_time - age; return ( remaining_time <= new_time && same_sign ) ? new_time : remaining_time; } -void morale_point::proceed( int ticks ) +void player_morale::morale_point::decay( int ticks ) { if( ticks < 0 ) { debugmsg( "%s(): Called with negative ticks %d.", __FUNCTION__, ticks ); @@ -137,10 +234,348 @@ void morale_point::proceed( int ticks ) } age += ticks; +} + +void player_morale::add( morale_type type, int bonus, int max_bonus, + int duration, int decay_start, + bool capped, const itype *item_type ) +{ + for( auto &m : points ) { + if( m.matches( type, item_type ) ) { + const int prev_bonus = m.get_net_bonus(); + + m.add( bonus, max_bonus, duration, decay_start, capped ); + + if( m.is_expired() ) { + remove_expired(); + } else if( m.get_net_bonus() != prev_bonus ) { + invalidate(); + } + + return; + } + } + + morale_point new_morale( type, item_type, bonus, duration, decay_start ); + + if( !new_morale.is_expired() ) { + points.push_back( new_morale ); + invalidate(); + } +} + +void player_morale::add_permanent( morale_type type, int bonus, int max_bonus, bool capped, + const itype *item_type ) +{ + add( type, bonus, max_bonus, 0, 0, capped, item_type ); +} + +int player_morale::has( morale_type type, const itype *item_type ) const +{ + for( auto &m : points ) { + if( m.matches( type, item_type ) ) { + return m.get_net_bonus(); + } + } + return 0; +} + +void player_morale::remove_if( const std::function &func ) +{ + const auto new_end = std::remove_if( points.begin(), points.end(), func ); + + if( new_end != points.end() ) { + points.erase( new_end, points.end() ); + invalidate(); + } +} + +void player_morale::remove( morale_type type, const itype *item_type ) +{ + remove_if( [ type, item_type ]( const morale_point & m ) -> bool { + return m.matches( type, item_type ); + } ); + +} + +void player_morale::remove_expired() +{ + remove_if( []( const morale_point & m ) -> bool { + return m.is_expired(); + } ); +} + +morale_mult player_morale::get_temper_mult() const +{ + morale_mult mult; + + if( has( MORALE_PERM_OPTIMIST ) ) { + mult *= morale_mults::optimist; + } + if( has( MORALE_PERM_BADTEMPER ) ) { + mult *= morale_mults::badtemper; + } + + return mult; +} + +int player_morale::get_level() const +{ + if( !level_is_valid ) { + const morale_mult mult = get_temper_mult(); + + level = 0; + for( auto &m : points ) { + level += m.get_net_bonus( mult ); + } + + if( took_prozac ) { + level *= morale_mults::prozac; + } + + level_is_valid = true; + } + + return level; +} + +void player_morale::decay( int ticks ) +{ + const auto do_decay = [ ticks ]( morale_point & m ) { + m.decay( ticks ); + }; + + std::for_each( points.begin(), points.end(), do_decay ); + remove_expired(); + + for( int i = 0; i < ticks; i++ ) { + update_bodytemp_penalty(); + } + + invalidate(); +} + +void player_morale::display( double focus_gain ) +{ + // Create and draw the window itself. + WINDOW *w = newwin( FULL_SCREEN_HEIGHT, FULL_SCREEN_WIDTH, + ( TERMY > FULL_SCREEN_HEIGHT ) ? ( TERMY - FULL_SCREEN_HEIGHT ) / 2 : 0, + ( TERMX > FULL_SCREEN_WIDTH ) ? ( TERMX - FULL_SCREEN_WIDTH ) / 2 : 0 ); + draw_border( w ); + + // Figure out how wide the name column needs to be. + int name_column_width = 18; + for( auto &i : points ) { + int length = utf8_width( i.get_name() ); + if( length > name_column_width ) { + name_column_width = length; + // If it's too wide, truncate. + if( name_column_width >= 72 ) { + name_column_width = 72; + break; + } + } + } + + // Header + mvwprintz( w, 1, 1, c_white, _( "Morale Modifiers:" ) ); + mvwprintz( w, 2, 1, c_ltgray, _( "Name" ) ); + mvwprintz( w, 2, name_column_width + 2, c_ltgray, _( "Value" ) ); + + // Start printing the number right after the name column. + // We'll right-justify it later. + int number_pos = name_column_width + 1; + + const morale_mult mult = get_temper_mult(); + // Print out the morale entries. + for( size_t i = 0; i < points.size(); i++ ) { + const std::string name = points[i].get_name(); + const int bonus = points[i].get_net_bonus( mult ); + const nc_color bonus_color = ( bonus < 0 ? c_red : c_green ); + + // Print out the name. + trim_and_print( w, i + 3, 1, name_column_width, bonus_color, name.c_str() ); + + // Print out the number, right-justified. + mvwprintz( w, i + 3, number_pos, bonus_color, "% 6d", bonus ); + } + + // Print out the total morale, right-justified. + const nc_color level_color = ( get_level() < 0 ? c_red : c_green ); + mvwprintz( w, 20, 1, level_color, _( "Total:" ) ); + mvwprintz( w, 20, number_pos, level_color, "% 6d", get_level() ); + + // Print out the focus gain rate, right-justified. + const nc_color gain_color = ( focus_gain < 0 ? c_red : c_green ); + mvwprintz( w, 22, 1, gain_color, _( "Focus gain:" ) ); + mvwprintz( w, 22, number_pos - 3, gain_color, _( "%6.2f per minute" ), focus_gain ); + + // Make sure the changes are shown. + wrefresh( w ); + + // Wait for any keystroke. + getch(); + + // Close the window. + werase( w ); + delwin( w ); +} + +void player_morale::clear() +{ + points.clear(); + covered.fill( 0 ); + cold.fill( 0 ); + hot.fill( 0 ); + took_prozac = false; + stylish = false; + super_fancy_bonus = 0; + + invalidate(); +} + +void player_morale::invalidate() +{ + level_is_valid = false; +} + +void player_morale::on_mutation_gain( const std::string &mid ) +{ + if( mid == "OPTIMISTIC" ) { + add_permanent( MORALE_PERM_OPTIMIST, 4, 4 ); + } else if( mid == "BADTEMPER" ) { + add_permanent( MORALE_PERM_BADTEMPER, -4, -4 ); + } else if( mid == "STYLISH" ) { + set_stylish( true ); + } +} + +void player_morale::on_mutation_loss( const std::string &mid ) +{ + if( mid == "OPTIMISTIC" ) { + remove( MORALE_PERM_OPTIMIST ); + } else if( mid == "BADTEMPER" ) { + remove( MORALE_PERM_BADTEMPER ); + } else if( mid == "STYLISH" ) { + set_stylish( false ); + } +} + +void player_morale::on_item_wear( const item &it ) +{ + set_worn( it, true ); +} + +void player_morale::on_item_takeoff( const item &it ) +{ + set_worn( it, false ); +} + +void player_morale::on_effect_int_change( const efftype_id &eid, int intensity, body_part bp ) +{ + if( eid == effect_took_prozac && bp == num_bp ) { + set_prozac( intensity != 0 ); + } else if( eid == effect_cold && bp < num_bp ) { + cold[bp] = intensity; + } else if( eid == effect_hot && bp < num_bp ) { + hot[bp] = intensity; + } +} + +void player_morale::set_worn( const item &it, bool worn ) +{ + const bool just_fancy = it.has_flag( "FANCY" ); + const bool super_fancy = it.has_flag( "SUPER_FANCY" ); + + if( just_fancy || super_fancy ) { + const int sign = ( worn ) ? 1 : -1; + + for( int i = 0; i < num_bp; i++ ) { + const auto bp = static_cast( i ); + if( it.covers( bp ) ) { + covered[i] = std::max( covered[i] + sign, 0 ); + } + } + + if( super_fancy ) { + super_fancy_bonus += 2 * sign; + } + + update_stylish_bonus(); + } +} + +void player_morale::set_prozac( bool new_took_prozac ) +{ + if( took_prozac != new_took_prozac ) { + took_prozac = new_took_prozac; + invalidate(); + } +} + +void player_morale::set_stylish( bool new_stylish ) +{ + if( stylish != new_stylish ) { + stylish = new_stylish; + update_stylish_bonus(); + } +} + +void player_morale::update_stylish_bonus() +{ + int bonus = 0; + + if( stylish ) { + if( covered[bp_torso] ) { + bonus += 6; + } + if( covered[bp_head] ) { + bonus += 3; + } + if( covered[bp_eyes] ) { + bonus += 2; + } + if( covered[bp_mouth] ) { + bonus += 2; + } + if( covered[bp_leg_l] || covered[bp_leg_r] ) { + bonus += 2; + } + if( covered[bp_foot_l] || covered[bp_foot_r] ) { + bonus += 1; + } + if( covered[bp_hand_l] || covered[bp_hand_r] ) { + bonus += 1; + } + + bonus = std::min( bonus + super_fancy_bonus, 20 ); + } + + add_permanent( MORALE_PERM_FANCY, bonus, bonus, true ); +} + +void player_morale::update_bodytemp_penalty() +{ + const auto bp_pen = [ this ]( body_part bp, double mul ) -> int { + return mul * ( hot[bp] - cold[bp] ); + }; + + const int pen = + bp_pen( bp_head, 2 ) + + bp_pen( bp_torso, 2 ) + + bp_pen( bp_mouth, 2 ) + + bp_pen( bp_arm_l, .5 ) + + bp_pen( bp_arm_r, .5 ) + + bp_pen( bp_leg_l, .5 ) + + bp_pen( bp_leg_r, .5 ) + + bp_pen( bp_hand_l, .5 ) + + bp_pen( bp_hand_r, .5 ) + + bp_pen( bp_foot_l, .5 ) + + bp_pen( bp_foot_r, .5 ); - if( is_expired() ) { - bonus = 0; - } else if( age > decay_start ) { - bonus *= logarithmic_range( decay_start, duration, age ); + if( pen < 0 ) { + add( MORALE_COLD, -2, pen, 10, 5, true ); + } else if( pen > 0 ) { + add( MORALE_HOT, -2, -pen, 10, 5, true ); } } diff --git a/src/morale.h b/src/morale.h index 74a5561a57ca0..8fb9c49c23645 100644 --- a/src/morale.h +++ b/src/morale.h @@ -4,191 +4,141 @@ #include "json.h" #include #include "calendar.h" +#include "effect.h" +#include "bodypart.h" +#include "morale_types.h" -struct itype; - -enum morale_type : int { - MORALE_NULL = 0, - MORALE_FOOD_GOOD, - MORALE_FOOD_HOT, - MORALE_MUSIC, - MORALE_HONEY, - MORALE_GAME, - MORALE_MARLOSS, - MORALE_MUTAGEN, - MORALE_FEELING_GOOD, - MORALE_SUPPORT, - MORALE_PHOTOS, - - MORALE_CRAVING_NICOTINE, - MORALE_CRAVING_CAFFEINE, - MORALE_CRAVING_ALCOHOL, - MORALE_CRAVING_OPIATE, - MORALE_CRAVING_SPEED, - MORALE_CRAVING_COCAINE, - MORALE_CRAVING_CRACK, - MORALE_CRAVING_MUTAGEN, - MORALE_CRAVING_DIAZEPAM, - MORALE_CRAVING_MARLOSS, - - MORALE_FOOD_BAD, - MORALE_CANNIBAL, - MORALE_VEGETARIAN, - MORALE_MEATARIAN, - MORALE_ANTIFRUIT, - MORALE_LACTOSE, - MORALE_ANTIJUNK, - MORALE_ANTIWHEAT, - MORALE_NO_DIGEST, - MORALE_WET, - MORALE_DRIED_OFF, - MORALE_COLD, - MORALE_HOT, - MORALE_FEELING_BAD, - MORALE_KILLED_INNOCENT, - MORALE_KILLED_FRIEND, - MORALE_KILLED_MONSTER, - MORALE_MUTILATE_CORPSE, - MORALE_MUTAGEN_ELF, - MORALE_MUTAGEN_CHIMERA, - MORALE_MUTAGEN_MUTATION, - - MORALE_MOODSWING, - MORALE_BOOK, - MORALE_COMFY, - - MORALE_SCREAM, - - MORALE_PERM_MASOCHIST, - MORALE_PERM_HOARDER, - MORALE_PERM_FANCY, - MORALE_PERM_OPTIMIST, - MORALE_PERM_BADTEMPER, - MORALE_PERM_CONSTRAINED, - MORALE_GAME_FOUND_KITTEN, - - MORALE_HAIRCUT, - MORALE_SHAVE, - - NUM_MORALE_TYPES -}; - -// Morale multiplier -struct morale_mult { - morale_mult(): good( 1.0 ), bad( 1.0 ) {} - morale_mult( double good, double bad ): good( good ), bad( bad ) {} - morale_mult( double both ): good( both ), bad( both ) {} - - double good; // For good morale - double bad; // For bad morale - - morale_mult operator * ( const morale_mult &rhs ) const { - return morale_mult( *this ) *= rhs; - } - - morale_mult &operator *= ( const morale_mult &rhs ) { - good *= rhs.good; - bad *= rhs.bad; - return *this; - } -}; - -inline double operator * ( double morale, const morale_mult &mult ) -{ - return morale * ( ( morale >= 0.0 ) ? mult.good : mult.bad ); -} +#include -inline double operator * ( const morale_mult &mult, double morale ) -{ - return morale * mult; -} +class item; -inline double operator *= ( double &morale, const morale_mult &mult ) -{ - morale = morale * mult; - return morale; -} - -inline int operator *= ( int &morale, const morale_mult &mult ) -{ - morale = morale * mult; - return morale; -} +struct itype; +struct morale_mult; -// Commonly used morale multipliers -namespace morale_mults -{ -// Optimistic characters focus on the good things in life, -// and downplay the bad things. -static const morale_mult optimistic( 1.25, 0.75 ); -// Again, those grouchy Bad-Tempered folks always focus on the negative. -// They can't handle positive things as well. They're No Fun. D: -static const morale_mult badtemper( 0.75, 1.25 ); -// Prozac reduces overall negative morale by 75%. -static const morale_mult prozac( 1.0, 0.25 ); -} - -class morale_point : public JsonSerializer, public JsonDeserializer +class player_morale { public: - morale_point( - morale_type type = MORALE_NULL, - const itype *item_type = nullptr, - int bonus = 0, - int duration = MINUTES( 6 ), - int decay_start = MINUTES( 3 ), - int age = 0 ): - type( type ), - item_type( item_type ), - bonus( bonus ), - duration( duration ), - decay_start( decay_start ), - age( age ) {}; - - using JsonDeserializer::deserialize; - void deserialize( JsonIn &jsin ) override; - using JsonSerializer::serialize; - void serialize( JsonOut &json ) const override; - - std::string get_name() const; - - morale_type get_type() const { - return type; - } - - const itype *get_item_type() const { - return item_type; - } - - int get_bonus() const { - return bonus; - } - - int get_net_bonus( const morale_mult &mult ) const { - return bonus * mult; - } - - bool is_expired() const { - return age >= duration || bonus == 0; - } - - void add( int new_bonus, int new_max_bonus, int new_duration, int new_decay_start, bool new_cap ); - void proceed( int ticks = 1 ); + player_morale() : + + covered {{}}, + hot {{}}, + cold {{}}, + level( 0 ), + level_is_valid( false ), + took_prozac( false ), + stylish( false ), + super_fancy_bonus( 0 ) {}; + + player_morale( player_morale && ) = default; + player_morale( const player_morale & ) = default; + player_morale &operator =( player_morale && ) = default; + player_morale &operator =( const player_morale & ) = default; + + /** Adds morale to existing or creates one */ + void add( morale_type type, int bonus, int max_bonus = 0, int duration = MINUTES( 6 ), + int decay_start = MINUTES( 3 ), bool capped = false, const itype *item_type = nullptr ); + /** Adds permanent morale to existing or creates one */ + void add_permanent( morale_type type, int bonus, int max_bonus = 0, + bool capped = false, const itype *item_type = nullptr ); + /** Returns bonus from specified morale */ + int has( morale_type type, const itype *item_type = nullptr ) const; + /** Removes specified morale */ + void remove( morale_type type, const itype *item_type = nullptr ); + /** Clears up all morale points */ + void clear(); + /** Returns overall morale level */ + int get_level() const; + /** Ticks down morale counters and removes them */ + void decay( int ticks = 1 ); + /** Displays morale screen */ + void display( double focus_gain ); + + void on_mutation_gain( const std::string &mid ); + void on_mutation_loss( const std::string &mid ); + void on_item_wear( const item &it ); + void on_item_takeoff( const item &it ); + void on_effect_int_change( const efftype_id &eid, int intensity, body_part bp = num_bp ); + + void store( JsonOut &jsout ) const; + void load( JsonObject &jsin ); + + private: + class morale_point : public JsonSerializer, public JsonDeserializer + { + public: + morale_point( + morale_type type = MORALE_NULL, + const itype *item_type = nullptr, + int bonus = 0, + int duration = MINUTES( 6 ), + int decay_start = MINUTES( 3 ), + int age = 0 ) : + + type( type ), + item_type( item_type ), + bonus( bonus ), + duration( duration ), + decay_start( decay_start ), + age( age ) {}; + + using JsonDeserializer::deserialize; + void deserialize( JsonIn &jsin ) override; + using JsonSerializer::serialize; + void serialize( JsonOut &json ) const override; + + std::string get_name() const; + int get_net_bonus() const; + int get_net_bonus( const morale_mult &mult ) const; + bool is_expired() const; + bool is_permanent() const; + bool matches( morale_type _type, const itype *_item_type = nullptr ) const; + + void add( int new_bonus, int new_max_bonus, int new_duration, + int new_decay_start, bool new_cap ); + void decay( int ticks = 1 ); + + private: + morale_type type; + const itype *item_type; + + int bonus; + int duration; // Zero duration == infinity + int decay_start; + int age; + + /** + * Returns either new_time or remaining time (which one is greater). + * Only returns new time if same_sign is true + */ + int pick_time( int cur_time, int new_time, bool same_sign ) const; + }; + protected: + morale_mult get_temper_mult() const; + + void set_prozac( bool new_took_prozac ); + void set_stylish( bool new_stylish ); + void set_worn( const item &it, bool worn ); + + void remove_if( const std::function &func ); + void remove_expired(); + void invalidate(); + + void update_stylish_bonus(); + void update_bodytemp_penalty(); private: - morale_type type; - const itype *item_type; - - int bonus; - int duration; - int decay_start; - int age; - - /** - * Returns either new_time or remaining time (which one is greater). - * Only returns new time if same_sign is true - */ - int pick_time( int cur_time, int new_time, bool same_sign ) const; + std::vector points; + std::array covered; + std::array hot; + std::array cold; + + // Mutability is required for lazy initialization + mutable int level; + mutable bool level_is_valid; + + bool took_prozac; + bool stylish; + int super_fancy_bonus; }; #endif diff --git a/src/morale_types.h b/src/morale_types.h new file mode 100644 index 0000000000000..c307f8dcdef43 --- /dev/null +++ b/src/morale_types.h @@ -0,0 +1,71 @@ +#ifndef MORALE_TYPES_H +#define MORALE_TYPES_H + +enum morale_type : int { + MORALE_NULL = 0, + MORALE_FOOD_GOOD, + MORALE_FOOD_HOT, + MORALE_MUSIC, + MORALE_HONEY, + MORALE_GAME, + MORALE_MARLOSS, + MORALE_MUTAGEN, + MORALE_FEELING_GOOD, + MORALE_SUPPORT, + MORALE_PHOTOS, + + MORALE_CRAVING_NICOTINE, + MORALE_CRAVING_CAFFEINE, + MORALE_CRAVING_ALCOHOL, + MORALE_CRAVING_OPIATE, + MORALE_CRAVING_SPEED, + MORALE_CRAVING_COCAINE, + MORALE_CRAVING_CRACK, + MORALE_CRAVING_MUTAGEN, + MORALE_CRAVING_DIAZEPAM, + MORALE_CRAVING_MARLOSS, + + MORALE_FOOD_BAD, + MORALE_CANNIBAL, + MORALE_VEGETARIAN, + MORALE_MEATARIAN, + MORALE_ANTIFRUIT, + MORALE_LACTOSE, + MORALE_ANTIJUNK, + MORALE_ANTIWHEAT, + MORALE_NO_DIGEST, + MORALE_WET, + MORALE_DRIED_OFF, + MORALE_COLD, + MORALE_HOT, + MORALE_FEELING_BAD, + MORALE_KILLED_INNOCENT, + MORALE_KILLED_FRIEND, + MORALE_KILLED_MONSTER, + MORALE_MUTILATE_CORPSE, + MORALE_MUTAGEN_ELF, + MORALE_MUTAGEN_CHIMERA, + MORALE_MUTAGEN_MUTATION, + + MORALE_MOODSWING, + MORALE_BOOK, + MORALE_COMFY, + + MORALE_SCREAM, + + MORALE_PERM_MASOCHIST, + MORALE_PERM_HOARDER, + MORALE_PERM_FANCY, + MORALE_PERM_OPTIMIST, + MORALE_PERM_BADTEMPER, + MORALE_PERM_CONSTRAINED, + MORALE_GAME_FOUND_KITTEN, + + MORALE_HAIRCUT, + MORALE_SHAVE, + + NUM_MORALE_TYPES +}; + +#endif + diff --git a/src/mutation.cpp b/src/mutation.cpp index b82c68f6eabe7..757db583af559 100644 --- a/src/mutation.cpp +++ b/src/mutation.cpp @@ -281,6 +281,8 @@ void Character::mutation_effect(std::string mut) } return true; } ); + + on_mutation_gain( mut ); } void Character::mutation_loss_effect(std::string mut) @@ -341,6 +343,8 @@ void Character::mutation_loss_effect(std::string mut) } else { apply_mods(mut, false); } + + on_mutation_loss( mut ); } bool Character::has_active_mutation(const std::string & b) const diff --git a/src/newcharacter.cpp b/src/newcharacter.cpp index 88921bf4faa8b..f9d50886770e6 100644 --- a/src/newcharacter.cpp +++ b/src/newcharacter.cpp @@ -2381,9 +2381,13 @@ std::vector Character::get_mutations() const void Character::empty_traits() { + for( auto &mut : my_mutations ) { + on_mutation_loss( mut.first ); + } my_traits.clear(); my_mutations.clear(); } + void Character::empty_skills() { for( auto &skill : Skill::skills ) { diff --git a/src/npc.cpp b/src/npc.cpp index eee1fc71efa17..cc0664a1bfd60 100644 --- a/src/npc.cpp +++ b/src/npc.cpp @@ -16,7 +16,7 @@ #include "mission.h" #include "json.h" #include "sounds.h" -#include "morale.h" +#include "morale_types.h" #include "overmap.h" #include "vehicle.h" #include "mtype.h" diff --git a/src/npctalk.cpp b/src/npctalk.cpp index 280ae254c6c19..2a77db58a1030 100644 --- a/src/npctalk.cpp +++ b/src/npctalk.cpp @@ -9,7 +9,7 @@ #include "catacharset.h" #include "messages.h" #include "mission.h" -#include "morale.h" +#include "morale_types.h" #include "ammo.h" #include "overmapbuffer.h" #include "json.h" diff --git a/src/player.cpp b/src/player.cpp index 264679bf00001..ba318669f4dfd 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -29,7 +29,7 @@ #include "sounds.h" #include "item_action.h" #include "mongroup.h" -#include "morale.h" +#include "morale_types.h" #include "input.h" #include "veh_type.h" #include "overmap.h" @@ -211,8 +211,6 @@ player::player() : Character() last_batch = 0; lastconsumed = itype_id("null"); next_expected_position = tripoint_min; - morale_level = 0; - morale_level_is_valid = false; empty_traits(); @@ -541,16 +539,7 @@ void player::action_taken() void player::update_morale() { - const auto proceed = []( morale_point &m ) { m.proceed(); }; - const auto is_expired = []( const morale_point &m ) -> bool { return m.is_expired(); }; - - std::for_each( morale.begin(), morale.end(), proceed ); - const auto new_end = std::remove_if( morale.begin(), morale.end(), is_expired ); - morale.erase( new_end, morale.end() ); - // We reapply persistent morale effects after every decay step, to keep them fresh. - apply_persistent_morale(); - // And invalidate the morale level to recalculate it on demand - invalidate_morale_level(); + morale.decay( 1 ); } void player::apply_persistent_morale() @@ -575,59 +564,6 @@ void player::apply_persistent_morale() } } - // The stylish get a morale bonus for each body part covered in an item - // with the FANCY or SUPER_FANCY tag. - if( has_trait("STYLISH") ) { - int bonus = 0; - std::string basic_flag = "FANCY"; - std::string bonus_flag = "SUPER_FANCY"; - - std::bitset covered; // body parts covered - for( auto &elem : worn ) { - if( elem.has_flag( basic_flag ) || elem.has_flag( bonus_flag ) ) { - covered |= elem.get_covered_body_parts(); - } - if( elem.has_flag( bonus_flag ) ) { - bonus+=2; - } else if( elem.has_flag( basic_flag ) ) { - if( ( covered & elem.get_covered_body_parts() ).none() ) { - bonus += 1; - } - } - } - if(covered.test(bp_torso)) { - bonus += 6; - } - if(covered.test(bp_leg_l) || covered.test(bp_leg_r)) { - bonus += 2; - } - if(covered.test(bp_foot_l) || covered.test(bp_foot_r)) { - bonus += 1; - } - if(covered.test(bp_hand_l) || covered.test(bp_hand_r)) { - bonus += 1; - } - if(covered.test(bp_head)) { - bonus += 3; - } - if(covered.test(bp_eyes)) { - bonus += 2; - } - if(covered.test(bp_arm_l) || covered.test(bp_arm_r)) { - bonus += 1; - } - if(covered.test(bp_mouth)) { - bonus += 2; - } - - if(bonus > 20) - bonus = 20; - - if(bonus) { - add_morale(MORALE_PERM_FANCY, bonus, bonus, 5, 5, true); - } - } - // Floral folks really don't like having their flowers covered. if( has_trait("FLOWERS") && wearing_something_on(bp_head) ) { add_morale(MORALE_PERM_CONSTRAINED, -10, -10, 5, 5, true); @@ -656,17 +592,6 @@ void player::apply_persistent_morale() add_morale(MORALE_PERM_MASOCHIST, bonus, bonus, 5, 5, true); } } - - // Optimist gives a base +4 to morale. - // The +25% boost from optimist also applies here, for a net of +5. - if (has_trait("OPTIMISTIC")) { - add_morale(MORALE_PERM_OPTIMIST, 4, 4, 5, 5, true); - } - - // And Bad Temper works just the same way. But in reverse. ): - if (has_trait("BADTEMPER")) { - add_morale(MORALE_PERM_BADTEMPER, -4, -4, 5, 5, true); - } } void player::update_mental_focus() @@ -843,8 +768,6 @@ void player::update_bodytemp() // Temperature norms // Ambient normal temperature is lower while asleep const int ambient_norm = has_sleep ? 3100 : 1900; - // This gets incremented in the for loop and used in the morale calculation - int morale_pen = 0; /** * Calculations that affect all body parts equally go here, not in the loop @@ -1149,33 +1072,6 @@ void player::update_bodytemp() remove_effect( effect_hot, (body_part)i ); } } - // MORALE : a negative morale_pen means the player is cold - // Intensity multiplier is negative for cold, positive for hot - if( has_effect( effect_cold, (body_part)i ) || has_effect( effect_hot, (body_part)i ) ) { - int cold_int = get_effect_int( effect_cold, (body_part)i ); - int hot_int = get_effect_int( effect_hot, (body_part)i ); - int intensity_mult = hot_int - cold_int; - - switch (i) { - case bp_head: - case bp_torso: - case bp_mouth: - morale_pen += 2 * intensity_mult; - break; - case bp_arm_l: - case bp_arm_r: - case bp_leg_l: - case bp_leg_r: - morale_pen += .5 * intensity_mult; - break; - case bp_hand_l: - case bp_hand_r: - case bp_foot_l: - case bp_foot_r: - morale_pen += .5 * intensity_mult; - break; - } - } // FROSTBITE - only occurs to hands, feet, face /** @@ -1317,13 +1213,6 @@ void player::update_bodytemp() add_msg(m_bad, _("Your clothing is not providing enough protection from the wind for your %s!"), body_part_name(body_part(i)).c_str()); } } - // Morale penalties, updated at the same rate morale is - if( morale_pen < 0 && calendar::once_every(MINUTES(1)) ) { - add_morale(MORALE_COLD, -2, -abs(morale_pen), 10, 5, true); - } - if( morale_pen > 0 && calendar::once_every(MINUTES(1)) ) { - add_morale(MORALE_HOT, -2, -abs(morale_pen), 10, 5, true); - } } bool player::can_use_floor_warmth() const @@ -3400,72 +3289,7 @@ Strength - 4; Dexterity - 4; Intelligence - 4; Perception - 4")); void player::disp_morale() { - // Ensure the player's persistent morale effects are up-to-date. - apply_persistent_morale(); - - // Create and draw the window itself. - WINDOW *w = newwin(FULL_SCREEN_HEIGHT, FULL_SCREEN_WIDTH, - (TERMY > FULL_SCREEN_HEIGHT) ? (TERMY-FULL_SCREEN_HEIGHT)/2 : 0, - (TERMX > FULL_SCREEN_WIDTH) ? (TERMX-FULL_SCREEN_WIDTH)/2 : 0); - draw_border(w); - - // Figure out how wide the name column needs to be. - int name_column_width = 18; - for (auto &i : morale) { - int length = utf8_width( i.get_name() ); - if ( length > name_column_width) { - name_column_width = length; - } - } - - // If it's too wide, truncate. - if (name_column_width > 72) { - name_column_width = 72; - } - - // Start printing the number right after the name column. - // We'll right-justify it later. - int number_pos = name_column_width + 1; - - // Header - mvwprintz(w, 1, 1, c_white, _("Morale Modifiers:")); - mvwprintz(w, 2, 1, c_ltgray, _("Name")); - mvwprintz(w, 2, name_column_width+2, c_ltgray, _("Value")); - - const morale_mult mult = get_traits_mult(); - // Print out the morale entries. - for (size_t i = 0; i < morale.size(); i++) - { - std::string name = morale[i].get_name(); - int bonus = morale[i].get_net_bonus( mult ); - - // Print out the name. - trim_and_print(w, i + 3, 1, name_column_width, (bonus < 0 ? c_red : c_green), name.c_str()); - - // Print out the number, right-justified. - mvwprintz(w, i + 3, number_pos, (bonus < 0 ? c_red : c_green), - "% 6d", bonus); - } - - // Print out the total morale, right-justified. - int mor = get_morale_level(); - mvwprintz(w, 20, 1, (mor < 0 ? c_red : c_green), _("Total:")); - mvwprintz(w, 20, number_pos, (mor < 0 ? c_red : c_green), "% 6d", mor); - - // Print out the focus gain rate, right-justified. - double gain = (calc_focus_equilibrium() - focus_pool) / 100.0; - mvwprintz(w, 22, 1, (gain < 0 ? c_red : c_green), _("Focus gain:")); - mvwprintz(w, 22, number_pos-3, (gain < 0 ? c_red : c_green), _("%6.2f per minute"), gain); - - // Make sure the changes are shown. - wrefresh(w); - - // Wait for any keystroke. - getch(); - - // Close the window. - werase(w); - delwin(w); + morale.display( ( calc_focus_equilibrium() - focus_pool ) / 100.0 ); } int player::print_aim_bars( WINDOW *w, int line_number, item *weapon, Creature *target, int predicted_recoil ) { @@ -8891,101 +8715,26 @@ void player::update_body_wetness( const w_point &weather ) // TODO: Make clothing slow down drying } -morale_mult player::get_traits_mult() const -{ - morale_mult ret; - - if( has_trait( "OPTIMISTIC" ) ) { - ret *= morale_mults::optimistic; - } - - if( has_trait( "BADTEMPER" ) ) { - ret *= morale_mults::badtemper; - } - - return ret; -} - -morale_mult player::get_effects_mult() const -{ - morale_mult ret; - - //TODO: Maybe add something here to cheer you up as well? - if( has_effect( effect_took_prozac ) ) { - ret *= morale_mults::prozac; - } - - return ret; -} - int player::get_morale_level() const { - if ( !morale_level_is_valid ) { - const morale_mult mult = get_traits_mult(); - - morale_level = 0; - for( auto &i : morale ) { - morale_level += i.get_net_bonus( mult ); - } - - morale_level *= get_effects_mult(); - morale_level_is_valid = true; - } - - return morale_level; -} - -void player::invalidate_morale_level() -{ - morale_level_is_valid = false; + return morale.get_level(); } void player::add_morale(morale_type type, int bonus, int max_bonus, int duration, int decay_start, bool capped, const itype* item_type) { - // Search for a matching morale entry. - for( auto &i : morale ) { - if( i.get_type() == type && i.get_item_type() == item_type ) { - const int prev_bonus = i.get_bonus(); - - i.add( bonus, max_bonus, duration, decay_start, capped ); - if ( i.get_bonus() != prev_bonus ) { - invalidate_morale_level(); - } - return; - } - } - - morale_point new_morale( type, item_type, bonus, duration, decay_start ); - - if( !new_morale.is_expired() ) { - morale.push_back( new_morale ); - invalidate_morale_level(); - } + morale.add( type, bonus, max_bonus, duration, decay_start, capped, item_type ); } int player::has_morale( morale_type type ) const { - for( auto &elem : morale ) { - if( elem.get_type() == type ) { - return elem.get_bonus(); - } - } - return 0; + return morale.has( type ); } void player::rem_morale(morale_type type, const itype* item_type) { - for( size_t i = 0; i < morale.size(); ++i ) { - if( morale[i].get_type() == type && morale[i].get_item_type() == item_type ) { - if ( morale[i].get_bonus() ) { - invalidate_morale_level(); - } - morale.erase( morale.begin() + i ); - break; - } - } + morale.remove( type, item_type ); } bool player::has_morale_to_read() const @@ -10524,6 +10273,7 @@ bool player::wear_item( const item &to_wear, bool interactive ) add_msg_if_player( m_info, _( "You're deafened!" ) ); } } else { + on_item_wear( to_wear ); add_msg_if_npc( _(" puts on their %s."), to_wear.tname().c_str() ); } @@ -13826,6 +13576,31 @@ bool player::has_items_with_quality( const std::string &quality_id, int level, i return amount <= 0; } +void player::on_mutation_gain( const std::string &mid ) +{ + morale.on_mutation_gain( mid ); +} + +void player::on_mutation_loss( const std::string &mid ) +{ + morale.on_mutation_loss( mid ); +} + +void player::on_item_wear( const item &it ) +{ + morale.on_item_wear( it ); +} + +void player::on_item_takeoff( const item &it ) +{ + morale.on_item_takeoff( it ); +} + +void player::on_effect_int_change( const efftype_id &eid, int intensity, body_part bp ) +{ + morale.on_effect_int_change( eid, intensity, bp ); +} + void player::on_mission_assignment( mission &new_mission ) { active_missions.push_back( &new_mission ); diff --git a/src/player.h b/src/player.h index 952a1ea54b07c..06a074942bc5f 100644 --- a/src/player.h +++ b/src/player.h @@ -24,7 +24,6 @@ class mission; class profession; nc_color encumb_color(int level); enum morale_type : int; -class morale_point; enum game_message_type : int; class ma_technique; class martialart; @@ -906,7 +905,6 @@ class player : public Character, public JsonSerializer, public JsonDeserializer void cancel_activity(); int get_morale_level() const; // Modified by traits, &c - void invalidate_morale_level(); void add_morale( morale_type type, int bonus, int max_bonus = 0, int duration = 60, int decay_start = 30, bool capped = false, const itype *item_type = nullptr ); int has_morale( morale_type type ) const; @@ -1144,7 +1142,7 @@ class player : public Character, public JsonSerializer, public JsonDeserializer std::array drench_capacity; std::array body_wetness; - std::vector morale; + player_morale morale; int focus_pool; @@ -1265,6 +1263,26 @@ class player : public Character, public JsonSerializer, public JsonDeserializer * Check @ref mission::failed to see which case it is. */ void on_mission_finished( mission &mission ); + /** + * Called when a mutation is gained + */ + virtual void on_mutation_gain( const std::string &mid ) override; + /** + * Called when a mutation is lost + */ + virtual void on_mutation_loss( const std::string &mid ) override; + /** + * Called when an item is worn + */ + virtual void on_item_wear( const item &it ) override; + /** + * Called when an item is taken off + */ + virtual void on_item_takeoff( const item &it ) override; + /** + * Called when effect intensity has been changed + */ + virtual void on_effect_int_change( const efftype_id &eid, int intensity, body_part bp = num_bp ) override; // formats and prints encumbrance info to specified window void print_encumbrance( WINDOW * win, int line = -1, item *selected_limb = nullptr ) const; @@ -1284,15 +1302,6 @@ class player : public Character, public JsonSerializer, public JsonDeserializer void load(JsonObject &jsin); private: - // Mutability is required for lazy initialization - mutable int morale_level; - mutable bool morale_level_is_valid; - - /** Returns current traits multiplier for morale */ - morale_mult get_traits_mult() const; - /** Returns current effects multiplier for morale */ - morale_mult get_effects_mult() const; - // Items the player has identified. std::unordered_set items_identified; /** Check if an area-of-effect technique has valid targets */ diff --git a/src/savegame_json.cpp b/src/savegame_json.cpp index 9c95f14059895..0e77e85b4df8d 100644 --- a/src/savegame_json.cpp +++ b/src/savegame_json.cpp @@ -292,6 +292,7 @@ void Character::load(JsonObject &data) for( auto it = my_mutations.begin(); it != my_mutations.end(); ) { const auto &mid = it->first; if( mutation_branch::has( mid ) ) { + on_mutation_gain( mid ); ++it; } else { debugmsg( "character %s has invalid mutation %s, it will be ignored", name.c_str(), mid.c_str() ); @@ -303,6 +304,9 @@ void Character::load(JsonObject &data) worn.clear(); data.read( "worn", worn ); + for( auto &w : worn ) { + on_item_wear( w ); + } if( !data.read( "hp_cur", hp_cur ) ) { debugmsg("Error, incompatible hp_cur in save file '%s'", parray.str().c_str()); @@ -612,8 +616,7 @@ void player::serialize(JsonOut &json) const // Player only, books they have read at least once. json.member( "items_identified", items_identified ); - // :( - json.member( "morale", morale ); + morale.store( json ); // mission stuff json.member("active_mission", active_mission == nullptr ? -1 : active_mission->get_id() ); @@ -742,9 +745,7 @@ void player::deserialize(JsonIn &jsin) items_identified.clear(); data.read( "items_identified", items_identified ); - morale.clear(); - data.read( "morale", morale ); - invalidate_morale_level(); + morale.load( data ); int tmpactive_mission; if( data.read( "active_mission", tmpactive_mission ) && tmpactive_mission != -1 ) { @@ -1948,7 +1949,11 @@ void Creature::load( JsonObject &jsin ) if ( !(std::istringstream(i.first) >> key_num) ) { key_num = 0; } - effects[id][(body_part)key_num] = i.second; + const body_part bp = static_cast( key_num ); + effect &e = i.second; + + effects[id][bp] = e; + on_effect_int_change( id, e.get_intensity(), bp ); } } } @@ -1984,7 +1989,7 @@ void Creature::load( JsonObject &jsin ) fake = false; // see Creature::load } -void morale_point::deserialize( JsonIn &jsin ) +void player_morale::morale_point::deserialize( JsonIn &jsin ) { JsonObject jo = jsin.get_object(); type = static_cast( jo.get_int( "type_enum" ) ); @@ -1998,7 +2003,7 @@ void morale_point::deserialize( JsonIn &jsin ) jo.read( "age", age ); } -void morale_point::serialize( JsonOut &json ) const +void player_morale::morale_point::serialize( JsonOut &json ) const { json.start_object(); json.member( "type_enum", static_cast( type ) ); @@ -2011,3 +2016,13 @@ void morale_point::serialize( JsonOut &json ) const json.member( "age", age ); json.end_object(); } + +void player_morale::store( JsonOut &jsout ) const +{ + jsout.member( "morale", points ); +} + +void player_morale::load( JsonObject &jsin ) +{ + jsin.read( "morale", points ); +} diff --git a/src/veh_interact.cpp b/src/veh_interact.cpp index d4a6ab383f885..ef818848673ca 100644 --- a/src/veh_interact.cpp +++ b/src/veh_interact.cpp @@ -13,7 +13,6 @@ #include "debug.h" #include "messages.h" #include "translations.h" -#include "morale.h" #include "veh_type.h" #include "ui.h" #include "itype.h" diff --git a/tests/morale_test.cpp b/tests/morale_test.cpp new file mode 100644 index 0000000000000..4ecab2e8cbbc0 --- /dev/null +++ b/tests/morale_test.cpp @@ -0,0 +1,366 @@ +#include "catch/catch.hpp" + +#include "morale.h" +#include "morale_types.h" +#include "effect.h" +#include "game.h" +#include "itype.h" +#include "item.h" +#include "bodypart.h" + +#include + +TEST_CASE( "player_morale" ) +{ + static const efftype_id effect_cold( "cold" ); + static const efftype_id effect_hot( "hot" ); + static const efftype_id effect_took_prozac( "took_prozac" ); + + player_morale m; + + GIVEN( "an empty morale" ) { + CHECK( m.get_level() == 0 ); + } + + GIVEN( "temporary morale (food)" ) { + m.add( MORALE_FOOD_GOOD, 20, 40, 20, 10 ); + m.add( MORALE_FOOD_BAD, -10, -20, 20, 10 ); + + CHECK( m.has( MORALE_FOOD_GOOD ) == 20 ); + CHECK( m.has( MORALE_FOOD_BAD ) == -10 ); + CHECK( m.get_level() == 10 ); + + WHEN( "it decays" ) { + AND_WHEN( "it's just started" ) { + m.decay( 10 ); + CHECK( m.has( MORALE_FOOD_GOOD ) == 20 ); + CHECK( m.has( MORALE_FOOD_BAD ) == -10 ); + CHECK( m.get_level() == 10 ); + } + AND_WHEN( "it's halfway there" ) { + m.decay( 15 ); + CHECK( m.has( MORALE_FOOD_GOOD ) == 10 ); + CHECK( m.has( MORALE_FOOD_BAD ) == -5 ); + CHECK( m.get_level() == 5 ); + } + AND_WHEN( "it's finished" ) { + m.decay( 20 ); + CHECK( m.has( MORALE_FOOD_GOOD ) == 0 ); + CHECK( m.has( MORALE_FOOD_BAD ) == 0 ); + CHECK( m.get_level() == 0 ); + } + } + + WHEN( "it gets deleted" ) { + AND_WHEN( "good one gets deleted" ) { + m.remove( MORALE_FOOD_GOOD ); + CHECK( m.get_level() == -10 ); + } + AND_WHEN( "bad one gets deleted" ) { + m.remove( MORALE_FOOD_BAD ); + CHECK( m.get_level() == 20 ); + } + AND_WHEN( "both get deleted" ) { + m.remove( MORALE_FOOD_BAD ); + m.remove( MORALE_FOOD_GOOD ); + CHECK( m.get_level() == 0 ); + } + } + + WHEN( "it gets cleared" ) { + m.clear(); + CHECK( m.get_level() == 0 ); + } + + WHEN( "it's added/subtracted (no cap)" ) { + m.add( MORALE_FOOD_GOOD, 10, 40, 20, 10, false ); + m.add( MORALE_FOOD_BAD, -10, -20, 20, 10, false ); + + CHECK( m.has( MORALE_FOOD_GOOD ) == 30 ); + CHECK( m.has( MORALE_FOOD_BAD ) == -20 ); + CHECK( m.get_level() == 10 ); + + } + + WHEN( "it's added/subtracted (with a cap)" ) { + m.add( MORALE_FOOD_GOOD, 5, 10, 20, 10, true ); + m.add( MORALE_FOOD_BAD, -5, -10, 20, 10, true ); + + CHECK( m.has( MORALE_FOOD_GOOD ) == 10 ); + CHECK( m.has( MORALE_FOOD_BAD ) == -10 ); + CHECK( m.get_level() == 0 ); + } + } + + GIVEN( "persistent morale" ) { + m.add_permanent( MORALE_PERM_MASOCHIST, 5 ); + + CHECK( m.has( MORALE_PERM_MASOCHIST ) == 5 ); + + WHEN( "it decays" ) { + m.decay( 100 ); + THEN( "nothing happens" ) { + CHECK( m.has( MORALE_PERM_MASOCHIST ) == 5 ); + CHECK( m.get_level() == 5 ); + } + } + } + + GIVEN( "OPTIMISTIC trait" ) { + m.on_mutation_gain( "OPTIMISTIC" ); + CHECK( m.has( MORALE_PERM_OPTIMIST ) == 4 ); + CHECK( m.get_level() == 5 ); + + WHEN( "lost the trait" ) { + m.on_mutation_loss( "OPTIMISTIC" ); + CHECK( m.has( MORALE_PERM_OPTIMIST ) == 0 ); + CHECK( m.get_level() == 0 ); + } + } + + GIVEN( "BADTEMPER trait" ) { + m.on_mutation_gain( "BADTEMPER" ); + CHECK( m.has( MORALE_PERM_BADTEMPER ) == -4 ); + CHECK( m.get_level() == -5 ); + + WHEN( "lost the trait" ) { + m.on_mutation_loss( "BADTEMPER" ); + CHECK( m.has( MORALE_PERM_BADTEMPER ) == 0 ); + CHECK( m.get_level() == 0 ); + } + } + + GIVEN( "killed an innocent" ) { + m.add( MORALE_KILLED_INNOCENT, -100 ); + + WHEN( "took prozac" ) { + m.on_effect_int_change( effect_took_prozac, 1 ); + + THEN( "it's not so bad" ) { + CHECK( m.get_level() == -25 ); + + AND_WHEN( "the effect ends" ) { + m.on_effect_int_change( effect_took_prozac, 0 ); + + THEN( "guilt returns" ) { + CHECK( m.get_level() == -100 ); + } + } + } + } + } + + GIVEN( "a set of super fancy bride's clothes" ) { + item dress_wedding( "dress_wedding", 0 ); // legs, torso | 8 + 2 | 10 + item veil_wedding( "veil_wedding", 0 ); // eyes, mouth | 4 + 2 | 6 + item heels( "heels", 0 ); // feet | 1 + 2 | 3 + + m.on_item_wear( dress_wedding ); + m.on_item_wear( veil_wedding ); + m.on_item_wear( heels ); + + WHEN( "not a stylish person" ) { + THEN( "just don't care (even if man)" ) { + CHECK( m.get_level() == 0 ); + } + } + + WHEN( "a stylish person" ) { + m.on_mutation_gain( "STYLISH" ); + + CHECK( m.get_level() == 19 ); + + AND_WHEN( "gets naked" ) { + m.on_item_takeoff( heels ); // the queen took off her sandal ... + CHECK( m.get_level() == 16 ); + m.on_item_takeoff( veil_wedding ); + CHECK( m.get_level() == 10 ); + m.on_item_takeoff( dress_wedding ); + CHECK( m.get_level() == 0 ); + } + AND_WHEN( "tries to be even fancier" ) { + item watch( "sf_watch", 0 ); + m.on_item_wear( watch ); + THEN( "there's a limit" ) { + CHECK( m.get_level() == 20 ); + } + } + AND_WHEN( "not anymore" ) { + m.on_mutation_loss( "STYLISH" ); + CHECK( m.get_level() == 0 ); + } + } + } + + GIVEN( "tough temperature conditions" ) { + WHEN( "chilly" ) { + m.on_effect_int_change( effect_cold, 1, bp_torso ); + m.on_effect_int_change( effect_cold, 1, bp_head ); + m.on_effect_int_change( effect_cold, 1, bp_eyes ); + m.on_effect_int_change( effect_cold, 1, bp_mouth ); + m.on_effect_int_change( effect_cold, 1, bp_arm_l ); + m.on_effect_int_change( effect_cold, 1, bp_arm_r ); + m.on_effect_int_change( effect_cold, 1, bp_leg_l ); + m.on_effect_int_change( effect_cold, 1, bp_leg_r ); + m.on_effect_int_change( effect_cold, 1, bp_hand_l ); + m.on_effect_int_change( effect_cold, 1, bp_hand_r ); + m.on_effect_int_change( effect_cold, 1, bp_foot_l ); + m.on_effect_int_change( effect_cold, 1, bp_foot_r ); + + AND_WHEN( "no time has passed" ) { + CHECK( m.get_level() == 0 ); + } + AND_WHEN( "1 minute has passed" ) { + m.decay( 1 ); + CHECK( m.get_level() == -2 ); + } + AND_WHEN( "2 minutes have passed" ) { + m.decay( 2 ); + CHECK( m.get_level() == -4 ); + } + AND_WHEN( "3 minutes have passed" ) { + m.decay( 3 ); + CHECK( m.get_level() == -6 ); + } + AND_WHEN( "an hour has passed" ) { + m.decay( 60 ); + CHECK( m.get_level() == -6 ); + } + } + + WHEN( "cold" ) { + m.on_effect_int_change( effect_cold, 2, bp_torso ); + m.on_effect_int_change( effect_cold, 2, bp_head ); + m.on_effect_int_change( effect_cold, 2, bp_eyes ); + m.on_effect_int_change( effect_cold, 2, bp_mouth ); + m.on_effect_int_change( effect_cold, 2, bp_arm_l ); + m.on_effect_int_change( effect_cold, 2, bp_arm_r ); + m.on_effect_int_change( effect_cold, 2, bp_leg_l ); + m.on_effect_int_change( effect_cold, 2, bp_leg_r ); + m.on_effect_int_change( effect_cold, 2, bp_hand_l ); + m.on_effect_int_change( effect_cold, 2, bp_hand_r ); + m.on_effect_int_change( effect_cold, 2, bp_foot_l ); + m.on_effect_int_change( effect_cold, 2, bp_foot_r ); + + AND_WHEN( "no time has passed" ) { + CHECK( m.get_level() == 0 ); + } + AND_WHEN( "1 minute has passed" ) { + m.decay( 1 ); + CHECK( m.get_level() == -2 ); + } + AND_WHEN( "9 minutes have passed" ) { + m.decay( 9 ); + CHECK( m.get_level() == -18 ); + } + AND_WHEN( "10 minutes have passed" ) { + m.decay( 10 ); + CHECK( m.get_level() == -20 ); + } + AND_WHEN( "an hour has passed" ) { + m.decay( 60 ); + CHECK( m.get_level() == -20 ); + } + } + + WHEN( "warm" ) { + m.on_effect_int_change( effect_hot, 1, bp_torso ); + m.on_effect_int_change( effect_hot, 1, bp_head ); + m.on_effect_int_change( effect_hot, 1, bp_eyes ); + m.on_effect_int_change( effect_hot, 1, bp_mouth ); + m.on_effect_int_change( effect_hot, 1, bp_arm_l ); + m.on_effect_int_change( effect_hot, 1, bp_arm_r ); + m.on_effect_int_change( effect_hot, 1, bp_leg_l ); + m.on_effect_int_change( effect_hot, 1, bp_leg_r ); + m.on_effect_int_change( effect_hot, 1, bp_hand_l ); + m.on_effect_int_change( effect_hot, 1, bp_hand_r ); + m.on_effect_int_change( effect_hot, 1, bp_foot_l ); + m.on_effect_int_change( effect_hot, 1, bp_foot_r ); + + AND_WHEN( "no time has passed" ) { + CHECK( m.get_level() == 0 ); + } + AND_WHEN( "1 minute has passed" ) { + m.decay( 1 ); + CHECK( m.get_level() == -2 ); + } + AND_WHEN( "2 minutes have passed" ) { + m.decay( 2 ); + CHECK( m.get_level() == -4 ); + } + AND_WHEN( "3 minutes have passed" ) { + m.decay( 3 ); + CHECK( m.get_level() == -6 ); + } + AND_WHEN( "an hour has passed" ) { + m.decay( 60 ); + CHECK( m.get_level() == -6 ); + } + } + + WHEN( "hot" ) { + m.on_effect_int_change( effect_hot, 2, bp_torso ); + m.on_effect_int_change( effect_hot, 2, bp_head ); + m.on_effect_int_change( effect_hot, 2, bp_eyes ); + m.on_effect_int_change( effect_hot, 2, bp_mouth ); + m.on_effect_int_change( effect_hot, 2, bp_arm_l ); + m.on_effect_int_change( effect_hot, 2, bp_arm_r ); + m.on_effect_int_change( effect_hot, 2, bp_leg_l ); + m.on_effect_int_change( effect_hot, 2, bp_leg_r ); + m.on_effect_int_change( effect_hot, 2, bp_hand_l ); + m.on_effect_int_change( effect_hot, 2, bp_hand_r ); + m.on_effect_int_change( effect_hot, 2, bp_foot_l ); + m.on_effect_int_change( effect_hot, 2, bp_foot_r ); + + AND_WHEN( "no time has passed" ) { + CHECK( m.get_level() == 0 ); + } + AND_WHEN( "1 minute has passed" ) { + m.decay( 1 ); + CHECK( m.get_level() == -2 ); + } + AND_WHEN( "9 minutes have passed" ) { + m.decay( 9 ); + CHECK( m.get_level() == -18 ); + } + AND_WHEN( "10 minutes have passed" ) { + m.decay( 10 ); + CHECK( m.get_level() == -20 ); + } + AND_WHEN( "an hour has passed" ) { + m.decay( 60 ); + CHECK( m.get_level() == -20 ); + } + } + + WHEN( "mixed" ) { + // TODO: Awfully low penalty for such conditions. Something has to be done about that. + // I think the penalties should be calculated independently for 'hot' and 'cold' effects. + m.on_effect_int_change( effect_hot, 3, bp_torso ); + m.on_effect_int_change( effect_cold, 2, bp_head ); + m.on_effect_int_change( effect_cold, 3, bp_mouth ); + m.on_effect_int_change( effect_cold, 3, bp_hand_l ); + m.on_effect_int_change( effect_hot, 1, bp_leg_r ); + + AND_WHEN( "no time has passed" ) { + CHECK( m.get_level() == 0 ); + } + AND_WHEN( "1 minute has passed" ) { + m.decay( 1 ); + CHECK( m.get_level() == -2 ); + } + AND_WHEN( "2 minutes have passed" ) { + m.decay( 2 ); + CHECK( m.get_level() == -4 ); + } + AND_WHEN( "3 minutes have passed" ) { + m.decay( 10 ); + CHECK( m.get_level() == -5 ); + } + AND_WHEN( "an hour has passed" ) { + m.decay( 60 ); + CHECK( m.get_level() == -5 ); + } + } + } +}