diff --git a/stepmania/src/GameState.cpp b/stepmania/src/GameState.cpp index 8846aa38b8..1c446d450b 100644 --- a/stepmania/src/GameState.cpp +++ b/stepmania/src/GameState.cpp @@ -682,6 +682,10 @@ void GameState::ResetMusicStatistics() m_fCurBPS = 10; m_bFreeze = false; Actor::SetBGMTime( 0, 0 ); + + + FOREACH_PlayerNumber( p ) + m_pPlayerState[p]->m_fLastHopoNoteMusicSeconds = -1; } void GameState::ResetStageStatistics() diff --git a/stepmania/src/GameState.h b/stepmania/src/GameState.h index 7a1ccf7a31..f84d3177cd 100644 --- a/stepmania/src/GameState.h +++ b/stepmania/src/GameState.h @@ -159,7 +159,7 @@ public: // // Let a lot of classes access this info here so the don't have to keep their own copies. // - float m_fMusicSeconds; // time into the current song + float m_fMusicSeconds; // time into the current song, not scaled by music rate float m_fSongBeat; float m_fSongBeatNoOffset; float m_fCurBPS; diff --git a/stepmania/src/NoteData.cpp b/stepmania/src/NoteData.cpp index 32bec67906..ec2796b685 100644 --- a/stepmania/src/NoteData.cpp +++ b/stepmania/src/NoteData.cpp @@ -466,6 +466,12 @@ int NoteData::GetNumTapNotes( int iStartIndex, int iEndIndex ) const return iNumNotes; } +int NoteData::GetNumTapNotesInRow( int iRow ) const +{ + // Optimization opportunity: There's no need to instantiate row iterators if we're only checking one row. + return GetNumTapNotes( iRow, iRow+1 ); +} + int NoteData::GetNumRowsWithTap( int iStartIndex, int iEndIndex ) const { int iNumNotes = 0; diff --git a/stepmania/src/NoteData.h b/stepmania/src/NoteData.h index 1029aa74fa..deb71ee16d 100644 --- a/stepmania/src/NoteData.h +++ b/stepmania/src/NoteData.h @@ -182,6 +182,7 @@ public: float GetFirstBeat() const { return NoteRowToBeat( GetFirstRow() ); } float GetLastBeat() const { return NoteRowToBeat( GetLastRow() ); } int GetNumTapNotes( int iStartIndex = 0, int iEndIndex = MAX_NOTE_ROW ) const; + int GetNumTapNotesInRow( int iRow ) const; int GetNumMines( int iStartIndex = 0, int iEndIndex = MAX_NOTE_ROW ) const; int GetNumRowsWithTap( int iStartIndex = 0, int iEndIndex = MAX_NOTE_ROW ) const; int GetNumRowsWithTapOrHoldHead( int iStartIndex = 0, int iEndIndex = MAX_NOTE_ROW ) const; diff --git a/stepmania/src/Player.cpp b/stepmania/src/Player.cpp index 2d5ffdca47..4fa654d2c0 100644 --- a/stepmania/src/Player.cpp +++ b/stepmania/src/Player.cpp @@ -48,6 +48,7 @@ RString ATTACK_DISPLAY_X_NAME( size_t p, size_t both_sides ) { return "AttackDis /* Distance to search for a note in Step(), in seconds. */ static const float StepSearchDistance = 1.0f; static const float JUMP_WINDOW_SECONDS = 0.25f; +static const float HOPO_CHAIN_SECONDS = 0.5f; void TimingWindowSecondsInit( size_t /*TimingWindow*/ i, RString &sNameOut, float &defaultValueOut ) { @@ -1078,13 +1079,77 @@ void Player::Fret( int col, int row, const RageTimer &tm, bool bHeld, bool bRele DEBUG_ASSERT_M( col >= 0 && col <= m_NoteData.GetNumTracks(), ssprintf("%i, %i", col, m_NoteData.GetNumTracks()) ); m_vbFretIsDown[ col ] = !bRelease; + + // Handle hammer-ons and pull-offs + const float fPositionSeconds = GAMESTATE->m_fMusicSeconds - tm.Ago(); + int iHopoCol = -1; + bool bDoHopo = + m_pPlayerState->m_fLastHopoNoteMusicSeconds != -1 && + fPositionSeconds <= m_pPlayerState->m_fLastHopoNoteMusicSeconds + HOPO_CHAIN_SECONDS; + if( bDoHopo ) + { + // do a Hopo if: + // pressed fret if no higher fret is held + // on next lowest pressed fret when the highest held fret is released + bool bHigherFretIsDown = false; + for( int i=col+1; i=0; i-- ) + { + if( m_vbFretIsDown[i] ) + { + iHopoCol = i; + break; + } + } + if( iHopoCol == -1 ) + bDoHopo = false; + } + } + + if( bDoHopo ) + Hopo( iHopoCol, row, tm, bHeld, bRelease ); } -void Player::StepOrStrum( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease, Player::ButtonType pbt ) +void Player::StepStrumHopo( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease, Player::ButtonType pbt ) { if( IsOniDead() ) return; + switch( pbt ) + { + DEFAULT_FAIL(pbt); + case ButtonType_Strum: + if( bRelease ) + return; + break; + case ButtonType_Hopo: + bRelease = false; + break; + case ButtonType_Step: + break; + } const float fPositionSeconds = GAMESTATE->m_fMusicSeconds - tm.Ago(); const float fSongBeat = GAMESTATE->m_pCurSong ? GAMESTATE->m_pCurSong->GetBeatFromElapsedTime( fPositionSeconds ) : GAMESTATE->m_fSongBeat; @@ -1215,6 +1280,7 @@ void Player::StepOrStrum( int col, int row, const RageTimer &tm, bool bHeld, boo case ButtonType_Strum: iRowOfOverlappingNoteOrRow = GetClosestNonEmptyRow( BeatToNoteRow(fSongBeat), iStepSearchRows, iStepSearchRows, false ); break; + case ButtonType_Hopo: case ButtonType_Step: iRowOfOverlappingNoteOrRow = GetClosestNote( col, BeatToNoteRow(fSongBeat), iStepSearchRows, iStepSearchRows, false ); break; @@ -1261,6 +1327,7 @@ void Player::StepOrStrum( int col, int row, const RageTimer &tm, bool bHeld, boo case ButtonType_Strum: pTN = &tnDummy; break; + case ButtonType_Hopo: case ButtonType_Step: NoteData::iterator iter = m_NoteData.FindTapNote( col, iRowOfOverlappingNoteOrRow ); DEBUG_ASSERT( iter!= m_NoteData.end(col) ); @@ -1366,14 +1433,81 @@ void Player::StepOrStrum( int col, int row, const RageTimer &tm, bool bHeld, boo case ButtonType_Strum: { bool bNoteRowMatchesFrets = true; + int iFirstNoteCol = -1; for( int i=0; iGetCurrentStyle()->m_iColsPerPlayer; i++ ) { const TapNote &tn = m_NoteData.GetTapNote( i, iRowOfOverlappingNoteOrRow ); - bool bNoteMatchesFret = m_vbFretIsDown[i] == (tn.type != TapNote::empty); - bNoteRowMatchesFrets &= bNoteMatchesFret; + bool bIsNote = (tn.type != TapNote::empty); + if( iFirstNoteCol == -1 && bIsNote ) + iFirstNoteCol = i; + + // Extra notes to the left (higher up on the string) can be held without penalty. It's necessary to hold + // the extra frets or pull-offs. + if( iFirstNoteCol == -1 ) + continue; + + bool bNoteMatchesFret = m_vbFretIsDown[i] == bIsNote; + if( !bNoteMatchesFret ) + { + bNoteRowMatchesFrets = false; + break; + } } + ASSERT( iFirstNoteCol != -1 ); if( !bNoteRowMatchesFrets ) + { score = TNS_None; + } + else + { + m_pPlayerState->m_fLastHopoNoteMusicSeconds = fStepSeconds; + } + } + break; + case ButtonType_Hopo: + { + bool bDidHopo = true; + + { + // only can hopo on a row with one note + if( m_NoteData.GetNumTapNotesInRow(iRowOfOverlappingNoteOrRow) != 1 ) + { + bDidHopo = false; + goto done_checking_hopo; + } + + const TapNote &tn = m_NoteData.GetTapNote( col, iRowOfOverlappingNoteOrRow ); + if( tn.type == TapNote::empty ) + { + bDidHopo = false; + goto done_checking_hopo; + } + + int iRowsAgoLastNote = 100000; // TODO: find more reasonable value based on HOPO_CHAIN_SECONDS? + NoteData::all_tracks_reverse_iterator iter = m_NoteData.GetTapNoteRangeAllTracksReverse( iRowsAgoLastNote-iRowsAgoLastNote, iRowOfOverlappingNoteOrRow-1 ); + ASSERT( !iter.IsAtEnd() ); // there must have been a note that started the hopo + if( !NoteDataWithScoring::IsRowCompletelyJudged(m_NoteData, iter.Row(), m_pPlayerState->m_PlayerNumber) ) + { + bDidHopo = false; + goto done_checking_hopo; + } + + const TapNoteResult &lastTNR = NoteDataWithScoring::LastTapNoteWithResult( m_NoteData, iter.Row(), m_pPlayerState->m_PlayerNumber ).result; + if( lastTNR.tns <= TNS_Miss ) + { + bDidHopo = false; + goto done_checking_hopo; + } + } +done_checking_hopo: + if( !bDidHopo ) + { + score = TNS_None; + } + else + { + m_pPlayerState->m_fLastHopoNoteMusicSeconds = fStepSeconds; + } } break; case ButtonType_Step: @@ -1437,6 +1571,7 @@ void Player::StepOrStrum( int col, int row, const RageTimer &tm, bool bHeld, boo } } break; + case ButtonType_Hopo: case ButtonType_Step: pTN->result.tns = score; pTN->result.fTapNoteOffset = -fNoteOffset; @@ -1465,6 +1600,13 @@ void Player::StepOrStrum( int col, int row, const RageTimer &tm, bool bHeld, boo } } + // check for hopo end + if( score <= TNS_Miss ) + { + m_pPlayerState->m_fLastHopoNoteMusicSeconds = -1; + } + + if( score != TNS_None ) { /* Search for keyed sounds separately. If we can't find a nearby note, search @@ -1492,6 +1634,7 @@ void Player::StepOrStrum( int col, int row, const RageTimer &tm, bool bHeld, boo } break; case ButtonType_Step: + case ButtonType_Hopo: const TapNote &tn = m_NoteData.GetTapNote( col, iRowOfOverlappingNoteOrRow ); if( tn.iKeysoundIndex >= 0 && tn.iKeysoundIndex < (int) m_vKeysounds.size() ) m_vKeysounds[tn.iKeysoundIndex].Play(); @@ -1515,6 +1658,7 @@ void Player::StepOrStrum( int col, int row, const RageTimer &tm, bool bHeld, boo } break; case ButtonType_Step: + case ButtonType_Hopo: m_pNoteField->Step( col, score ); break; } diff --git a/stepmania/src/Player.h b/stepmania/src/Player.h index b862da53ff..938d15a025 100644 --- a/stepmania/src/Player.h +++ b/stepmania/src/Player.h @@ -113,11 +113,17 @@ public: void CrossedRow( int iNoteRow, const RageTimer &now ); void CrossedMineRow( int iNoteRow, const RageTimer &now ); bool IsOniDead() const; + + // Called when a fret, step, or strum type button changes void Fret( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease ); - enum ButtonType { ButtonType_Step, ButtonType_Strum }; - void StepOrStrum( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease, ButtonType gbt ); - void Step( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease ) { StepOrStrum(col, row, tm, bHeld, bRelease, ButtonType_Step); } - void Strum( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease ) { StepOrStrum(col, row, tm, bHeld, bRelease, ButtonType_Strum); } + enum ButtonType { ButtonType_Step, ButtonType_Strum, ButtonType_Hopo }; + void StepStrumHopo( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease, ButtonType gbt ); + void Step( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease ) { StepStrumHopo(col, row, tm, bHeld, bRelease, ButtonType_Step); } + void Strum( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease ) { StepStrumHopo(col, row, tm, bHeld, bRelease, ButtonType_Strum); } + + // called by Fret for Hammer-ons and Pull-offs + void Hopo( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease ) { StepStrumHopo(col, row, tm, bHeld, bRelease, ButtonType_Hopo); } + void RandomizeNotes( int iNoteRow ); void FadeToFail(); void CacheAllUsedNoteSkins(); diff --git a/stepmania/src/PlayerState.cpp b/stepmania/src/PlayerState.cpp index a3866458b9..73264d9d0e 100644 --- a/stepmania/src/PlayerState.cpp +++ b/stepmania/src/PlayerState.cpp @@ -19,6 +19,8 @@ void PlayerState::Reset() m_HealthState = ALIVE; + m_fLastHopoNoteMusicSeconds = -1; + m_PlayerController = PC_HUMAN; m_iCpuSkill = 5; diff --git a/stepmania/src/PlayerState.h b/stepmania/src/PlayerState.h index 5eb7e88f1c..b04458057b 100644 --- a/stepmania/src/PlayerState.h +++ b/stepmania/src/PlayerState.h @@ -34,6 +34,9 @@ public: enum HealthState { HOT, ALIVE, DANGER, DEAD }; HealthState m_HealthState; + // Set to the MusicSeconds of the last note successfully strummed or hammered in a hammer chain + float m_fLastHopoNoteMusicSeconds; // if -1, then there is no current hammer chain + PlayerController m_PlayerController; // @@ -51,6 +54,7 @@ public: float m_fSecondsUntilAttacksPhasedOut;// positive means PlayerAI is still affected bool m_bAttackBeganThisUpdate; // flag for other objects to watch (play sounds) bool m_bAttackEndedThisUpdate; // flag for other objects to watch (play sounds) + AttackArray m_ActiveAttacks; vector m_ModsToApply;