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 );
+ }
+ }
+ }
+}