#include "global.h" /* ----------------------------------------------------------------------------- Class: Player Desc: See header. Copyright (c) 2001-2002 by the person(s) listed below. All rights reserved. Chris Danford ----------------------------------------------------------------------------- */ #include "GameConstantsAndTypes.h" #include // for fabs() #include "Player.h" #include "RageUtil.h" #include "PrefsManager.h" #include "GameConstantsAndTypes.h" #include "ArrowEffects.h" #include "GameManager.h" #include "InputMapper.h" #include "SongManager.h" #include "GameState.h" #include "ScoreKeeperMAX2.h" #include "RageLog.h" #include "RageMath.h" #include "RageDisplay.h" #include "ThemeManager.h" #include "Combo.h" #include "ScoreDisplay.h" #include "LifeMeter.h" #define GRAY_ARROWS_Y THEME->GetMetricF("Player","GrayArrowsY") #define JUDGMENT_Y THEME->GetMetricF("Player","JudgmentY") #define COMBO_Y THEME->GetMetricF("Player","ComboY") #define HOLD_JUDGMENT_Y THEME->GetMetricF("Player","HoldJudgmentY") CachedThemeMetricI BRIGHT_GHOST_COMBO_THRESHOLD("Player","BrightGhostComboThreshold"); #define START_DRAWING_AT_PIXELS THEME->GetMetricI("Player","StartDrawingAtPixels") #define STOP_DRAWING_AT_PIXELS THEME->GetMetricI("Player","StopDrawingAtPixels") Player::Player() { BRIGHT_GHOST_COMBO_THRESHOLD.Refresh(); m_PlayerNumber = PLAYER_INVALID; m_pLifeMeter = NULL; m_pScore = NULL; m_pScoreKeeper = NULL; m_pInventory = NULL; m_iOffsetSample = 0; this->AddChild( &m_GrayArrowRow ); this->AddChild( &m_NoteField ); this->AddChild( &m_GhostArrowRow ); this->AddChild( &m_Judgment ); this->AddChild( &m_Combo ); for( int c=0; cAddChild( &m_HoldJudgment[c] ); } Player::~Player() { } void Player::Load( PlayerNumber pn, NoteData* pNoteData, LifeMeter* pLM, ScoreDisplay* pScore, Inventory* pInventory, ScoreKeeper* pScoreKeeper ) { //LOG->Trace( "Player::Load()", ); m_PlayerNumber = pn; m_pLifeMeter = pLM; m_pScore = pScore; m_pInventory = pInventory; m_pScoreKeeper = pScoreKeeper; const StyleDef* pStyleDef = GAMESTATE->GetCurrentStyleDef(); // init scoring NoteDataWithScoring::Init(); // copy note data this->CopyAll( pNoteData ); if( GAMESTATE->m_SongOptions.m_LifeType == SongOptions::LIFE_BATTERY && GAMESTATE->m_CurStageStats.bFailed[pn] ) // Oni dead this->ClearAll(); /* The editor reuses Players ... so we really need to make sure everything * is reset and not tweening. Perhaps ActorFrame should recurse to subactors; * then we could just this->StopTweening()? -glenn */ m_Judgment.StopTweening(); // m_Combo.Reset(); // don't reset combos between songs in a course! m_Combo.Init( pn ); m_Judgment.Reset(); if( m_pScore ) m_pScore->Init( pn ); if( !GAMESTATE->m_PlayerOptions[pn].m_bHoldNotes ) NoteDataUtil::RemoveHoldNotes(*this); switch( GAMESTATE->m_PlayerOptions[pn].m_Turn ) { case PlayerOptions::TURN_NONE: break; case PlayerOptions::TURN_MIRROR: NoteDataUtil::Turn( *this, NoteDataUtil::mirror ); break; case PlayerOptions::TURN_LEFT: NoteDataUtil::Turn( *this, NoteDataUtil::left ); break; case PlayerOptions::TURN_RIGHT: NoteDataUtil::Turn( *this, NoteDataUtil::right ); break; case PlayerOptions::TURN_SHUFFLE: NoteDataUtil::Turn( *this, NoteDataUtil::shuffle ); break; case PlayerOptions::TURN_SUPER_SHUFFLE: NoteDataUtil::Turn( *this, NoteDataUtil::super_shuffle ); break; default: ASSERT(0); } switch( GAMESTATE->m_PlayerOptions[pn].m_Transform ) { case PlayerOptions::TRANSFORM_NONE: break; case PlayerOptions::TRANSFORM_LITTLE: NoteDataUtil::Little(*this); break; case PlayerOptions::TRANSFORM_WIDE: NoteDataUtil::Wide(*this); break; case PlayerOptions::TRANSFORM_BIG: NoteDataUtil::Big(*this); break; case PlayerOptions::TRANSFORM_QUICK: NoteDataUtil::Quick(*this); break; case PlayerOptions::TRANSFORM_SKIPPY: NoteDataUtil::Skippy(*this); break; default: ASSERT(0); } int iStartDrawingAtPixels = GAMESTATE->m_bEditing ? -100 : START_DRAWING_AT_PIXELS; int iStopDrawingAtPixels = GAMESTATE->m_bEditing ? 400 : STOP_DRAWING_AT_PIXELS; // If both options are on, we *do* need to multiply it twice. if( GAMESTATE->m_PlayerOptions[pn].m_fEffects[PlayerOptions::EFFECT_MINI]==1 ) { iStartDrawingAtPixels *= 2; iStopDrawingAtPixels *= 2; } if( GAMESTATE->m_PlayerOptions[pn].m_fEffects[PlayerOptions::EFFECT_SPACE]==1 ) { iStartDrawingAtPixels *= 2; iStopDrawingAtPixels *= 2; } m_NoteField.Load( (NoteData*)this, pn, iStartDrawingAtPixels, iStopDrawingAtPixels ); m_GrayArrowRow.Load( pn ); m_GhostArrowRow.Load( pn ); bool bReverse = GAMESTATE->m_PlayerOptions[pn].m_fReverseScroll == 1; m_Combo.SetY( bReverse ? SCREEN_BOTTOM-COMBO_Y : SCREEN_TOP+COMBO_Y ); m_Judgment.SetY( bReverse ? SCREEN_BOTTOM-JUDGMENT_Y : SCREEN_TOP+JUDGMENT_Y ); int c; for( c=0; cm_iColsPerPlayer; c++ ) { m_HoldJudgment[c].SetY( bReverse ? SCREEN_BOTTOM-HOLD_JUDGMENT_Y : SCREEN_TOP+HOLD_JUDGMENT_Y ); m_HoldJudgment[c].SetX( (float)pStyleDef->m_ColumnInfo[pn][c].fXOffset ); } m_NoteField.SetY( bReverse ? SCREEN_BOTTOM-GRAY_ARROWS_Y : SCREEN_TOP+GRAY_ARROWS_Y ); m_GrayArrowRow.SetY( bReverse ? SCREEN_BOTTOM-GRAY_ARROWS_Y : SCREEN_TOP+GRAY_ARROWS_Y ); m_GhostArrowRow.SetY( bReverse ? SCREEN_BOTTOM-GRAY_ARROWS_Y : SCREEN_TOP+GRAY_ARROWS_Y ); if( GAMESTATE->m_PlayerOptions[pn].m_fEffects[PlayerOptions::EFFECT_MINI] == 1 ) { m_NoteField.SetZoom( 0.5f ); m_GrayArrowRow.SetZoom( 0.5f ); m_GhostArrowRow.SetZoom( 0.5f ); } } void Player::Update( float fDeltaTime ) { //LOG->Trace( "Player::Update(%f)", fDeltaTime ); const float fSongBeat = GAMESTATE->m_fSongBeat; // // Check for TapNote misses // UpdateTapNotesMissedOlderThan( GetMaxStepDistanceSeconds() ); // // update HoldNotes logic // for( int i=0; i < GetNumHoldNotes(); i++ ) // for each HoldNote { const HoldNote &hn = GetHoldNote(i); HoldNoteScore hns = GetHoldNoteScore(i); float fLife = GetHoldNoteLife(i); int iHoldStartIndex = BeatToNoteRow(hn.fStartBeat); m_NoteField.m_bIsHoldingHoldNote[i] = false; // set host flag so NoteField can do intelligent drawing if( hns != HNS_NONE ) // if this HoldNote already has a result continue; // we don't need to update the logic for this one const StyleInput StyleI( m_PlayerNumber, hn.iTrack ); const GameInput GameI = GAMESTATE->GetCurrentStyleDef()->StyleInputToGameInput( StyleI ); // if they got a bad score or haven't stepped on the corresponding tap yet const TapNoteScore tns = GetTapNoteScore(hn.iTrack, iHoldStartIndex); const bool bSteppedOnTapNote = tns != TNS_NONE && tns != TNS_MISS; // did they step on the start of this hold? if( hn.fStartBeat < fSongBeat && fSongBeat < hn.fEndBeat ) // if the song beat is in the range of this hold { bool bIsHoldingButton = INPUTMAPPER->IsButtonDown( GameI ); if( !GAMESTATE->m_bEditing && (PREFSMAN->m_bAutoPlay || GAMESTATE->m_bDemonstrationOrJukebox) ) bIsHoldingButton = true; m_NoteField.m_bIsHoldingHoldNote[i] = bIsHoldingButton && bSteppedOnTapNote; // set host flag so NoteField can do intelligent drawing if( bSteppedOnTapNote ) // this note is not judged and we stepped on its head { m_NoteField.GetHoldNote(i).fStartBeat = fSongBeat; // move the start of this Hold } if( bSteppedOnTapNote && bIsHoldingButton ) { // Increase life fLife += fDeltaTime/PREFSMAN->m_fJudgeWindowOKSeconds; fLife = min( fLife, 1 ); // clamp m_GhostArrowRow.HoldNote( hn.iTrack ); // update the "electric ghost" effect } else { if( fSongBeat-hn.fStartBeat > GAMESTATE->m_fCurBPS * GetMaxStepDistanceSeconds() ) { // Decrease life fLife -= fDeltaTime/PREFSMAN->m_fJudgeWindowOKSeconds; fLife = max( fLife, 0 ); // clamp } } } /* check for NG. If the head was missed completely, don't count * an NG. */ if( bSteppedOnTapNote && fLife == 0 ) // the player has not pressed the button for a long time! hns = HNS_NG; // check for OK if( fSongBeat >= hn.fEndBeat && bSteppedOnTapNote && fLife > 0 ) // if this HoldNote is in the past { fLife = 1; hns = HNS_OK; m_GhostArrowRow.TapNote( StyleI.col, TNS_PERFECT, true ); // bright ghost flash } if( hns != HNS_NONE ) { /* this note's been judged */ HandleHoldScore( hns, tns ); m_HoldJudgment[hn.iTrack].SetHoldJudgment( hns ); } m_NoteField.SetHoldNoteLife(i, fLife); // update the NoteField display m_NoteField.SetHoldNoteScore(i, hns); // update the NoteField display SetHoldNoteLife(i, fLife); SetHoldNoteScore(i, hns); } ActorFrame::Update( fDeltaTime ); } void Player::DrawPrimitives() { m_Combo.Draw(); // draw this below everything else if( GAMESTATE->m_CurrentPlayerOptions[m_PlayerNumber].m_fEffects[PlayerOptions::EFFECT_SPACE] == 1 ) { DISPLAY->PushMatrix(); DISPLAY->EnterPerspective(45, false); // construct view and project matrix RageVector3 Eye, At, Up( 0.0f, 1.0f, 0.0f ); if( GAMESTATE->m_CurrentPlayerOptions[m_PlayerNumber].m_fReverseScroll==1 ) { Eye = RageVector3( CENTER_X, -300.0f, 400.0f ); At = RageVector3( CENTER_X, 100.0f, 0.0f ); } else { Eye = RageVector3( CENTER_X, 800, 400 ); At = RageVector3( CENTER_X, 400, 0.0f ); } DISPLAY->LookAt(Eye, At, Up); } else DISPLAY->PushMatrix(); m_GrayArrowRow.Draw(); m_NoteField.Draw(); m_GhostArrowRow.Draw(); if( GAMESTATE->m_CurrentPlayerOptions[m_PlayerNumber].m_fEffects[PlayerOptions::EFFECT_SPACE]==1 ) { DISPLAY->ExitPerspective(); DISPLAY->PopMatrix(); } else DISPLAY->PopMatrix(); m_Judgment.Draw(); for( int c=0; c DistToBack ) return Back; return Fwd; } void Player::Step( int col ) { if( GAMESTATE->m_SongOptions.m_LifeType == SongOptions::LIFE_BATTERY && GAMESTATE->m_CurStageStats.bFailed[m_PlayerNumber] ) // Oni dead return; // do nothing //LOG->Trace( "Player::HandlePlayerStep()" ); ASSERT( col >= 0 && col <= GetNumTracks() ); int iIndexOverlappingNote = GetClosestNote( col, GAMESTATE->m_fSongBeat, GAMESTATE->m_fCurBPS * GAMESTATE->m_SongOptions.m_fMusicRate, GAMESTATE->m_fCurBPS * GAMESTATE->m_SongOptions.m_fMusicRate ); //LOG->Trace( "iIndexStartLookingAt = %d, iNumElementsToExamine = %d", iIndexStartLookingAt, iNumElementsToExamine ); bool bDestroyedNote = false; if( iIndexOverlappingNote != -1 ) { // compute the score for this hit const float fStepBeat = NoteRowToBeat( (float)iIndexOverlappingNote ); const float fStepSeconds = GAMESTATE->m_pCurSong->GetElapsedTimeFromBeat(fStepBeat); // The offset from the actual step in seconds: const float fNoteOffset = fStepSeconds - GAMESTATE->m_fMusicSeconds; const float fSecondsFromPerfect = fabsf( fNoteOffset ) / GAMESTATE->m_SongOptions.m_fMusicRate; // account for music rate TapNoteScore score; LOG->Trace("fSecondsFromPerfect = %f", fSecondsFromPerfect); if( fSecondsFromPerfect <= PREFSMAN->m_fJudgeWindowMarvelousSeconds ) score = TNS_MARVELOUS; else if( fSecondsFromPerfect <= PREFSMAN->m_fJudgeWindowPerfectSeconds ) score = TNS_PERFECT; else if( fSecondsFromPerfect <= PREFSMAN->m_fJudgeWindowGreatSeconds ) score = TNS_GREAT; else if( fSecondsFromPerfect <= PREFSMAN->m_fJudgeWindowGoodSeconds ) score = TNS_GOOD; else if( fSecondsFromPerfect <= PREFSMAN->m_fJudgeWindowBooSeconds ) score = TNS_BOO; else score = TNS_NONE; if( !GAMESTATE->m_bEditing && (GAMESTATE->m_bDemonstrationOrJukebox || PREFSMAN->m_bAutoPlay) ) score = TNS_MARVELOUS; if( score==TNS_MARVELOUS && !PREFSMAN->m_bMarvelousTiming ) score = TNS_PERFECT; bDestroyedNote = (score >= TNS_GOOD); LOG->Trace("(%2d/%2d)Note offset: %f, Score: %i", m_iOffsetSample, SAMPLE_COUNT, fNoteOffset, score); SetTapNoteScore(col, iIndexOverlappingNote, score); SetTapNoteOffset(col, iIndexOverlappingNote, fNoteOffset); if ( score >= TNS_GREAT ) HandleAutosync(fNoteOffset); if (score > TNS_NONE && MinTapNoteScore(iIndexOverlappingNote) >= TNS_BOO ) OnRowDestroyed( iIndexOverlappingNote ); } if( !bDestroyedNote ) m_GrayArrowRow.Step( col ); } void Player::HandleAutosync(float fNoteOffset) { if( !GAMESTATE->m_SongOptions.m_bAutoSync ) return; m_fOffset[m_iOffsetSample++] = fNoteOffset; if (m_iOffsetSample < SAMPLE_COUNT) return; /* need more */ const float mean = calc_mean(m_fOffset, m_fOffset+SAMPLE_COUNT); const float stddev = calc_stddev(m_fOffset, m_fOffset+SAMPLE_COUNT); if (stddev < .03 && stddev < fabsf(mean)) { //If they stepped with less than .03 error GAMESTATE->m_pCurSong->m_fBeat0OffsetInSeconds += mean; LOG->Trace("Offset corrected by %f. Error in steps: %f seconds.", mean, stddev); } else LOG->Trace("Offset NOT corrected. Average offset: %f seconds. Error: %f seconds.", mean, stddev); m_iOffsetSample = 0; } void Player::OnRowDestroyed( int iIndexThatWasSteppedOn ) { LOG->Trace( "Player::OnRowDestroyed" ); /* Find the minimum score of the row. This will never be TNS_NONE, since this * function is only called when a row is completed. */ // TapNoteScore score = MinTapNoteScore(iIndexThatWasSteppedOn); TapNoteScore score = LastTapNoteScore(iIndexThatWasSteppedOn); /* If the whole row was hit with perfects or greats, remove the row * from the NoteField, so it disappears. */ switch ( score ) { case TNS_MARVELOUS: case TNS_PERFECT: case TNS_GREAT: m_NoteField.RemoveTapNoteRow( iIndexThatWasSteppedOn ); break; } for( int c=0; cm_CurStageStats.iCurCombo[m_PlayerNumber]>(int)BRIGHT_GHOST_COMBO_THRESHOLD; m_GhostArrowRow.TapNote( c, score, bBright ); } break; } } HandleTapRowScore( iIndexThatWasSteppedOn ); // update score m_Combo.SetCombo( GAMESTATE->m_CurStageStats.iCurCombo[m_PlayerNumber] ); m_Judgment.SetJudgment( score ); } void Player::UpdateTapNotesMissedOlderThan( float fMissIfOlderThanSeconds ) { //LOG->Trace( "Notes::UpdateTapNotesMissedOlderThan(%f)", fMissIfOlderThanThisBeat ); const float fEarliestTime = GAMESTATE->m_fMusicSeconds - fMissIfOlderThanSeconds; const float fMissIfOlderThanThisBeat = GAMESTATE->m_pCurSong->GetBeatFromElapsedTime(fEarliestTime); int iMissIfOlderThanThisIndex = BeatToNoteRow( fMissIfOlderThanThisBeat ); // Since this is being called every frame, let's not check the whole array every time. // Instead, only check 10 elements back. Even 10 is overkill. int iStartCheckingAt = max( 0, iMissIfOlderThanThisIndex-10 ); //LOG->Trace( "iStartCheckingAt: %d iMissIfOlderThanThisIndex: %d", iStartCheckingAt, iMissIfOlderThanThisIndex ); /* If we're on a freeze, and the freeze has been running for fMissIfOlderThanSeconds, * then iMissIfOlderThanThisIndex will be the freeze itself, in which case we do * want to update the row of the freeze itself; otherwise we won't show misses * for tap notes on freezes until the freeze finishes. */ int iNumMissesFound = 0; for( int r=iStartCheckingAt; r<=iMissIfOlderThanThisIndex; r++ ) { bool MissedNoteOnThisRow = false; for( int t=0; tm_CurStageStats.iCurCombo[m_PlayerNumber] ); } if( iNumMissesFound > 0 ) { m_Judgment.SetJudgment( TNS_MISS ); } } void Player::CrossedRow( int iNoteRow ) { if( PREFSMAN->m_bAutoPlay || GAMESTATE->m_bDemonstrationOrJukebox ) { // check to see if there's at the crossed row for( int t=0; tStep( t ); } } } void Player::HandleTapRowScore( unsigned row ) { TapNoteScore scoreOfLastTap = LastTapNoteScore(row); int iNumTapsInRow = 0; for( int c=0; c 0); #ifndef DEBUG // don't accumulate points if AutoPlay is on. if( PREFSMAN->m_bAutoPlay && !GAMESTATE->m_bDemonstrationOrJukebox ) return; #endif //DEBUG if(m_pScoreKeeper) m_pScoreKeeper->HandleTapRowScore(scoreOfLastTap, iNumTapsInRow, m_pInventory); if (m_pScore) m_pScore->SetScore(GAMESTATE->m_CurStageStats.fScore[m_PlayerNumber]); if( m_pLifeMeter ) { m_pLifeMeter->ChangeLife( scoreOfLastTap ); m_pLifeMeter->OnDancePointsChange(); // update oni life meter } } void Player::HandleHoldScore( HoldNoteScore holdScore, TapNoteScore tapScore ) { #ifndef DEBUG // don't accumulate points if AutoPlay is on. if( PREFSMAN->m_bAutoPlay && !GAMESTATE->m_bDemonstrationOrJukebox ) return; #endif //DEBUG if(m_pScoreKeeper) m_pScoreKeeper->HandleHoldScore(holdScore, tapScore); if (m_pScore) m_pScore->SetScore(GAMESTATE->m_CurStageStats.fScore[m_PlayerNumber]); if( m_pLifeMeter ) { m_pLifeMeter->ChangeLife( holdScore, tapScore ); // refresh Oni life meter m_pLifeMeter->OnDancePointsChange(); } } float Player::GetMaxStepDistanceSeconds() { return GAMESTATE->m_SongOptions.m_fMusicRate * PREFSMAN->m_fJudgeWindowBooSeconds * PREFSMAN->m_fJudgeWindowScale; } void Player::FadeToFail() { m_NoteField.FadeToFail(); }