diff --git a/Docs/Luadoc/Lua.xml b/Docs/Luadoc/Lua.xml
index 6a8a2ef7a8..c07f6b9ba3 100644
--- a/Docs/Luadoc/Lua.xml
+++ b/Docs/Luadoc/Lua.xml
@@ -1147,8 +1147,12 @@
+
+
+
+
@@ -1163,6 +1167,8 @@
+
+
@@ -1190,15 +1196,21 @@
+
+
+
+
+
+
diff --git a/Docs/Luadoc/LuaDocumentation.xml b/Docs/Luadoc/LuaDocumentation.xml
index 628696206d..3c397cd957 100644
--- a/Docs/Luadoc/LuaDocumentation.xml
+++ b/Docs/Luadoc/LuaDocumentation.xml
@@ -3284,12 +3284,24 @@ save yourself some time, copy this for undocumented things:
+
+ Adds cals to the daily total.
+
Adds a screenshot entry to the profile. filename must be the full path of the screenshot, as returned by SaveScreenshot.
+
+ Calculates the number of calories burned based on the heart rate (in beats per minute), the duration (in seconds), and data in the profile.
+
+
+ Returns the age.
+
Returns a table of all high score names that have been used on this profile.
+
+ Returns the birth year.
+
Returns the number of calories burned during the current day.
@@ -3326,6 +3338,12 @@ save yourself some time, copy this for undocumented things:
Returns the GUID of this Profile.
+
+ Returns whether this profile ignores the step count based calorie calculation.
+
+
+ Returns whether this profile uses the male formula when CalculateCaloriesFromHeartRate is used.
+
Gets the profile's HighScoreList for a specified Song and Steps. (Alternate arguments for Courses: Course c, Trail t)
@@ -3405,6 +3423,9 @@ save yourself some time, copy this for undocumented things:
Returns the user table for this Profile.
+
+ Returns the VO2 max for this profile.
+
Returns how much the player weighs.
@@ -3414,9 +3435,15 @@ save yourself some time, copy this for undocumented things:
Returns true if the specified code sUnlockEntryID is unlocked.
+
+ Sets the birth year of the profile.
+
Sets the current for the Profile.
+
+ Sets the display name of the profile to name.
+
Sets the goal to iCals calories.
@@ -3426,9 +3453,18 @@ save yourself some time, copy this for undocumented things:
Sets the current goal type to gt.
+
+ Sets whether this profile ignores the step count based calorie counting.
+
+
+ Sets whether this profile uses the male formula when CalculateCaloriesFromHeartRate is used.
+
Sets last used high score name.
+
+ Sets the VO2 max for the profile. 0 is treated as unset.
+
Sets how much the player weighs (in pounds) to weightPounds.
diff --git a/Docs/Themerdocs/calories.txt b/Docs/Themerdocs/calories.txt
new file mode 100644
index 0000000000..bea235fdfb
--- /dev/null
+++ b/Docs/Themerdocs/calories.txt
@@ -0,0 +1,50 @@
+SM5 has two systems for calculating the number of calories burned during a song.
+
+The first system simply uses the weight of the player and adds a small amount for every step. This does not require any specific support from the theme and is the default for new profiles.
+
+
+The second system uses age, gender, weight, heart rate, and song duration to calculate the amount used during a song. This requires a theme to support it, and the relevant parts of Editable.ini to be set for the profile.
+
+Editable.ini fields:
+BirthYear, -- defaults to 1995
+IgnoreStepCountCalories, -- Must be set to 1 to use the second system.
+IsMale, -- Defaults to 1. Set to 0 for females.
+Voomax, -- VO2max. Optional. Calorie calculation is more accurate if it is set, but it is not necessary. 0 means unset. http://www.shapesense.com/fitness-exercise/calculators/vo2max-calculator.aspx is a site for estimating V02max, and the source of the equations used.
+WeightPounds -- Weight in pounds.
+
+Lua API functions: (names only, details are in Docs/Luadoc/Lua.xml)
+GetWeightPounds()
+SetWeightPounds()
+GetVoomax()
+SetVoomax()
+GetAge()
+GetBirthYear()
+SetBirthYear()
+GetIgnoreStepCountCalories()
+SetIgnoreStepCountCalories()
+GetIsMale()
+SetIsMale()
+AddCaloriesToDailyTotal()
+CalculateCaloriesFromHeartRate()
+
+Intended usage:
+A theme that supports heart rate calorie calculation should take the following steps:
+1. Create a new screen that the player will be able to use to enter their heart rate. It shall be called "ScreenHeartEntry" in this list, though a theme can name it anything.
+2. During ScreenGameplay, track the amount of time that the player is hitting steps for. This duration must be in seconds.
+3. When proceeding from ScreenGameplay:
+3a. Check whether GetIgnoreStepCountCalories returns true for either player's profile.
+3b. If it returns true for either player, go to ScreenHeartEntry instead of ScreenEvaluationNormal.
+3c. This means changing the NextScreen metric for ScreenGameplay to use a function that checks the profiles and returns the correct screen.
+4. On ScreenHeartEntry:
+4a. Provide a timer that the players can watch while measuring their pulse.
+4b. Provide a way for the players to enter their heart rate in beats per minute.
+4c. When a player enters their heart rate, use Profile:CalculateCaloriesFromHeartRate().
+4d. Do not add calories to a profile if Profile:IgnoreStepCountCalories() returns false for that profile.
+4e. Pass the heart rate and the duration from ScreenGameplay to Profile:CalculateCaloriesFromHeartRate() and store its return value.
+4f. Pass the return value of Profile:CalculateCaloriesFromHeartRate() to Profile:AddCaloriesToDailyTotal() to add them to the total for that player's profile.
+4g. When all active players have finished entering their heart rate, proceed to ScreenEvaluationNormal.
+5. This concludes the special steps required for theme support of heart rate calorie calculation.
+
+Side notes:
+CalculateCaloriesFromHeartRate and AddCaloriesToDailyTotal are separate functions so that a theme can display or use the calorie amount in other ways easily.
+If IgnoreStepCountCalories is true for a profile, but the theme in use does not provide support using the steps listed above, that profile will not accumulate calories.
diff --git a/src/Profile.cpp b/src/Profile.cpp
index ff0d808392..c6f72606a2 100644
--- a/src/Profile.cpp
+++ b/src/Profile.cpp
@@ -51,7 +51,8 @@ static ThemeMetric UNLOCK_AUTH_STRING( "Profile", "UnlockAuthString" );
* 5 /* HighScores per Steps */ \
* 1024 /* size in bytes of a HighScores XNode */
-const unsigned int DEFAULT_WEIGHT_POUNDS = 120;
+const int DEFAULT_WEIGHT_POUNDS = 120;
+const float DEFAULT_BIRTH_YEAR= 1995;
#if defined(_MSC_VER)
#pragma warning (disable : 4706) // assignment within conditional expression
@@ -85,6 +86,10 @@ void Profile::InitEditableData()
m_sCharacterID = "";
m_sLastUsedHighScoreName = "";
m_iWeightPounds = 0;
+ m_Voomax= 0;
+ m_BirthYear= 0;
+ m_IgnoreStepCountCalories= false;
+ m_IsMale= true;
}
void Profile::ClearStats()
@@ -221,6 +226,15 @@ int Profile::GetCalculatedWeightPounds() const
return m_iWeightPounds;
}
+int Profile::GetAge() const
+{
+ if(m_BirthYear == 0)
+ {
+ return (GetLocalTime().tm_year+1900) - DEFAULT_BIRTH_YEAR;
+ }
+ return (GetLocalTime().tm_year+1900) - m_BirthYear;
+}
+
RString Profile::GetDisplayTotalCaloriesBurned() const
{
return FormatCalories( m_fTotalCaloriesBurned );
@@ -954,6 +968,10 @@ ProfileLoadResult Profile::LoadStatsXmlFromNode( const XNode *xml, bool bIgnoreE
RString sCharacterID = m_sCharacterID;
RString sLastUsedHighScoreName = m_sLastUsedHighScoreName;
int iWeightPounds = m_iWeightPounds;
+ float Voomax= m_Voomax;
+ int BirthYear= m_BirthYear;
+ bool IgnoreStepCountCalories= m_IgnoreStepCountCalories;
+ bool IsMale= m_IsMale;
LOAD_NODE( GeneralData );
LOAD_NODE( SongScores );
@@ -968,6 +986,10 @@ ProfileLoadResult Profile::LoadStatsXmlFromNode( const XNode *xml, bool bIgnoreE
m_sCharacterID = sCharacterID;
m_sLastUsedHighScoreName = sLastUsedHighScoreName;
m_iWeightPounds = iWeightPounds;
+ m_Voomax= Voomax;
+ m_BirthYear= BirthYear;
+ m_IgnoreStepCountCalories= IgnoreStepCountCalories;
+ m_IsMale= IsMale;
}
return ProfileLoadResult_Success;
@@ -1095,6 +1117,10 @@ void Profile::SaveEditableDataToDir( RString sDir ) const
ini.SetValue( "Editable", "CharacterID", m_sCharacterID );
ini.SetValue( "Editable", "LastUsedHighScoreName", m_sLastUsedHighScoreName );
ini.SetValue( "Editable", "WeightPounds", m_iWeightPounds );
+ ini.SetValue( "Editable", "Voomax", m_Voomax );
+ ini.SetValue( "Editable", "BirthYear", m_BirthYear );
+ ini.SetValue( "Editable", "IgnoreStepCountCalories", m_IgnoreStepCountCalories );
+ ini.SetValue( "Editable", "IsMale", m_IsMale );
ini.WriteFile( sDir + EDITABLE_INI );
}
@@ -1110,8 +1136,12 @@ XNode* Profile::SaveGeneralDataCreateNode() const
pGeneralDataNode->AppendChild( "CharacterID", m_sCharacterID );
pGeneralDataNode->AppendChild( "LastUsedHighScoreName", m_sLastUsedHighScoreName );
pGeneralDataNode->AppendChild( "WeightPounds", m_iWeightPounds );
+ pGeneralDataNode->AppendChild( "Voomax", m_Voomax );
+ pGeneralDataNode->AppendChild( "BirthYear", m_BirthYear );
+ pGeneralDataNode->AppendChild( "IgnoreStepCountCalories", m_IgnoreStepCountCalories );
+ pGeneralDataNode->AppendChild( "IsMale", m_IsMale );
+
pGeneralDataNode->AppendChild( "IsMachine", IsMachine() );
- pGeneralDataNode->AppendChild( "IsWeightSet", m_iWeightPounds != 0 );
pGeneralDataNode->AppendChild( "Guid", m_sGuid );
pGeneralDataNode->AppendChild( "SortOrder", SortOrderToString(m_SortOrder) );
@@ -1274,6 +1304,10 @@ ProfileLoadResult Profile::LoadEditableDataFromDir( RString sDir )
ini.GetValue( "Editable", "CharacterID", m_sCharacterID );
ini.GetValue( "Editable", "LastUsedHighScoreName", m_sLastUsedHighScoreName );
ini.GetValue( "Editable", "WeightPounds", m_iWeightPounds );
+ ini.GetValue( "Editable", "Voomax", m_Voomax );
+ ini.GetValue( "Editable", "BirthYear", m_BirthYear );
+ ini.GetValue( "Editable", "IgnoreStepCountCalories", m_IgnoreStepCountCalories );
+ ini.GetValue( "Editable", "IsMale", m_IsMale );
// This is data that the user can change, so we have to validate it.
wstring wstr = RStringToWstring(m_sDisplayName);
@@ -1298,6 +1332,10 @@ void Profile::LoadGeneralDataFromNode( const XNode* pNode )
pNode->GetChildValue( "CharacterID", m_sCharacterID );
pNode->GetChildValue( "LastUsedHighScoreName", m_sLastUsedHighScoreName );
pNode->GetChildValue( "WeightPounds", m_iWeightPounds );
+ pNode->GetChildValue( "Voomax", m_Voomax );
+ pNode->GetChildValue( "BirthYear", m_BirthYear );
+ pNode->GetChildValue( "IgnoreStepCountCalories", m_IgnoreStepCountCalories );
+ pNode->GetChildValue( "IsMale", m_IsMale );
pNode->GetChildValue( "Guid", m_sGuid );
pNode->GetChildValue( "SortOrder", s ); m_SortOrder = StringToSortOrder( s );
pNode->GetChildValue( "LastDifficulty", s ); m_LastDifficulty = StringToDifficulty( s );
@@ -1451,10 +1489,94 @@ void Profile::AddStepTotals( int iTotalTapsAndHolds, int iTotalJumps, int iTotal
m_iTotalHands += iTotalHands;
m_iTotalLifts += iTotalLifts;
- m_fTotalCaloriesBurned += fCaloriesBurned;
+ if(!m_IgnoreStepCountCalories)
+ {
+ m_fTotalCaloriesBurned += fCaloriesBurned;
+ DateTime date = DateTime::GetNowDate();
+ m_mapDayToCaloriesBurned[date].fCals += fCaloriesBurned;
+ }
+}
+// It's a bit unclean to have this flag for routing around the old step count
+// based calorie calculation, but I can't think of a better way to do it.
+// AddStepTotals is called (through a couple layers) by CommitStageStats at
+// the end of ScreenGameplay, so it can't be moved to somewhere else. The
+// player can't put in their heart rate for calculation until after
+// ScreenGameplay
+void Profile::AddCaloriesToDailyTotal(float cals)
+{
+ m_fTotalCaloriesBurned += cals;
DateTime date = DateTime::GetNowDate();
- m_mapDayToCaloriesBurned[date].fCals += fCaloriesBurned;
+ m_mapDayToCaloriesBurned[date].fCals += cals;
+}
+
+float Profile::CalculateCaloriesFromHeartRate(float HeartRate, float Duration)
+{
+ // Copied from http://www.shapesense.com/fitness-exercise/calculators/heart-rate-based-calorie-burn-calculator.aspx
+ /*
+ Male: ((-55.0969 + (0.6309 x HR) + (0.1988 x W) + (0.2017 x A))/4.184) x T
+ Female: ((-20.4022 + (0.4472 x HR) - (0.1263 x W) + (0.074 x A))/4.184) x T
+ where
+
+ HR = Heart rate (in beats/minute)
+ W = Weight (in kilograms)
+ A = Age (in years)
+ T = Exercise duration time (in minutes)
+
+ Equations for Determination of Calorie Burn if VO2max is Known
+
+ Male: ((-95.7735 + (0.634 x HR) + (0.404 x VO2max) + (0.394 x W) + (0.271 x A))/4.184) x T
+ Female: ((-59.3954 + (0.45 x HR) + (0.380 x VO2max) + (0.103 x W) + (0.274 x A))/4.184) x T
+ where
+
+ HR = Heart rate (in beats/minute)
+ VO2max = Maximal oxygen consumption (in mL•kg-1•min-1)
+ W = Weight (in kilograms)
+ A = Age (in years)
+ T = Exercise duration time (in minutes)
+ */
+ // Duration passed in is in seconds. Convert it to minutes to make the code
+ // match the equations from the website.
+ Duration= Duration / 60.0;
+ float kilos= GetCalculatedWeightPounds() / 2.205;
+ float age= GetAge();
+
+ // Names for the constants in the equations.
+ // Assumes male and unknown voomax.
+ float gender_factor= -55.0969;
+ float heart_factor= 0.6309;
+ float voo_factor= 0;
+ float weight_factor= 0.1988;
+ float age_factor= 0.2017;
+ if(m_Voomax > 0)
+ {
+ if(m_IsMale)
+ {
+ gender_factor= -95.7735;
+ heart_factor= 0.634;
+ voo_factor= 0.404;
+ weight_factor= 0.394;
+ age_factor= 0.271;
+ }
+ else
+ {
+ gender_factor= -59.3954;
+ heart_factor= 0.45;
+ voo_factor= 0.380;
+ weight_factor= 0.103;
+ age_factor= 0.274;
+ }
+ }
+ else if(!m_IsMale)
+ {
+ gender_factor= -20.4022;
+ heart_factor= 0.6309;
+ weight_factor= 0.1988;
+ age_factor= 0.2017;
+ }
+ return ((gender_factor + (heart_factor * HeartRate) +
+ (voo_factor * m_Voomax) + (weight_factor * kilos) + (age_factor + age))
+ / 4.184) * Duration;
}
XNode* Profile::SaveSongScoresCreateNode() const
@@ -2031,6 +2153,11 @@ public:
}
static int GetDisplayName( T* p, lua_State *L ) { lua_pushstring(L, p->m_sDisplayName ); return 1; }
+ static int SetDisplayName( T* p, lua_State *L )
+ {
+ p->m_sDisplayName= SArg(1);
+ return 0;
+ }
static int GetLastUsedHighScoreName( T* p, lua_State *L ) { lua_pushstring(L, p->m_sLastUsedHighScoreName ); return 1; }
static int SetLastUsedHighScoreName( T* p, lua_State *L )
{
@@ -2127,6 +2254,37 @@ public:
static int SetCharacter( T* p, lua_State *L ) { p->SetCharacter(SArg(1)); return 0; }
static int GetWeightPounds( T* p, lua_State *L ) { lua_pushnumber(L, p->m_iWeightPounds ); return 1; }
static int SetWeightPounds( T* p, lua_State *L ) { p->m_iWeightPounds = IArg(1); return 0; }
+ DEFINE_METHOD(GetVoomax, m_Voomax);
+ DEFINE_METHOD(GetAge, GetAge());
+ DEFINE_METHOD(GetBirthYear, m_BirthYear);
+ DEFINE_METHOD(GetIgnoreStepCountCalories, m_IgnoreStepCountCalories);
+ DEFINE_METHOD(GetIsMale, m_IsMale);
+ static int SetVoomax( T* p, lua_State *L )
+ {
+ p->m_Voomax= FArg(1);
+ return 0;
+ }
+ static int SetBirthYear( T* p, lua_State *L )
+ {
+ p->m_BirthYear= IArg(1);
+ return 0;
+ }
+ static int SetIgnoreStepCountCalories( T* p, lua_State *L )
+ {
+ p->m_IgnoreStepCountCalories= BArg(1);
+ return 0;
+ }
+ static int SetIsMale( T* p, lua_State *L )
+ {
+ p->m_IsMale= BArg(1);
+ return 0;
+ }
+ static int AddCaloriesToDailyTotal( T* p, lua_State *L )
+ {
+ p->AddCaloriesToDailyTotal(FArg(1));
+ return 0;
+ }
+ DEFINE_METHOD(CalculateCaloriesFromHeartRate, CalculateCaloriesFromHeartRate(FArg(1), FArg(2)));
static int GetGoalType( T* p, lua_State *L ) { lua_pushnumber(L, p->m_GoalType ); return 1; }
static int SetGoalType( T* p, lua_State *L ) { p->m_GoalType = Enum::Check(L, 1); return 0; }
static int GetGoalCalories( T* p, lua_State *L ) { lua_pushnumber(L, p->m_iGoalCalories ); return 1; }
@@ -2216,6 +2374,7 @@ public:
{
ADD_METHOD( AddScreenshot );
ADD_METHOD( GetDisplayName );
+ ADD_METHOD( SetDisplayName );
ADD_METHOD( GetLastUsedHighScoreName );
ADD_METHOD( SetLastUsedHighScoreName );
ADD_METHOD( GetAllUsedHighScoreNames );
@@ -2226,6 +2385,17 @@ public:
ADD_METHOD( SetCharacter );
ADD_METHOD( GetWeightPounds );
ADD_METHOD( SetWeightPounds );
+ ADD_METHOD( GetVoomax );
+ ADD_METHOD( SetVoomax );
+ ADD_METHOD( GetAge );
+ ADD_METHOD( GetBirthYear );
+ ADD_METHOD( SetBirthYear );
+ ADD_METHOD( GetIgnoreStepCountCalories );
+ ADD_METHOD( SetIgnoreStepCountCalories );
+ ADD_METHOD( GetIsMale );
+ ADD_METHOD( SetIsMale );
+ ADD_METHOD( AddCaloriesToDailyTotal );
+ ADD_METHOD( CalculateCaloriesFromHeartRate );
ADD_METHOD( GetGoalType );
ADD_METHOD( SetGoalType );
ADD_METHOD( GetGoalCalories );
diff --git a/src/Profile.h b/src/Profile.h
index ca19b69415..bc3d55932f 100644
--- a/src/Profile.h
+++ b/src/Profile.h
@@ -73,6 +73,8 @@ public:
* Note: there are probably a lot of variables. */
Profile(): m_sDisplayName(""), m_sCharacterID(""),
m_sLastUsedHighScoreName(""), m_iWeightPounds(0),
+ m_Voomax(0), m_BirthYear(0), m_IgnoreStepCountCalories(false),
+ m_IsMale(true),
m_sGuid(MakeGuid()), m_sDefaultModifiers(),
m_SortOrder(SortOrder_Invalid),
m_LastDifficulty(Difficulty_Invalid),
@@ -120,6 +122,7 @@ public:
RString GetDisplayTotalCaloriesBurned() const; // remove me and use Lua instead
RString GetDisplayTotalCaloriesBurnedToday() const; // remove me and use Lua instead
int GetCalculatedWeightPounds() const; // returns a default value if m_iWeightPounds isn't set
+ int GetAge() const; // returns a default value if m_Age isn't set
float GetCaloriesBurnedToday() const;
int GetTotalNumSongsPassed() const;
int GetTotalStepsWithTopGrade( StepsType st, Difficulty d, Grade g ) const;
@@ -139,6 +142,8 @@ public:
void AddStepTotals( int iNumTapsAndHolds, int iNumJumps, int iNumHolds, int iNumRolls, int iNumMines,
int iNumHands, int iNumLifts, float fCaloriesBurned );
+ void AddCaloriesToDailyTotal(float cals);
+ float CalculateCaloriesFromHeartRate(float HeartRate, float Duration);
bool IsMachine() const;
@@ -152,6 +157,14 @@ public:
* so that it can be ready quickly. */
RString m_sLastUsedHighScoreName;
int m_iWeightPounds; // 0 == not set
+ // Voomax and BirthYear are used for calculating calories from heart rate.
+ float m_Voomax; // 0 == not set
+ int m_BirthYear; // 0 == not set
+ // m_IgnoreStepCountCalories is so that the step count based calorie
+ // counter can be ignored in favor of calculating calories from heart rate
+ // and voomax.
+ bool m_IgnoreStepCountCalories;
+ bool m_IsMale; // Used solely for calculating calories from heart rate.
//RString m_sProfileImageName; // todo: add a default image -aj
// General data