From 853af1ea69de38020479a4e27d4aed600fcb9ee0 Mon Sep 17 00:00:00 2001 From: AJ Kelly Date: Fri, 6 Jul 2012 23:12:37 -0500 Subject: [PATCH] [PlayerStageStats] Added GetSongsPassed and GetSongsPlayed Lua bindings. --- Docs/Changelog_sm5.txt | 1 + src/PlayerStageStats.cpp | 1684 +++++++++++++++++++------------------- 2 files changed, 845 insertions(+), 840 deletions(-) diff --git a/Docs/Changelog_sm5.txt b/Docs/Changelog_sm5.txt index f733ebb2d8..ba856b65f0 100644 --- a/Docs/Changelog_sm5.txt +++ b/Docs/Changelog_sm5.txt @@ -11,6 +11,7 @@ StepMania 5.0 ???? ?? | 20120??? 2012/07/06 ---------- * [ScoreDisplayRave] Add FrameBase and FrameOver to the metrics. [AJ] +* [PlayerStageStats] Added GetSongsPassed and GetSongsPlayed Lua bindings. [AJ] 2012/07/05 ---------- diff --git a/src/PlayerStageStats.cpp b/src/PlayerStageStats.cpp index 7b7be7168b..b18628594f 100644 --- a/src/PlayerStageStats.cpp +++ b/src/PlayerStageStats.cpp @@ -1,840 +1,844 @@ -#include "global.h" -#include "PlayerStageStats.h" -#include "RageLog.h" -#include "ThemeManager.h" -#include "Foreach.h" -#include "LuaManager.h" -#include -#include "GameState.h" -#include "Course.h" -#include "Steps.h" -#include "ScoreKeeperNormal.h" -#include "PrefsManager.h" -#include "CommonMetrics.h" - -#define GRADE_PERCENT_TIER(i) THEME->GetMetricF("PlayerStageStats",ssprintf("GradePercent%s",GradeToString((Grade)i).c_str())) -// deprecated, but no solution to replace them exists yet: -#define GRADE_TIER02_IS_ALL_W2S THEME->GetMetricB("PlayerStageStats","GradeTier02IsAllW2s") -#define GRADE_TIER01_IS_ALL_W2S THEME->GetMetricB("PlayerStageStats","GradeTier01IsAllW2s") -#define GRADE_TIER02_IS_FULL_COMBO THEME->GetMetricB("PlayerStageStats","GradeTier02IsFullCombo") - -static ThemeMetric g_MinScoreToMaintainCombo( "Gameplay", "MinScoreToMaintainCombo" ); -static ThemeMetric g_MineHitIncrementsMissCombo( "Gameplay", "MineHitIncrementsMissCombo" ); - -const float LESSON_PASS_THRESHOLD = 0.8f; - -Grade GetGradeFromPercent( float fPercent ); - -void PlayerStageStats::Init() -{ - m_bPlayerCanAchieveFullCombo = true; - m_bJoined = false; - m_vpPossibleSteps.clear(); - m_iStepsPlayed = 0; - m_fAliveSeconds = 0; - m_bFailed = false; - m_iPossibleDancePoints = 0; - m_iCurPossibleDancePoints = 0; - m_iActualDancePoints = 0; - m_iPossibleGradePoints = 0; - m_iCurCombo = 0; - m_iMaxCombo = 0; - m_iCurMissCombo = 0; - m_iCurScoreMultiplier = 1; - m_iScore = 0; - m_iMaxScore = 0; - m_iCurMaxScore = 0; - m_iSongsPassed = 0; - m_iSongsPlayed = 0; - m_fLifeRemainingSeconds = 0; - m_iNumControllerSteps = 0; - m_fCaloriesBurned = 0; - - ZERO( m_iTapNoteScores ); - ZERO( m_iHoldNoteScores ); - m_radarPossible.Zero(); - m_radarActual.Zero(); - - m_fFirstSecond = FLT_MAX; - m_fLastSecond = 0; - - m_StageAward = StageAward_Invalid; - m_PeakComboAward = PeakComboAward_Invalid; - m_iPersonalHighScoreIndex = -1; - m_iMachineHighScoreIndex = -1; - m_bDisqualified = false; - m_rc = RankingCategory_Invalid; - m_HighScore = HighScore(); -} - -void PlayerStageStats::AddStats( const PlayerStageStats& other ) -{ - m_bJoined = other.m_bJoined; - FOREACH_CONST( Steps*, other.m_vpPossibleSteps, s ) - m_vpPossibleSteps.push_back( *s ); - m_iStepsPlayed += other.m_iStepsPlayed; - m_fAliveSeconds += other.m_fAliveSeconds; - m_bFailed |= other.m_bFailed; - m_iPossibleDancePoints += other.m_iPossibleDancePoints; - m_iActualDancePoints += other.m_iActualDancePoints; - m_iCurPossibleDancePoints += other.m_iCurPossibleDancePoints; - m_iPossibleGradePoints += other.m_iPossibleGradePoints; - - for( int t=0; t::const_iterator it; - for( it = other.m_fLifeRecord.begin(); it != other.m_fLifeRecord.end(); ++it ) - { - const float pos = it->first; - const float life = it->second; - m_fLifeRecord[fOtherFirstSecond+pos] = life; - } - - for( unsigned i=0; i 0.001 ) - continue; - - // These are really the same combo. - prevcombo.m_fSizeSeconds += combo.m_fSizeSeconds; - prevcombo.m_cnt += combo.m_cnt; - m_ComboList.erase( m_ComboList.begin()+i ); - --i; - } -} - -Grade GetGradeFromPercent( float fPercent ) -{ - Grade grade = Grade_Failed; - - FOREACH_ENUM( Grade,g) - { - if( fPercent >= GRADE_PERCENT_TIER(g) ) - { - grade = g; - break; - } - } - return grade; -} - -Grade PlayerStageStats::GetGrade() const -{ - if( m_bFailed ) - return Grade_Failed; - - /* XXX: This entire calculation should be in ScoreKeeper, but final evaluation - * is tricky since at that point the ScoreKeepers no longer exist. */ - float fActual = 0; - - bool bIsBeginner = false; - if( m_iStepsPlayed > 0 && !GAMESTATE->IsCourseMode() ) - bIsBeginner = m_vpPossibleSteps[0]->GetDifficulty() == Difficulty_Beginner; - - FOREACH_ENUM( TapNoteScore, tns ) - { - int iTapScoreValue = ScoreKeeperNormal::TapNoteScoreToGradePoints( tns, bIsBeginner ); - fActual += m_iTapNoteScores[tns] * iTapScoreValue; - //LOG->Trace( "GetGrade actual: %i * %i", m_iTapNoteScores[tns], iTapScoreValue ); - } - - FOREACH_ENUM( HoldNoteScore, hns ) - { - int iHoldScoreValue = ScoreKeeperNormal::HoldNoteScoreToGradePoints( hns, bIsBeginner ); - fActual += m_iHoldNoteScores[hns] * iHoldScoreValue; - //LOG->Trace( "GetGrade actual: %i * %i", m_iHoldNoteScores[hns], iHoldScoreValue ); - } - - //LOG->Trace( "GetGrade: fActual: %f, fPossible: %d", fActual, m_iPossibleGradePoints ); - - float fPercent = (m_iPossibleGradePoints == 0) ? 0 : fActual / m_iPossibleGradePoints; - - Grade grade = GetGradeFromPercent( fPercent ); - - //LOG->Trace( "GetGrade: Grade: %s, %i", GradeToString(grade).c_str(), GRADE_TIER02_IS_ALL_W2S ); - - // TODO: Change these conditions to use Lua instead. -aj - if( GRADE_TIER02_IS_ALL_W2S ) - { - if( FullComboOfScore(TNS_W1) ) - return Grade_Tier01; - - if( FullComboOfScore(TNS_W2) ) - return Grade_Tier02; - - grade = max( grade, Grade_Tier03 ); - } - - if( GRADE_TIER01_IS_ALL_W2S ) - { - if( FullComboOfScore(TNS_W2) ) - return Grade_Tier01; - grade = max( grade, Grade_Tier02 ); - } - - if( GRADE_TIER02_IS_FULL_COMBO ) - { - if( FullComboOfScore(g_MinScoreToMaintainCombo) ) - return Grade_Tier02; - grade = max( grade, Grade_Tier03 ); - } - - return grade; -} - -float PlayerStageStats::MakePercentScore( int iActual, int iPossible ) -{ - if( iPossible == 0 ) - return 0; // div/0 - - if( iActual == iPossible ) - return 1; // correct for rounding error - - // This can happen in battle, with transform attacks. - //ASSERT_M( iActual <= iPossible, ssprintf("%i/%i", iActual, iPossible) ); - - float fPercent = iActual / (float)iPossible; - - // don't allow negative - fPercent = max( 0, fPercent ); - - int iPercentTotalDigits = 3 + CommonMetrics::PERCENT_SCORE_DECIMAL_PLACES; // "100" + "." + "00" - - // TRICKY: printf will round, but we want to truncate. Otherwise, we may display - // a percent score that's too high and doesn't match up with the calculated grade. - float fTruncInterval = powf( 0.1f, (float)iPercentTotalDigits-1 ); - - // TRICKY: ftruncf is rounding 1.0000000 to 0.99990004. Give a little boost - // to fPercentDancePoints to correct for this. - fPercent += 0.000001f; - - fPercent = ftruncf( fPercent, fTruncInterval ); - return fPercent; -} - -RString PlayerStageStats::FormatPercentScore( float fPercentDancePoints ) -{ - int iPercentTotalDigits = 3 + CommonMetrics::PERCENT_SCORE_DECIMAL_PLACES; // "100" + "." + "00" - - RString s = ssprintf( "%*.*f%%", iPercentTotalDigits, - (int)CommonMetrics::PERCENT_SCORE_DECIMAL_PLACES, - fPercentDancePoints*100 ); - return s; -} - -float PlayerStageStats::GetPercentDancePoints() const -{ - return MakePercentScore( m_iActualDancePoints, m_iPossibleDancePoints ); -} - -float PlayerStageStats::GetCurMaxPercentDancePoints() const -{ - if ( m_iPossibleDancePoints == 0 ) - return 0; // div/0 - - if ( m_iCurPossibleDancePoints == m_iPossibleDancePoints ) - return 1; // correct for rounding error - - float fCurMaxPercentDancePoints = m_iCurPossibleDancePoints / (float)m_iPossibleDancePoints; - - return fCurMaxPercentDancePoints; -} - -// TODO: Make this use lua. Let more judgments be possible. -Wolfman2000 -int PlayerStageStats::GetLessonScoreActual() const -{ - int iScore = 0; - - FOREACH_ENUM( TapNoteScore, tns ) - { - switch( tns ) - { - case TNS_AvoidMine: - case TNS_W5: - case TNS_W4: - case TNS_W3: - case TNS_W2: - case TNS_W1: - iScore += m_iTapNoteScores[tns]; - default: - break; - } - } - - FOREACH_ENUM( HoldNoteScore, hns ) - { - switch( hns ) - { - case HNS_Held: - iScore += m_iHoldNoteScores[hns]; - default: - break; - } - } - - return iScore; -} - -int PlayerStageStats::GetLessonScoreNeeded() const -{ - float fScore = 0; - - FOREACH_CONST( Steps*, m_vpPossibleSteps, steps ) - fScore += (*steps)->GetRadarValues( PLAYER_1 ).m_Values.v.fNumTapsAndHolds; - - return lrintf( fScore * LESSON_PASS_THRESHOLD ); -} - -void PlayerStageStats::ResetScoreForLesson() -{ - m_iCurPossibleDancePoints = 0; - m_iActualDancePoints = 0; - FOREACH_ENUM( TapNoteScore, tns ) - m_iTapNoteScores[tns] = 0; - FOREACH_ENUM( HoldNoteScore, hns ) - m_iHoldNoteScores[hns] = 0; - m_iCurCombo = 0; - m_iMaxCombo = 0; - m_iCurMissCombo = 0; - m_iScore = 0; - m_iCurMaxScore = 0; - m_iMaxScore = 0; -} - -void PlayerStageStats::SetLifeRecordAt( float fLife, float fStepsSecond ) -{ - // Don't save life stats in endless courses, or could run OOM in a few hours. - if( GAMESTATE->m_pCurCourse && GAMESTATE->m_pCurCourse->IsEndless() ) - return; - - if( fStepsSecond < 0 ) - return; - - m_fFirstSecond = min( fStepsSecond, m_fFirstSecond ); - m_fLastSecond = max( fStepsSecond, m_fLastSecond ); - //LOG->Trace( "fLastSecond = %f", m_fLastSecond ); - - // fSecond will always be greater than any value already in the map. - m_fLifeRecord[fStepsSecond] = fLife; - - MESSAGEMAN->Broadcast( Message_LifeMeterChangedP1 ); - - // Memory optimization: - // If we have three consecutive records A, B, and C all with the same fLife, - // we can eliminate record B without losing data. Only check the last three - // records in the map since we're only inserting at the end, and we know all - // earlier redundant records have already been removed. - map::iterator C = m_fLifeRecord.end(); - --C; - if( C == m_fLifeRecord.begin() ) // no earlier records left - return; - - map::iterator B = C; - --B; - if( B == m_fLifeRecord.begin() ) // no earlier records left - return; - - map::iterator A = B; - --A; - - if( A->second == B->second && B->second == C->second ) - m_fLifeRecord.erase(B); -} - -float PlayerStageStats::GetLifeRecordAt( float fStepsSecond ) const -{ - if( m_fLifeRecord.empty() ) - return 0; - - // Find the first element whose key is greater than k. - map::const_iterator it = m_fLifeRecord.upper_bound( fStepsSecond ); - - // Find the last element whose key is less than or equal to k. - if( it != m_fLifeRecord.begin() ) - --it; - - return it->second; - -} - -float PlayerStageStats::GetLifeRecordLerpAt( float fStepsSecond ) const -{ - if( m_fLifeRecord.empty() ) - return 0; - - // Find the first element whose key is greater than k. - map::const_iterator later = m_fLifeRecord.upper_bound( fStepsSecond ); - - // Find the last element whose key is less than or equal to k. - map::const_iterator earlier = later; - if( earlier != m_fLifeRecord.begin() ) - --earlier; - - if( later == m_fLifeRecord.end() ) - return earlier->second; - - if( earlier->first == later->first ) // two samples from the same time. Don't divide by zero in SCALE - return earlier->second; - - // earlier <= pos <= later - return SCALE( fStepsSecond, earlier->first, later->first, earlier->second, later->second ); -} - -void PlayerStageStats::GetLifeRecord( float *fLifeOut, int iNumSamples, float fStepsEndSecond ) const -{ - for( int i = 0; i < iNumSamples; ++i ) - { - float from = SCALE( i, 0, (float)iNumSamples, 0.0f, fStepsEndSecond ); - fLifeOut[i] = GetLifeRecordLerpAt( from ); - } -} - -float PlayerStageStats::GetCurrentLife() const -{ - if( m_fLifeRecord.empty() ) - return 0; - map::const_iterator iter = m_fLifeRecord.end(); - --iter; - return iter->second; -} - -/* If bRollover is true, we're being called before gameplay begins, so we can - * record the amount of the first combo that comes from the previous song. */ -void PlayerStageStats::UpdateComboList( float fSecond, bool bRollover ) -{ - // Don't save combo stats in endless courses, or could run OOM in a few hours. - if( GAMESTATE->m_pCurCourse && GAMESTATE->m_pCurCourse->IsEndless() ) - return; - - if( fSecond < 0 ) - return; - - if( !bRollover ) - { - m_fFirstSecond = min( fSecond, m_fFirstSecond ); - m_fLastSecond = max( fSecond, m_fLastSecond ); - //LOG->Trace( "fLastSecond = %f", fLastSecond ); - } - - int cnt = m_iCurCombo; - if( !cnt ) - return; // no combo - - if( m_ComboList.size() == 0 || m_ComboList.back().m_cnt >= cnt ) - { - /* If the previous combo (if any) starts on -9999, then we rolled over - * some combo, but missed the first step. Remove it. */ - if( m_ComboList.size() && m_ComboList.back().m_fStartSecond == -9999 ) - m_ComboList.erase( m_ComboList.begin()+m_ComboList.size()-1, m_ComboList.end() ); - - // This is a new combo. - Combo_t NewCombo; - /* "start" is the position that the combo started within this song. - * If we're recording rollover, the combo hasn't started yet (within - * this song), so put a placeholder in and set it on the next call. - * (Otherwise, start will be less than fFirstPos.) */ - if( bRollover ) - NewCombo.m_fStartSecond = -9999; - else - NewCombo.m_fStartSecond = fSecond; - m_ComboList.push_back( NewCombo ); - } - - Combo_t &combo = m_ComboList.back(); - if( !bRollover && combo.m_fStartSecond == -9999 ) - combo.m_fStartSecond = 0; - - combo.m_fSizeSeconds = fSecond - combo.m_fStartSecond; - combo.m_cnt = cnt; - - if( bRollover ) - combo.m_rollover = cnt; -} - -/* This returns the largest combo contained within the song, as if - * m_bComboContinuesBetweenSongs is turned off. */ -PlayerStageStats::Combo_t PlayerStageStats::GetMaxCombo() const -{ - if( m_ComboList.size() == 0 ) - return Combo_t(); - - int m = 0; - for( unsigned i = 1; i < m_ComboList.size(); ++i ) - { - if( m_ComboList[i].m_cnt > m_ComboList[m].m_cnt ) - m = i; - } - - return m_ComboList[m]; -} - -int PlayerStageStats::GetComboAtStartOfStage() const -{ - if( m_ComboList.empty() ) - return 0; - else - return m_ComboList[0].m_rollover; -} - -bool PlayerStageStats::FullComboOfScore( TapNoteScore tnsAllGreaterOrEqual ) const -{ - ASSERT( tnsAllGreaterOrEqual >= TNS_W5 ); - ASSERT( tnsAllGreaterOrEqual <= TNS_W1 ); - - //if we've set MissCombo to anything besides 0, it's not a full combo - if( !m_bPlayerCanAchieveFullCombo ) - return false; - - // If missed any holds, then it's not a full combo - if( m_iHoldNoteScores[HNS_LetGo] > 0 ) - return false; - - //if any checkpoints were missed, it's not a full combo either - if( m_iTapNoteScores[TNS_CheckpointMiss] > 0 ) - return false; - - // If has any of the judgments below, then not a full combo - for( int i=TNS_Miss; i 0 ) - return false; - } - - // hit any mines when they increment the miss combo? It's not a full combo. - if( g_MineHitIncrementsMissCombo && m_iTapNoteScores[TNS_HitMine] > 0 ) - return false; - - // If has at least one of the judgments equal to or above, then is a full combo. - for( int i=tnsAllGreaterOrEqual; i 0 ) - return true; - } - - return false; -} - -TapNoteScore PlayerStageStats::GetBestFullComboTapNoteScore() const -{ - // Optimization opportunity: ... - // (seriously? -aj) - for( TapNoteScore i=TNS_W1; i >= TNS_W5; enum_add(i,-1)) - { - if( FullComboOfScore(i) ) - return i; - } - return TapNoteScore_Invalid; -} - -bool PlayerStageStats::SingleDigitsOfScore( TapNoteScore tnsAllGreaterOrEqual ) const -{ - return FullComboOfScore( tnsAllGreaterOrEqual ) && - m_iTapNoteScores[tnsAllGreaterOrEqual] < 10; -} - -bool PlayerStageStats::OneOfScore( TapNoteScore tnsAllGreaterOrEqual ) const -{ - return FullComboOfScore( tnsAllGreaterOrEqual ) && - m_iTapNoteScores[tnsAllGreaterOrEqual] == 1; -} - -int PlayerStageStats::GetTotalTaps() const -{ - int iTotalTaps = 0; - for( int i=TNS_Miss; iTrace( "hand out awards" ); - - m_PeakComboAward = PeakComboAward_Invalid; - - if( bGaveUp || bUsedAutoplay ) - return; - - deque &vPdas = GAMESTATE->m_vLastStageAwards[p]; - - //LOG->Trace( "per difficulty awards" ); - - // per-difficulty awards - // don't give per-difficutly awards if using easy mods - if( !IsDisqualified() ) - { - if( FullComboOfScore( TNS_W3 ) ) - vPdas.push_back( StageAward_FullComboW3 ); - if( SingleDigitsOfScore( TNS_W3 ) ) - vPdas.push_back( StageAward_SingleDigitW3 ); - if( FullComboOfScore( TNS_W2 ) ) - vPdas.push_back( StageAward_FullComboW2 ); - if( SingleDigitsOfScore( TNS_W2 ) ) - vPdas.push_back( StageAward_SingleDigitW2 ); - if( FullComboOfScore( TNS_W1 ) ) - vPdas.push_back( StageAward_FullComboW1 ); - - if( OneOfScore( TNS_W3 ) ) - vPdas.push_back( StageAward_OneW3 ); - if( OneOfScore( TNS_W2 ) ) - vPdas.push_back( StageAward_OneW2 ); - - float fPercentW3s = GetPercentageOfTaps( TNS_W3 ); - if( fPercentW3s >= 0.8f ) - vPdas.push_back( StageAward_80PercentW3 ); - if( fPercentW3s >= 0.9f ) - vPdas.push_back( StageAward_90PercentW3 ); - if( fPercentW3s >= 1.f ) - vPdas.push_back( StageAward_100PercentW3 ); - } - - // Max one PDA per stage - if( !vPdas.empty() ) - vPdas.erase( vPdas.begin(), vPdas.end()-1 ); - - if( !vPdas.empty() ) - m_StageAward = vPdas.back(); - else - m_StageAward = StageAward_Invalid; - - //LOG->Trace( "done with per difficulty awards" ); - - // DO give peak combo awards if using easy mods - int iComboAtStartOfStage = GetComboAtStartOfStage(); - int iPeakCombo = GetMaxCombo().m_cnt; - - FOREACH_ENUM( PeakComboAward,pca ) - { - int iLevel = 1000 * (pca+1); - bool bCrossedLevel = iComboAtStartOfStage < iLevel && iPeakCombo >= iLevel; - //LOG->Trace( "pca = %d, iLevel = %d, bCrossedLevel = %d", pca, iLevel, bCrossedLevel ); - if( bCrossedLevel ) - GAMESTATE->m_vLastPeakComboAwards[p].push_back( pca ); - } - - if( !GAMESTATE->m_vLastPeakComboAwards[p].empty() ) - m_PeakComboAward = GAMESTATE->m_vLastPeakComboAwards[p].back(); - else - m_PeakComboAward = PeakComboAward_Invalid; - - //LOG->Trace( "done with per combo awards" ); - -} - -bool PlayerStageStats::IsDisqualified() const -{ - if( !PREFSMAN->m_bDisqualification ) - return false; - return m_bDisqualified; -} - -LuaFunction( GetGradeFromPercent, GetGradeFromPercent( FArg(1) ) ) -LuaFunction( FormatPercentScore, PlayerStageStats::FormatPercentScore( FArg(1) ) ) - - -// lua start -#include "LuaBinding.h" - -/** @brief Allow Lua to have access to the PlayerStageStats. */ -class LunaPlayerStageStats: public Luna -{ -public: - DEFINE_METHOD( GetCaloriesBurned, m_fCaloriesBurned ) - DEFINE_METHOD( GetNumControllerSteps, m_iNumControllerSteps ) - DEFINE_METHOD( GetLifeRemainingSeconds, m_fLifeRemainingSeconds ) - DEFINE_METHOD( GetSurvivalSeconds, GetSurvivalSeconds() ) - DEFINE_METHOD( GetCurrentCombo, m_iCurCombo ) - DEFINE_METHOD( GetCurrentMissCombo, m_iCurMissCombo ) - DEFINE_METHOD( GetCurrentScoreMultiplier, m_iCurScoreMultiplier ) - DEFINE_METHOD( GetScore, m_iScore ) - DEFINE_METHOD( GetCurMaxScore, m_iCurMaxScore ) - DEFINE_METHOD( GetTapNoteScores, m_iTapNoteScores[Enum::Check(L, 1)] ) - DEFINE_METHOD( GetHoldNoteScores, m_iHoldNoteScores[Enum::Check(L, 1)] ) - DEFINE_METHOD( FullCombo, FullCombo() ) - DEFINE_METHOD( FullComboOfScore, FullComboOfScore( Enum::Check(L, 1) ) ) - DEFINE_METHOD( MaxCombo, GetMaxCombo().m_cnt ) - DEFINE_METHOD( GetCurrentLife, GetCurrentLife() ) - DEFINE_METHOD( GetGrade, GetGrade() ) - DEFINE_METHOD( GetActualDancePoints, m_iActualDancePoints ) - DEFINE_METHOD( GetPossibleDancePoints, m_iPossibleDancePoints ) - DEFINE_METHOD( GetCurrentPossibleDancePoints, m_iCurPossibleDancePoints ) - DEFINE_METHOD( GetPercentDancePoints, GetPercentDancePoints() ) - DEFINE_METHOD( GetLessonScoreActual, GetLessonScoreActual() ) - DEFINE_METHOD( GetLessonScoreNeeded, GetLessonScoreNeeded() ) - DEFINE_METHOD( GetPersonalHighScoreIndex, m_iPersonalHighScoreIndex ) - DEFINE_METHOD( GetMachineHighScoreIndex, m_iMachineHighScoreIndex ) - DEFINE_METHOD( GetStageAward, m_StageAward ) - DEFINE_METHOD( GetPeakComboAward, m_PeakComboAward ) - DEFINE_METHOD( IsDisqualified, IsDisqualified() ) - DEFINE_METHOD( GetAliveSeconds, m_fAliveSeconds ) - DEFINE_METHOD( GetPercentageOfTaps, GetPercentageOfTaps( Enum::Check(L, 1) ) ) - DEFINE_METHOD( GetBestFullComboTapNoteScore, GetBestFullComboTapNoteScore() ) - DEFINE_METHOD( GetFailed, m_bFailed ) - - static int GetPlayedSteps( T* p, lua_State *L ) - { - lua_newtable(L); - for( int i = 0; i < (int) min(p->m_iStepsPlayed, (int) p->m_vpPossibleSteps.size()); ++i ) - { - p->m_vpPossibleSteps[i]->PushSelf(L); - lua_rawseti( L, -2, i+1 ); - } - return 1; - } - static int GetPossibleSteps( T* p, lua_State *L ) - { - lua_newtable(L); - for( int i = 0; i < (int) p->m_vpPossibleSteps.size(); ++i ) - { - p->m_vpPossibleSteps[i]->PushSelf(L); - lua_rawseti( L, -2, i+1 ); - } - return 1; - } - - static int GetRadarPossible( T* p, lua_State *L ) { p->m_radarPossible.PushSelf(L); return 1; } - static int GetRadarActual( T* p, lua_State *L ) { p->m_radarActual.PushSelf(L); return 1; } - static int SetScore( T* p, lua_State *L ) - { - if( IArg(1) >= 0 ) - { - p->m_iScore = IArg(1); - return 1; - } - return 0; - } - static int SetCurMaxScore( T* p, lua_State *L ) - { - if( IArg(1) >= 0 ) - { - p->m_iCurMaxScore = IArg(1); - return 1; - } - return 0; - } - - static int FailPlayer( T* p, lua_State *L ) - { - p->m_bFailed = true; - return 0; - } - - LunaPlayerStageStats() - { - ADD_METHOD( GetCaloriesBurned ); - ADD_METHOD( GetNumControllerSteps ); - ADD_METHOD( GetLifeRemainingSeconds ); - ADD_METHOD( GetSurvivalSeconds ); - ADD_METHOD( GetCurrentCombo ); - ADD_METHOD( GetCurrentMissCombo ); - ADD_METHOD( GetCurrentScoreMultiplier ); - ADD_METHOD( GetScore ); - ADD_METHOD( GetCurMaxScore ); - ADD_METHOD( GetTapNoteScores ); - ADD_METHOD( GetHoldNoteScores ); - ADD_METHOD( FullCombo ); - ADD_METHOD( FullComboOfScore ); - ADD_METHOD( MaxCombo ); - ADD_METHOD( GetCurrentLife ); - ADD_METHOD( GetGrade ); - ADD_METHOD( GetActualDancePoints ); - ADD_METHOD( GetPossibleDancePoints ); - ADD_METHOD( GetCurrentPossibleDancePoints ); - ADD_METHOD( GetPercentDancePoints ); - ADD_METHOD( GetLessonScoreActual ); - ADD_METHOD( GetLessonScoreNeeded ); - ADD_METHOD( GetPersonalHighScoreIndex ); - ADD_METHOD( GetMachineHighScoreIndex ); - ADD_METHOD( GetStageAward ); - ADD_METHOD( GetPeakComboAward ); - ADD_METHOD( IsDisqualified ); - ADD_METHOD( GetPlayedSteps ); - ADD_METHOD( GetPossibleSteps ); - ADD_METHOD( GetAliveSeconds ); - ADD_METHOD( GetPercentageOfTaps ); - ADD_METHOD( GetRadarActual ); - ADD_METHOD( GetRadarPossible ); - ADD_METHOD( GetBestFullComboTapNoteScore ); - ADD_METHOD( GetFailed ); - ADD_METHOD( SetScore ); - ADD_METHOD( GetCurMaxScore ); - ADD_METHOD( SetCurMaxScore ); - ADD_METHOD( FailPlayer ); - } -}; - -LUA_REGISTER_CLASS( PlayerStageStats ) -// lua end - -/* - * (c) 2001-2004 Chris Danford, Glenn Maynard - * All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, and/or sell copies of the Software, and to permit persons to - * whom the Software is furnished to do so, provided that the above - * copyright notice(s) and this permission notice appear in all copies of - * the Software and that both the above copyright notice(s) and this - * permission notice appear in supporting documentation. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF - * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS - * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT - * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS - * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. - */ +#include "global.h" +#include "PlayerStageStats.h" +#include "RageLog.h" +#include "ThemeManager.h" +#include "Foreach.h" +#include "LuaManager.h" +#include +#include "GameState.h" +#include "Course.h" +#include "Steps.h" +#include "ScoreKeeperNormal.h" +#include "PrefsManager.h" +#include "CommonMetrics.h" + +#define GRADE_PERCENT_TIER(i) THEME->GetMetricF("PlayerStageStats",ssprintf("GradePercent%s",GradeToString((Grade)i).c_str())) +// deprecated, but no solution to replace them exists yet: +#define GRADE_TIER02_IS_ALL_W2S THEME->GetMetricB("PlayerStageStats","GradeTier02IsAllW2s") +#define GRADE_TIER01_IS_ALL_W2S THEME->GetMetricB("PlayerStageStats","GradeTier01IsAllW2s") +#define GRADE_TIER02_IS_FULL_COMBO THEME->GetMetricB("PlayerStageStats","GradeTier02IsFullCombo") + +static ThemeMetric g_MinScoreToMaintainCombo( "Gameplay", "MinScoreToMaintainCombo" ); +static ThemeMetric g_MineHitIncrementsMissCombo( "Gameplay", "MineHitIncrementsMissCombo" ); + +const float LESSON_PASS_THRESHOLD = 0.8f; + +Grade GetGradeFromPercent( float fPercent ); + +void PlayerStageStats::Init() +{ + m_bPlayerCanAchieveFullCombo = true; + m_bJoined = false; + m_vpPossibleSteps.clear(); + m_iStepsPlayed = 0; + m_fAliveSeconds = 0; + m_bFailed = false; + m_iPossibleDancePoints = 0; + m_iCurPossibleDancePoints = 0; + m_iActualDancePoints = 0; + m_iPossibleGradePoints = 0; + m_iCurCombo = 0; + m_iMaxCombo = 0; + m_iCurMissCombo = 0; + m_iCurScoreMultiplier = 1; + m_iScore = 0; + m_iMaxScore = 0; + m_iCurMaxScore = 0; + m_iSongsPassed = 0; + m_iSongsPlayed = 0; + m_fLifeRemainingSeconds = 0; + m_iNumControllerSteps = 0; + m_fCaloriesBurned = 0; + + ZERO( m_iTapNoteScores ); + ZERO( m_iHoldNoteScores ); + m_radarPossible.Zero(); + m_radarActual.Zero(); + + m_fFirstSecond = FLT_MAX; + m_fLastSecond = 0; + + m_StageAward = StageAward_Invalid; + m_PeakComboAward = PeakComboAward_Invalid; + m_iPersonalHighScoreIndex = -1; + m_iMachineHighScoreIndex = -1; + m_bDisqualified = false; + m_rc = RankingCategory_Invalid; + m_HighScore = HighScore(); +} + +void PlayerStageStats::AddStats( const PlayerStageStats& other ) +{ + m_bJoined = other.m_bJoined; + FOREACH_CONST( Steps*, other.m_vpPossibleSteps, s ) + m_vpPossibleSteps.push_back( *s ); + m_iStepsPlayed += other.m_iStepsPlayed; + m_fAliveSeconds += other.m_fAliveSeconds; + m_bFailed |= other.m_bFailed; + m_iPossibleDancePoints += other.m_iPossibleDancePoints; + m_iActualDancePoints += other.m_iActualDancePoints; + m_iCurPossibleDancePoints += other.m_iCurPossibleDancePoints; + m_iPossibleGradePoints += other.m_iPossibleGradePoints; + + for( int t=0; t::const_iterator it; + for( it = other.m_fLifeRecord.begin(); it != other.m_fLifeRecord.end(); ++it ) + { + const float pos = it->first; + const float life = it->second; + m_fLifeRecord[fOtherFirstSecond+pos] = life; + } + + for( unsigned i=0; i 0.001 ) + continue; + + // These are really the same combo. + prevcombo.m_fSizeSeconds += combo.m_fSizeSeconds; + prevcombo.m_cnt += combo.m_cnt; + m_ComboList.erase( m_ComboList.begin()+i ); + --i; + } +} + +Grade GetGradeFromPercent( float fPercent ) +{ + Grade grade = Grade_Failed; + + FOREACH_ENUM( Grade,g) + { + if( fPercent >= GRADE_PERCENT_TIER(g) ) + { + grade = g; + break; + } + } + return grade; +} + +Grade PlayerStageStats::GetGrade() const +{ + if( m_bFailed ) + return Grade_Failed; + + /* XXX: This entire calculation should be in ScoreKeeper, but final evaluation + * is tricky since at that point the ScoreKeepers no longer exist. */ + float fActual = 0; + + bool bIsBeginner = false; + if( m_iStepsPlayed > 0 && !GAMESTATE->IsCourseMode() ) + bIsBeginner = m_vpPossibleSteps[0]->GetDifficulty() == Difficulty_Beginner; + + FOREACH_ENUM( TapNoteScore, tns ) + { + int iTapScoreValue = ScoreKeeperNormal::TapNoteScoreToGradePoints( tns, bIsBeginner ); + fActual += m_iTapNoteScores[tns] * iTapScoreValue; + //LOG->Trace( "GetGrade actual: %i * %i", m_iTapNoteScores[tns], iTapScoreValue ); + } + + FOREACH_ENUM( HoldNoteScore, hns ) + { + int iHoldScoreValue = ScoreKeeperNormal::HoldNoteScoreToGradePoints( hns, bIsBeginner ); + fActual += m_iHoldNoteScores[hns] * iHoldScoreValue; + //LOG->Trace( "GetGrade actual: %i * %i", m_iHoldNoteScores[hns], iHoldScoreValue ); + } + + //LOG->Trace( "GetGrade: fActual: %f, fPossible: %d", fActual, m_iPossibleGradePoints ); + + float fPercent = (m_iPossibleGradePoints == 0) ? 0 : fActual / m_iPossibleGradePoints; + + Grade grade = GetGradeFromPercent( fPercent ); + + //LOG->Trace( "GetGrade: Grade: %s, %i", GradeToString(grade).c_str(), GRADE_TIER02_IS_ALL_W2S ); + + // TODO: Change these conditions to use Lua instead. -aj + if( GRADE_TIER02_IS_ALL_W2S ) + { + if( FullComboOfScore(TNS_W1) ) + return Grade_Tier01; + + if( FullComboOfScore(TNS_W2) ) + return Grade_Tier02; + + grade = max( grade, Grade_Tier03 ); + } + + if( GRADE_TIER01_IS_ALL_W2S ) + { + if( FullComboOfScore(TNS_W2) ) + return Grade_Tier01; + grade = max( grade, Grade_Tier02 ); + } + + if( GRADE_TIER02_IS_FULL_COMBO ) + { + if( FullComboOfScore(g_MinScoreToMaintainCombo) ) + return Grade_Tier02; + grade = max( grade, Grade_Tier03 ); + } + + return grade; +} + +float PlayerStageStats::MakePercentScore( int iActual, int iPossible ) +{ + if( iPossible == 0 ) + return 0; // div/0 + + if( iActual == iPossible ) + return 1; // correct for rounding error + + // This can happen in battle, with transform attacks. + //ASSERT_M( iActual <= iPossible, ssprintf("%i/%i", iActual, iPossible) ); + + float fPercent = iActual / (float)iPossible; + + // don't allow negative + fPercent = max( 0, fPercent ); + + int iPercentTotalDigits = 3 + CommonMetrics::PERCENT_SCORE_DECIMAL_PLACES; // "100" + "." + "00" + + // TRICKY: printf will round, but we want to truncate. Otherwise, we may display + // a percent score that's too high and doesn't match up with the calculated grade. + float fTruncInterval = powf( 0.1f, (float)iPercentTotalDigits-1 ); + + // TRICKY: ftruncf is rounding 1.0000000 to 0.99990004. Give a little boost + // to fPercentDancePoints to correct for this. + fPercent += 0.000001f; + + fPercent = ftruncf( fPercent, fTruncInterval ); + return fPercent; +} + +RString PlayerStageStats::FormatPercentScore( float fPercentDancePoints ) +{ + int iPercentTotalDigits = 3 + CommonMetrics::PERCENT_SCORE_DECIMAL_PLACES; // "100" + "." + "00" + + RString s = ssprintf( "%*.*f%%", iPercentTotalDigits, + (int)CommonMetrics::PERCENT_SCORE_DECIMAL_PLACES, + fPercentDancePoints*100 ); + return s; +} + +float PlayerStageStats::GetPercentDancePoints() const +{ + return MakePercentScore( m_iActualDancePoints, m_iPossibleDancePoints ); +} + +float PlayerStageStats::GetCurMaxPercentDancePoints() const +{ + if ( m_iPossibleDancePoints == 0 ) + return 0; // div/0 + + if ( m_iCurPossibleDancePoints == m_iPossibleDancePoints ) + return 1; // correct for rounding error + + float fCurMaxPercentDancePoints = m_iCurPossibleDancePoints / (float)m_iPossibleDancePoints; + + return fCurMaxPercentDancePoints; +} + +// TODO: Make this use lua. Let more judgments be possible. -Wolfman2000 +int PlayerStageStats::GetLessonScoreActual() const +{ + int iScore = 0; + + FOREACH_ENUM( TapNoteScore, tns ) + { + switch( tns ) + { + case TNS_AvoidMine: + case TNS_W5: + case TNS_W4: + case TNS_W3: + case TNS_W2: + case TNS_W1: + iScore += m_iTapNoteScores[tns]; + default: + break; + } + } + + FOREACH_ENUM( HoldNoteScore, hns ) + { + switch( hns ) + { + case HNS_Held: + iScore += m_iHoldNoteScores[hns]; + default: + break; + } + } + + return iScore; +} + +int PlayerStageStats::GetLessonScoreNeeded() const +{ + float fScore = 0; + + FOREACH_CONST( Steps*, m_vpPossibleSteps, steps ) + fScore += (*steps)->GetRadarValues( PLAYER_1 ).m_Values.v.fNumTapsAndHolds; + + return lrintf( fScore * LESSON_PASS_THRESHOLD ); +} + +void PlayerStageStats::ResetScoreForLesson() +{ + m_iCurPossibleDancePoints = 0; + m_iActualDancePoints = 0; + FOREACH_ENUM( TapNoteScore, tns ) + m_iTapNoteScores[tns] = 0; + FOREACH_ENUM( HoldNoteScore, hns ) + m_iHoldNoteScores[hns] = 0; + m_iCurCombo = 0; + m_iMaxCombo = 0; + m_iCurMissCombo = 0; + m_iScore = 0; + m_iCurMaxScore = 0; + m_iMaxScore = 0; +} + +void PlayerStageStats::SetLifeRecordAt( float fLife, float fStepsSecond ) +{ + // Don't save life stats in endless courses, or could run OOM in a few hours. + if( GAMESTATE->m_pCurCourse && GAMESTATE->m_pCurCourse->IsEndless() ) + return; + + if( fStepsSecond < 0 ) + return; + + m_fFirstSecond = min( fStepsSecond, m_fFirstSecond ); + m_fLastSecond = max( fStepsSecond, m_fLastSecond ); + //LOG->Trace( "fLastSecond = %f", m_fLastSecond ); + + // fSecond will always be greater than any value already in the map. + m_fLifeRecord[fStepsSecond] = fLife; + + MESSAGEMAN->Broadcast( Message_LifeMeterChangedP1 ); + + // Memory optimization: + // If we have three consecutive records A, B, and C all with the same fLife, + // we can eliminate record B without losing data. Only check the last three + // records in the map since we're only inserting at the end, and we know all + // earlier redundant records have already been removed. + map::iterator C = m_fLifeRecord.end(); + --C; + if( C == m_fLifeRecord.begin() ) // no earlier records left + return; + + map::iterator B = C; + --B; + if( B == m_fLifeRecord.begin() ) // no earlier records left + return; + + map::iterator A = B; + --A; + + if( A->second == B->second && B->second == C->second ) + m_fLifeRecord.erase(B); +} + +float PlayerStageStats::GetLifeRecordAt( float fStepsSecond ) const +{ + if( m_fLifeRecord.empty() ) + return 0; + + // Find the first element whose key is greater than k. + map::const_iterator it = m_fLifeRecord.upper_bound( fStepsSecond ); + + // Find the last element whose key is less than or equal to k. + if( it != m_fLifeRecord.begin() ) + --it; + + return it->second; + +} + +float PlayerStageStats::GetLifeRecordLerpAt( float fStepsSecond ) const +{ + if( m_fLifeRecord.empty() ) + return 0; + + // Find the first element whose key is greater than k. + map::const_iterator later = m_fLifeRecord.upper_bound( fStepsSecond ); + + // Find the last element whose key is less than or equal to k. + map::const_iterator earlier = later; + if( earlier != m_fLifeRecord.begin() ) + --earlier; + + if( later == m_fLifeRecord.end() ) + return earlier->second; + + if( earlier->first == later->first ) // two samples from the same time. Don't divide by zero in SCALE + return earlier->second; + + // earlier <= pos <= later + return SCALE( fStepsSecond, earlier->first, later->first, earlier->second, later->second ); +} + +void PlayerStageStats::GetLifeRecord( float *fLifeOut, int iNumSamples, float fStepsEndSecond ) const +{ + for( int i = 0; i < iNumSamples; ++i ) + { + float from = SCALE( i, 0, (float)iNumSamples, 0.0f, fStepsEndSecond ); + fLifeOut[i] = GetLifeRecordLerpAt( from ); + } +} + +float PlayerStageStats::GetCurrentLife() const +{ + if( m_fLifeRecord.empty() ) + return 0; + map::const_iterator iter = m_fLifeRecord.end(); + --iter; + return iter->second; +} + +/* If bRollover is true, we're being called before gameplay begins, so we can + * record the amount of the first combo that comes from the previous song. */ +void PlayerStageStats::UpdateComboList( float fSecond, bool bRollover ) +{ + // Don't save combo stats in endless courses, or could run OOM in a few hours. + if( GAMESTATE->m_pCurCourse && GAMESTATE->m_pCurCourse->IsEndless() ) + return; + + if( fSecond < 0 ) + return; + + if( !bRollover ) + { + m_fFirstSecond = min( fSecond, m_fFirstSecond ); + m_fLastSecond = max( fSecond, m_fLastSecond ); + //LOG->Trace( "fLastSecond = %f", fLastSecond ); + } + + int cnt = m_iCurCombo; + if( !cnt ) + return; // no combo + + if( m_ComboList.size() == 0 || m_ComboList.back().m_cnt >= cnt ) + { + /* If the previous combo (if any) starts on -9999, then we rolled over + * some combo, but missed the first step. Remove it. */ + if( m_ComboList.size() && m_ComboList.back().m_fStartSecond == -9999 ) + m_ComboList.erase( m_ComboList.begin()+m_ComboList.size()-1, m_ComboList.end() ); + + // This is a new combo. + Combo_t NewCombo; + /* "start" is the position that the combo started within this song. + * If we're recording rollover, the combo hasn't started yet (within + * this song), so put a placeholder in and set it on the next call. + * (Otherwise, start will be less than fFirstPos.) */ + if( bRollover ) + NewCombo.m_fStartSecond = -9999; + else + NewCombo.m_fStartSecond = fSecond; + m_ComboList.push_back( NewCombo ); + } + + Combo_t &combo = m_ComboList.back(); + if( !bRollover && combo.m_fStartSecond == -9999 ) + combo.m_fStartSecond = 0; + + combo.m_fSizeSeconds = fSecond - combo.m_fStartSecond; + combo.m_cnt = cnt; + + if( bRollover ) + combo.m_rollover = cnt; +} + +/* This returns the largest combo contained within the song, as if + * m_bComboContinuesBetweenSongs is turned off. */ +PlayerStageStats::Combo_t PlayerStageStats::GetMaxCombo() const +{ + if( m_ComboList.size() == 0 ) + return Combo_t(); + + int m = 0; + for( unsigned i = 1; i < m_ComboList.size(); ++i ) + { + if( m_ComboList[i].m_cnt > m_ComboList[m].m_cnt ) + m = i; + } + + return m_ComboList[m]; +} + +int PlayerStageStats::GetComboAtStartOfStage() const +{ + if( m_ComboList.empty() ) + return 0; + else + return m_ComboList[0].m_rollover; +} + +bool PlayerStageStats::FullComboOfScore( TapNoteScore tnsAllGreaterOrEqual ) const +{ + ASSERT( tnsAllGreaterOrEqual >= TNS_W5 ); + ASSERT( tnsAllGreaterOrEqual <= TNS_W1 ); + + //if we've set MissCombo to anything besides 0, it's not a full combo + if( !m_bPlayerCanAchieveFullCombo ) + return false; + + // If missed any holds, then it's not a full combo + if( m_iHoldNoteScores[HNS_LetGo] > 0 ) + return false; + + //if any checkpoints were missed, it's not a full combo either + if( m_iTapNoteScores[TNS_CheckpointMiss] > 0 ) + return false; + + // If has any of the judgments below, then not a full combo + for( int i=TNS_Miss; i 0 ) + return false; + } + + // hit any mines when they increment the miss combo? It's not a full combo. + if( g_MineHitIncrementsMissCombo && m_iTapNoteScores[TNS_HitMine] > 0 ) + return false; + + // If has at least one of the judgments equal to or above, then is a full combo. + for( int i=tnsAllGreaterOrEqual; i 0 ) + return true; + } + + return false; +} + +TapNoteScore PlayerStageStats::GetBestFullComboTapNoteScore() const +{ + // Optimization opportunity: ... + // (seriously? -aj) + for( TapNoteScore i=TNS_W1; i >= TNS_W5; enum_add(i,-1)) + { + if( FullComboOfScore(i) ) + return i; + } + return TapNoteScore_Invalid; +} + +bool PlayerStageStats::SingleDigitsOfScore( TapNoteScore tnsAllGreaterOrEqual ) const +{ + return FullComboOfScore( tnsAllGreaterOrEqual ) && + m_iTapNoteScores[tnsAllGreaterOrEqual] < 10; +} + +bool PlayerStageStats::OneOfScore( TapNoteScore tnsAllGreaterOrEqual ) const +{ + return FullComboOfScore( tnsAllGreaterOrEqual ) && + m_iTapNoteScores[tnsAllGreaterOrEqual] == 1; +} + +int PlayerStageStats::GetTotalTaps() const +{ + int iTotalTaps = 0; + for( int i=TNS_Miss; iTrace( "hand out awards" ); + + m_PeakComboAward = PeakComboAward_Invalid; + + if( bGaveUp || bUsedAutoplay ) + return; + + deque &vPdas = GAMESTATE->m_vLastStageAwards[p]; + + //LOG->Trace( "per difficulty awards" ); + + // per-difficulty awards + // don't give per-difficutly awards if using easy mods + if( !IsDisqualified() ) + { + if( FullComboOfScore( TNS_W3 ) ) + vPdas.push_back( StageAward_FullComboW3 ); + if( SingleDigitsOfScore( TNS_W3 ) ) + vPdas.push_back( StageAward_SingleDigitW3 ); + if( FullComboOfScore( TNS_W2 ) ) + vPdas.push_back( StageAward_FullComboW2 ); + if( SingleDigitsOfScore( TNS_W2 ) ) + vPdas.push_back( StageAward_SingleDigitW2 ); + if( FullComboOfScore( TNS_W1 ) ) + vPdas.push_back( StageAward_FullComboW1 ); + + if( OneOfScore( TNS_W3 ) ) + vPdas.push_back( StageAward_OneW3 ); + if( OneOfScore( TNS_W2 ) ) + vPdas.push_back( StageAward_OneW2 ); + + float fPercentW3s = GetPercentageOfTaps( TNS_W3 ); + if( fPercentW3s >= 0.8f ) + vPdas.push_back( StageAward_80PercentW3 ); + if( fPercentW3s >= 0.9f ) + vPdas.push_back( StageAward_90PercentW3 ); + if( fPercentW3s >= 1.f ) + vPdas.push_back( StageAward_100PercentW3 ); + } + + // Max one PDA per stage + if( !vPdas.empty() ) + vPdas.erase( vPdas.begin(), vPdas.end()-1 ); + + if( !vPdas.empty() ) + m_StageAward = vPdas.back(); + else + m_StageAward = StageAward_Invalid; + + //LOG->Trace( "done with per difficulty awards" ); + + // DO give peak combo awards if using easy mods + int iComboAtStartOfStage = GetComboAtStartOfStage(); + int iPeakCombo = GetMaxCombo().m_cnt; + + FOREACH_ENUM( PeakComboAward,pca ) + { + int iLevel = 1000 * (pca+1); + bool bCrossedLevel = iComboAtStartOfStage < iLevel && iPeakCombo >= iLevel; + //LOG->Trace( "pca = %d, iLevel = %d, bCrossedLevel = %d", pca, iLevel, bCrossedLevel ); + if( bCrossedLevel ) + GAMESTATE->m_vLastPeakComboAwards[p].push_back( pca ); + } + + if( !GAMESTATE->m_vLastPeakComboAwards[p].empty() ) + m_PeakComboAward = GAMESTATE->m_vLastPeakComboAwards[p].back(); + else + m_PeakComboAward = PeakComboAward_Invalid; + + //LOG->Trace( "done with per combo awards" ); + +} + +bool PlayerStageStats::IsDisqualified() const +{ + if( !PREFSMAN->m_bDisqualification ) + return false; + return m_bDisqualified; +} + +LuaFunction( GetGradeFromPercent, GetGradeFromPercent( FArg(1) ) ) +LuaFunction( FormatPercentScore, PlayerStageStats::FormatPercentScore( FArg(1) ) ) + + +// lua start +#include "LuaBinding.h" + +/** @brief Allow Lua to have access to the PlayerStageStats. */ +class LunaPlayerStageStats: public Luna +{ +public: + DEFINE_METHOD( GetCaloriesBurned, m_fCaloriesBurned ) + DEFINE_METHOD( GetNumControllerSteps, m_iNumControllerSteps ) + DEFINE_METHOD( GetLifeRemainingSeconds, m_fLifeRemainingSeconds ) + DEFINE_METHOD( GetSurvivalSeconds, GetSurvivalSeconds() ) + DEFINE_METHOD( GetCurrentCombo, m_iCurCombo ) + DEFINE_METHOD( GetCurrentMissCombo, m_iCurMissCombo ) + DEFINE_METHOD( GetCurrentScoreMultiplier, m_iCurScoreMultiplier ) + DEFINE_METHOD( GetScore, m_iScore ) + DEFINE_METHOD( GetCurMaxScore, m_iCurMaxScore ) + DEFINE_METHOD( GetTapNoteScores, m_iTapNoteScores[Enum::Check(L, 1)] ) + DEFINE_METHOD( GetHoldNoteScores, m_iHoldNoteScores[Enum::Check(L, 1)] ) + DEFINE_METHOD( FullCombo, FullCombo() ) + DEFINE_METHOD( FullComboOfScore, FullComboOfScore( Enum::Check(L, 1) ) ) + DEFINE_METHOD( MaxCombo, GetMaxCombo().m_cnt ) + DEFINE_METHOD( GetCurrentLife, GetCurrentLife() ) + DEFINE_METHOD( GetGrade, GetGrade() ) + DEFINE_METHOD( GetActualDancePoints, m_iActualDancePoints ) + DEFINE_METHOD( GetPossibleDancePoints, m_iPossibleDancePoints ) + DEFINE_METHOD( GetCurrentPossibleDancePoints, m_iCurPossibleDancePoints ) + DEFINE_METHOD( GetPercentDancePoints, GetPercentDancePoints() ) + DEFINE_METHOD( GetLessonScoreActual, GetLessonScoreActual() ) + DEFINE_METHOD( GetLessonScoreNeeded, GetLessonScoreNeeded() ) + DEFINE_METHOD( GetPersonalHighScoreIndex, m_iPersonalHighScoreIndex ) + DEFINE_METHOD( GetMachineHighScoreIndex, m_iMachineHighScoreIndex ) + DEFINE_METHOD( GetStageAward, m_StageAward ) + DEFINE_METHOD( GetPeakComboAward, m_PeakComboAward ) + DEFINE_METHOD( IsDisqualified, IsDisqualified() ) + DEFINE_METHOD( GetAliveSeconds, m_fAliveSeconds ) + DEFINE_METHOD( GetPercentageOfTaps, GetPercentageOfTaps( Enum::Check(L, 1) ) ) + DEFINE_METHOD( GetBestFullComboTapNoteScore, GetBestFullComboTapNoteScore() ) + DEFINE_METHOD( GetFailed, m_bFailed ) + DEFINE_METHOD( GetSongsPassed, m_iSongsPassed ) + DEFINE_METHOD( GetSongsPlayed, m_iSongsPlayed ) + + static int GetPlayedSteps( T* p, lua_State *L ) + { + lua_newtable(L); + for( int i = 0; i < (int) min(p->m_iStepsPlayed, (int) p->m_vpPossibleSteps.size()); ++i ) + { + p->m_vpPossibleSteps[i]->PushSelf(L); + lua_rawseti( L, -2, i+1 ); + } + return 1; + } + static int GetPossibleSteps( T* p, lua_State *L ) + { + lua_newtable(L); + for( int i = 0; i < (int) p->m_vpPossibleSteps.size(); ++i ) + { + p->m_vpPossibleSteps[i]->PushSelf(L); + lua_rawseti( L, -2, i+1 ); + } + return 1; + } + + static int GetRadarPossible( T* p, lua_State *L ) { p->m_radarPossible.PushSelf(L); return 1; } + static int GetRadarActual( T* p, lua_State *L ) { p->m_radarActual.PushSelf(L); return 1; } + static int SetScore( T* p, lua_State *L ) + { + if( IArg(1) >= 0 ) + { + p->m_iScore = IArg(1); + return 1; + } + return 0; + } + static int SetCurMaxScore( T* p, lua_State *L ) + { + if( IArg(1) >= 0 ) + { + p->m_iCurMaxScore = IArg(1); + return 1; + } + return 0; + } + + static int FailPlayer( T* p, lua_State *L ) + { + p->m_bFailed = true; + return 0; + } + + LunaPlayerStageStats() + { + ADD_METHOD( GetCaloriesBurned ); + ADD_METHOD( GetNumControllerSteps ); + ADD_METHOD( GetLifeRemainingSeconds ); + ADD_METHOD( GetSurvivalSeconds ); + ADD_METHOD( GetCurrentCombo ); + ADD_METHOD( GetCurrentMissCombo ); + ADD_METHOD( GetCurrentScoreMultiplier ); + ADD_METHOD( GetScore ); + ADD_METHOD( GetCurMaxScore ); + ADD_METHOD( GetTapNoteScores ); + ADD_METHOD( GetHoldNoteScores ); + ADD_METHOD( FullCombo ); + ADD_METHOD( FullComboOfScore ); + ADD_METHOD( MaxCombo ); + ADD_METHOD( GetCurrentLife ); + ADD_METHOD( GetGrade ); + ADD_METHOD( GetActualDancePoints ); + ADD_METHOD( GetPossibleDancePoints ); + ADD_METHOD( GetCurrentPossibleDancePoints ); + ADD_METHOD( GetPercentDancePoints ); + ADD_METHOD( GetLessonScoreActual ); + ADD_METHOD( GetLessonScoreNeeded ); + ADD_METHOD( GetPersonalHighScoreIndex ); + ADD_METHOD( GetMachineHighScoreIndex ); + ADD_METHOD( GetStageAward ); + ADD_METHOD( GetPeakComboAward ); + ADD_METHOD( IsDisqualified ); + ADD_METHOD( GetPlayedSteps ); + ADD_METHOD( GetPossibleSteps ); + ADD_METHOD( GetAliveSeconds ); + ADD_METHOD( GetPercentageOfTaps ); + ADD_METHOD( GetRadarActual ); + ADD_METHOD( GetRadarPossible ); + ADD_METHOD( GetBestFullComboTapNoteScore ); + ADD_METHOD( GetFailed ); + ADD_METHOD( SetScore ); + ADD_METHOD( GetCurMaxScore ); + ADD_METHOD( SetCurMaxScore ); + ADD_METHOD( FailPlayer ); + ADD_METHOD( GetSongsPassed ); + ADD_METHOD( GetSongsPlayed ); + } +}; + +LUA_REGISTER_CLASS( PlayerStageStats ) +// lua end + +/* + * (c) 2001-2004 Chris Danford, Glenn Maynard + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, provided that the above + * copyright notice(s) and this permission notice appear in all copies of + * the Software and that both the above copyright notice(s) and this + * permission notice appear in supporting documentation. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS + * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT + * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */