Files
itgmania212121/stepmania/src/Player.cpp
T

1900 lines
58 KiB
C++
Raw Normal View History

2003-02-16 04:01:45 +00:00
#include "global.h"
#include "Player.h"
2002-05-01 19:14:55 +00:00
#include "GameConstantsAndTypes.h"
#include "RageUtil.h"
2005-01-25 18:29:42 +00:00
#include "RageTimer.h"
2002-07-23 01:41:40 +00:00
#include "PrefsManager.h"
2002-05-01 19:14:55 +00:00
#include "GameConstantsAndTypes.h"
2002-04-16 17:31:00 +00:00
#include "GameManager.h"
#include "InputMapper.h"
2002-06-14 22:25:22 +00:00
#include "SongManager.h"
2002-07-23 01:41:40 +00:00
#include "GameState.h"
2002-11-03 07:17:03 +00:00
#include "ScoreKeeperMAX2.h"
2002-07-28 20:28:37 +00:00
#include "RageLog.h"
#include "RageMath.h"
#include "RageDisplay.h"
#include "ThemeManager.h"
2003-02-25 02:51:04 +00:00
#include "ScoreDisplay.h"
#include "LifeMeter.h"
2003-11-11 07:36:28 +00:00
#include "CombinedLifeMeter.h"
2003-04-07 22:07:44 +00:00
#include "PlayerAI.h"
2005-01-25 18:29:42 +00:00
#include "NoteField.h"
2003-04-14 22:32:08 +00:00
#include "NoteFieldPositioning.h"
2003-08-07 06:36:34 +00:00
#include "NoteDataUtil.h"
#include "ScreenGameplay.h" /* for SM_ComboStopped */
#include "ScreenManager.h"
#include "StageStats.h"
#include "ArrowEffects.h"
2004-07-25 17:07:32 +00:00
#include "Game.h"
#include "NetworkSyncManager.h" //used for sending timing offset
2004-08-19 01:18:11 +00:00
#include "DancingCharacters.h"
#include "ScreenDimensions.h"
2004-10-02 05:08:59 +00:00
#include "RageSoundManager.h"
#include "ThemeMetric.h"
#include "PlayerState.h"
#include "GameSoundManager.h"
2005-01-15 02:01:26 +00:00
#include "Style.h"
2005-02-15 07:09:07 +00:00
#include "MessageManager.h"
#include "ProfileManager.h"
2005-07-01 05:07:22 +00:00
#include "Profile.h"
#include "StatsManager.h"
2005-07-03 02:49:42 +00:00
#include "Steps.h"
#include "GameCommand.h"
2001-11-03 10:52:42 +00:00
2005-03-23 21:04:40 +00:00
CString COMBO_X_NAME( size_t p, size_t both_sides ) { return "ComboXOffset" + (both_sides ? CString("BothSides") : ssprintf("OneSideP%d",int(p+1)) ); }
CString ATTACK_DISPLAY_X_NAME( size_t p, size_t both_sides ){ return "AttackDisplayXOffset" + (both_sides ? CString("BothSides") : ssprintf("OneSideP%d",int(p+1)) ); }
2005-02-06 19:15:38 +00:00
2004-10-25 00:31:56 +00:00
/* Distance to search for a note in Step(), in seconds. */
static const float StepSearchDistance = 1.0f;
2002-02-24 01:43:11 +00:00
enum HoldWindow { HW_OK, HW_Roll };
enum TapWindow { TW_Marvelous, TW_Perfect, TW_Great, TW_Good, TW_Boo, TW_Mine, TW_Attack };
float AdjustedWindowTap( TapWindow tw, bool bIsPlayingBeginner )
{
float fSecs = 0;
switch( tw )
{
case TW_Marvelous: fSecs = PREFSMAN->m_fJudgeWindowSecondsMarvelous; break;
case TW_Perfect: fSecs = PREFSMAN->m_fJudgeWindowSecondsPerfect; break;
case TW_Great: fSecs = PREFSMAN->m_fJudgeWindowSecondsGreat; break;
case TW_Good: fSecs = PREFSMAN->m_fJudgeWindowSecondsGood; break;
case TW_Boo: fSecs = PREFSMAN->m_fJudgeWindowSecondsBoo; break;
case TW_Mine: fSecs = PREFSMAN->m_fJudgeWindowSecondsMine; break;
case TW_Attack: fSecs = PREFSMAN->m_fJudgeWindowSecondsAttack; break;
default: ASSERT(0);
}
fSecs *= PREFSMAN->m_fJudgeWindowScale;
fSecs += PREFSMAN->m_fJudgeWindowAdd;
if( bIsPlayingBeginner && PREFSMAN->m_bMercifulBeginner && tw==TW_Boo )
fSecs += 0.5f;
return fSecs;
}
2003-04-07 21:24:14 +00:00
float AdjustedWindowHold( HoldWindow hw, bool bIsPlayingBeginner )
{
float fSecs = 0;
switch( hw )
{
case HW_OK: fSecs = PREFSMAN->m_fJudgeWindowSecondsOK; break;
case HW_Roll: fSecs = PREFSMAN->m_fJudgeWindowSecondsRoll; break;
default: ASSERT(0);
}
fSecs *= PREFSMAN->m_fJudgeWindowScale;
fSecs += PREFSMAN->m_fJudgeWindowAdd;
return fSecs;
}
#define ADJUSTED_WINDOW_TAP( tw ) AdjustedWindowTap( tw, IsPlayingBeginner() )
#define ADJUSTED_WINDOW_HOLD( hw ) AdjustedWindowHold( hw, IsPlayingBeginner() )
2003-04-07 21:24:14 +00:00
Player::Player( bool bShowNoteField, bool bShowJudgment )
2001-11-03 10:52:42 +00:00
{
2005-07-22 21:04:27 +00:00
m_bLoaded = false;
m_pPlayerState = NULL;
m_pPlayerStageStats = NULL;
m_fNoteFieldHeight = 0;
2001-11-20 11:49:10 +00:00
2002-04-28 20:42:32 +00:00
m_pLifeMeter = NULL;
2003-06-30 18:08:27 +00:00
m_pCombinedLifeMeter = NULL;
2003-11-26 06:40:03 +00:00
m_pScoreDisplay = NULL;
m_pSecondaryScoreDisplay = NULL;
2003-06-30 18:08:27 +00:00
m_pPrimaryScoreKeeper = NULL;
m_pSecondaryScoreKeeper = NULL;
2003-02-25 02:51:04 +00:00
m_pInventory = NULL;
2002-11-03 07:17:03 +00:00
2005-03-11 03:54:40 +00:00
m_bPaused = false;
m_iOffsetSample = 0;
m_pJudgment = NULL;
if( bShowJudgment )
{
m_pJudgment = new Judgment;
this->AddChild( m_pJudgment );
}
m_pCombo = NULL;
if( bShowNoteField )
{
m_pCombo = new Combo;
this->AddChild( m_pCombo );
}
m_pAttackDisplay = NULL;
if( bShowNoteField )
{
m_pAttackDisplay = new AttackDisplay;
this->AddChild( m_pAttackDisplay );
}
PlayerAI::InitFromDisk();
2005-01-15 01:03:48 +00:00
m_pNoteField = NULL;
if( bShowNoteField )
{
m_pNoteField = new NoteField;
}
2005-05-19 23:29:39 +00:00
this->SubscribeToMessage( Message_AutosyncChanged );
2001-11-03 10:52:42 +00:00
}
2005-01-15 01:03:48 +00:00
Player::~Player()
2002-11-03 07:17:03 +00:00
{
SAFE_DELETE( m_pJudgment );
SAFE_DELETE( m_pCombo );
SAFE_DELETE( m_pAttackDisplay );
SAFE_DELETE( m_pNoteField );
2002-11-03 07:17:03 +00:00
}
/* Init() does the expensive stuff: load sounds and note skins. Load() just loads a NoteData. */
void Player::Init(
2005-02-26 05:59:12 +00:00
const CString &sType,
PlayerState* pPlayerState,
PlayerStageStats* pPlayerStageStats,
2004-10-23 17:43:49 +00:00
LifeMeter* pLM,
CombinedLifeMeter* pCombinedLM,
ScoreDisplay* pScoreDisplay,
ScoreDisplay* pSecondaryScoreDisplay,
Inventory* pInventory,
ScoreKeeper* pPrimaryScoreKeeper,
2005-01-15 01:03:48 +00:00
ScoreKeeper* pSecondaryScoreKeeper )
2002-02-02 05:11:12 +00:00
{
2005-02-26 05:59:12 +00:00
GRAY_ARROWS_Y_STANDARD.Load(sType,"ReceptorArrowsYStandard");
GRAY_ARROWS_Y_REVERSE.Load(sType,"ReceptorArrowsYReverse");
COMBO_X.Load(sType,COMBO_X_NAME,NUM_PLAYERS,2);
COMBO_Y.Load(sType,"ComboY");
COMBO_Y_REVERSE.Load(sType,"ComboYReverse");
2005-04-05 21:40:30 +00:00
COMBO_CENTERED_ADDY.Load(sType,"ComboCenteredAddY");
COMBO_CENTERED_ADDY_REVERSE.Load(sType,"ComboCenteredAddYReverse");
2005-02-26 05:59:12 +00:00
ATTACK_DISPLAY_X.Load(sType,ATTACK_DISPLAY_X_NAME,NUM_PLAYERS,2);
ATTACK_DISPLAY_Y.Load(sType,"AttackDisplayY");
ATTACK_DISPLAY_Y_REVERSE.Load(sType,"AttackDisplayYReverse");
HOLD_JUDGMENT_Y_STANDARD.Load(sType,"HoldJudgmentYStandard");
HOLD_JUDGMENT_Y_REVERSE.Load(sType,"HoldJudgmentYReverse");
BRIGHT_GHOST_COMBO_THRESHOLD.Load(sType,"BrightGhostComboThreshold");
TAP_JUDGMENTS_UNDER_FIELD.Load(sType,"TapJudgmentsUnderField");
HOLD_JUDGMENTS_UNDER_FIELD.Load(sType,"HoldJudgmentsUnderField");
START_DRAWING_AT_PIXELS.Load(sType,"StartDrawingAtPixels");
STOP_DRAWING_AT_PIXELS.Load(sType,"StopDrawingAtPixels");
MAX_PRO_TIMING_ERROR.Load(sType,"MaxProTimingError");
if( m_pJudgment )
{
//
// Init judgment positions
//
LuaExpression expr;
expr.SetFromExpression( THEME->GetMetric(sType,"JudgmentTransformCommandFunction") );
bool bPlayerUsingBothSides = GAMESTATE->GetCurrentStyle()->m_StyleType==ONE_PLAYER_TWO_SIDES;
Actor temp;
int iEnabledPlayerIndex = -1;
int iNumEnabledPlayers = 0;
if( GAMESTATE->m_bMultiplayer )
{
FOREACH_EnabledMultiPlayer( p )
{
if( p == pPlayerState->m_mp )
iEnabledPlayerIndex = iNumEnabledPlayers;
iNumEnabledPlayers++;
}
}
else
{
FOREACH_EnabledPlayer( p )
{
if( p == pPlayerState->m_PlayerNumber )
iEnabledPlayerIndex = iNumEnabledPlayers;
iNumEnabledPlayers++;
}
}
if( iNumEnabledPlayers == 0 ) // hack for ScreenHowToPlay where no players are joined
{
iEnabledPlayerIndex = 0;
iNumEnabledPlayers = 1;
}
for( int i=0; i<NUM_REVERSE; i++ )
{
for( int j=0; j<NUM_CENTERED; j++ )
{
Lua *L = LUA->Get();
expr.PushSelf( L );
ASSERT( !lua_isnil(L, -1) );
temp.PushSelf( L );
LuaHelpers::Push( pPlayerState->m_PlayerNumber, L );
LuaHelpers::Push( pPlayerState->m_mp, L );
LuaHelpers::Push( iEnabledPlayerIndex, L );
LuaHelpers::Push( iNumEnabledPlayers, L );
LuaHelpers::Push( bPlayerUsingBothSides, L );
LuaHelpers::Push( !!i, L );
LuaHelpers::Push( !!j, L );
lua_call( L, 8, 0 ); // 8 args, 0 results
LUA->Release(L);
m_tsJudgment[i][j] = temp.DestTweenState();
}
}
// Load Judgment frame
if( GAMESTATE->m_bMultiplayer && !m_sprJudgmentFrame.IsLoaded() ) // only load the first time
{
GameCommand gc;
ASSERT( pPlayerState->m_mp != MultiPlayer_INVALID );
gc.m_MultiPlayer = pPlayerState->m_mp;
Lua *L = LUA->Get();
gc.PushSelf( L );
lua_setglobal( L, "ThisGameCommand" );
LUA->Release( L );
m_sprJudgmentFrame.Load( THEME->GetPathG(sType,"JudgmentFrame") );
LUA->UnsetGlobal( "ThisGameCommand" );
this->AddChild( m_sprJudgmentFrame );
}
}
m_pPlayerState = pPlayerState;
m_pPlayerStageStats = pPlayerStageStats;
2002-07-28 20:28:37 +00:00
m_pLifeMeter = pLM;
2003-06-30 18:08:27 +00:00
m_pCombinedLifeMeter = pCombinedLM;
2003-11-26 06:40:03 +00:00
m_pScoreDisplay = pScoreDisplay;
m_pSecondaryScoreDisplay = pSecondaryScoreDisplay;
2003-02-25 02:51:04 +00:00
m_pInventory = pInventory;
2003-06-30 18:08:27 +00:00
m_pPrimaryScoreKeeper = pPrimaryScoreKeeper;
m_pSecondaryScoreKeeper = pSecondaryScoreKeeper;
2005-02-15 07:09:07 +00:00
// TODO: Remove use of PlayerNumber.
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
m_sMessageToSendOnStep = ssprintf("StepP%d",pn+1);
2005-02-26 05:59:12 +00:00
m_soundMine.Load( THEME->GetPathS(sType,"mine"), true );
/* Attacks can be launched in course modes and in battle modes. They both come
* here to play, but allow loading a different sound for different modes. */
switch( GAMESTATE->m_PlayMode )
{
case PLAY_MODE_RAVE:
case PLAY_MODE_BATTLE:
2005-02-26 05:59:12 +00:00
m_soundAttackLaunch.Load( THEME->GetPathS(sType,"battle attack launch"), true );
m_soundAttackEnding.Load( THEME->GetPathS(sType,"battle attack ending"), true );
break;
default:
2005-02-26 05:59:12 +00:00
m_soundAttackLaunch.Load( THEME->GetPathS(sType,"course attack launch"), true );
m_soundAttackEnding.Load( THEME->GetPathS(sType,"course attack ending"), true );
break;
}
RageSoundParams p;
GameSoundManager::SetPlayerBalance( pn, p );
m_soundMine.SetParams( p );
m_soundAttackLaunch.SetParams( p );
m_soundAttackEnding.SetParams( p );
2005-02-26 05:45:51 +00:00
if( m_pCombo )
{
m_pCombo->SetName( "Combo" );
m_pCombo->Load( m_pPlayerState, m_pPlayerStageStats );
ActorUtil::OnCommand( m_pCombo, sType );
}
if( m_pJudgment )
{
m_pJudgment->SetName( "Judgment" );
m_pJudgment->Load( IsPlayingBeginner() );
ActorUtil::OnCommand( m_pJudgment, sType );
}
2005-02-26 06:31:56 +00:00
m_fNoteFieldHeight = GRAY_ARROWS_Y_REVERSE-GRAY_ARROWS_Y_STANDARD;
if( m_pNoteField )
m_pNoteField->Init( m_pPlayerState, m_fNoteFieldHeight );
}
void Player::Load( const NoteData& noteData )
{
2005-07-22 21:04:27 +00:00
m_bLoaded = true;
m_LastTapNoteScore = TNS_NONE;
m_iRowLastCrossed = BeatToNoteRowNotRounded( GAMESTATE->m_fSongBeat ) - 1; // why this?
m_iMineRowLastCrossed = BeatToNoteRowNotRounded( GAMESTATE->m_fSongBeat ) - 1; // why this?
2002-07-28 20:28:37 +00:00
// TODO: Remove use of PlayerNumber.
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
GAMESTATE->ResetNoteSkinsForPlayer( m_pPlayerState );
2004-10-23 17:43:49 +00:00
// init steps
m_NoteData.Init();
bool bOniDead = GAMESTATE->m_SongOptions.m_LifeType == SongOptions::LIFE_BATTERY &&
(m_pPlayerStageStats == NULL || m_pPlayerStageStats->bFailed);
2004-10-23 17:43:49 +00:00
if( !bOniDead )
m_NoteData.CopyAll( noteData );
2002-07-28 20:28:37 +00:00
2002-08-26 08:11:52 +00:00
/* 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 */
if( m_pJudgment )
m_pJudgment->StopTweening();
// m_pCombo->Reset(); // don't reset combos between songs in a course!
if( m_pPlayerStageStats )
{
if( m_pCombo )
m_pCombo->SetCombo( m_pPlayerStageStats->iCurCombo, m_pPlayerStageStats->iCurMissCombo ); // combo can persist between songs and games
}
if( m_pAttackDisplay )
m_pAttackDisplay->Init( m_pPlayerState );
if( m_pJudgment )
m_pJudgment->Reset();
2002-07-28 20:28:37 +00:00
2003-10-10 23:44:17 +00:00
/* Don't re-init this; that'll reload graphics. Add a separate Reset() call
* if some ScoreDisplays need it. */
// if( m_pScore )
// m_pScore->Init( pn );
2002-04-28 20:42:32 +00:00
2003-08-13 19:17:28 +00:00
/* Apply transforms. */
NoteDataUtil::TransformNoteData( m_NoteData, GAMESTATE->m_pPlayerState[pn]->m_PlayerOptions, GAMESTATE->GetCurrentStyle()->m_StepsType );
2003-11-20 06:50:05 +00:00
switch( GAMESTATE->m_PlayMode )
{
2005-01-15 03:10:08 +00:00
case PLAY_MODE_RAVE:
case PLAY_MODE_BATTLE:
{
// ugly, ugly, ugly. Works only w/ dance.
NoteDataUtil::TransformNoteData( m_NoteData, GAMESTATE->m_pPlayerState[pn]->m_PlayerOptions, GAMESTATE->GetCurrentStyle()->m_StepsType );
// shuffle either p1 or p2
static int count = 0;
switch( count )
2003-11-20 06:50:05 +00:00
{
2005-01-15 03:10:08 +00:00
case 0:
case 3:
NoteDataUtil::Turn( m_NoteData, STEPS_TYPE_DANCE_SINGLE, NoteDataUtil::left);
break;
case 1:
case 2:
NoteDataUtil::Turn( m_NoteData, STEPS_TYPE_DANCE_SINGLE, NoteDataUtil::right);
break;
default:
ASSERT(0);
2003-11-20 06:50:05 +00:00
}
2005-01-15 03:10:08 +00:00
count++;
count %= 4;
}
break;
2003-11-20 06:50:05 +00:00
}
2001-11-30 19:08:44 +00:00
int iStartDrawingAtPixels = GAMESTATE->IsEditing() ? -100 : START_DRAWING_AT_PIXELS;
int iStopDrawingAtPixels = GAMESTATE->IsEditing() ? 400 : STOP_DRAWING_AT_PIXELS;
2005-01-15 01:09:08 +00:00
float fNoteFieldMiddle = (GRAY_ARROWS_Y_STANDARD+GRAY_ARROWS_Y_REVERSE)/2;
if( m_pNoteField )
{
2005-01-15 01:09:08 +00:00
m_pNoteField->SetY( fNoteFieldMiddle );
m_pNoteField->Load( &m_NoteData, iStartDrawingAtPixels, iStopDrawingAtPixels );
}
2001-12-20 12:37:38 +00:00
const bool bReverse = GAMESTATE->m_pPlayerState[pn]->m_PlayerOptions.GetReversePercentForColumn(0) == 1;
bool bPlayerUsingBothSides = GAMESTATE->GetCurrentStyle()->m_StyleType==ONE_PLAYER_TWO_SIDES;
if( m_pCombo )
{
m_pCombo->SetX( COMBO_X.GetValue(pn,bPlayerUsingBothSides) );
m_pCombo->SetY( bReverse ? COMBO_Y_REVERSE : COMBO_Y );
}
if( m_pAttackDisplay )
m_pAttackDisplay->SetX( ATTACK_DISPLAY_X.GetValue(pn,bPlayerUsingBothSides) - 40 );
// set this in Update //m_pAttackDisplay->SetY( bReverse ? ATTACK_DISPLAY_Y_REVERSE : ATTACK_DISPLAY_Y );
// set this in Update
//m_pJudgment->SetX( JUDGMENT_X.GetValue(pn,bPlayerUsingBothSides) );
//m_pJudgment->SetY( bReverse ? JUDGMENT_Y_REVERSE : JUDGMENT_Y );
2001-12-20 12:37:38 +00:00
2003-08-14 08:13:08 +00:00
// Need to set Y positions of all these elements in Update since
// they change depending on PlayerOptions.
2003-04-22 04:54:04 +00:00
// Load HoldJudgments
FOREACH( HoldJudgment, m_vHoldJudgment, i )
2005-08-26 20:52:03 +00:00
this->RemoveChild( &*i );
m_vHoldJudgment.resize( GAMESTATE->GetCurrentStyle()->m_iColsPerPlayer );
FOREACH( HoldJudgment, m_vHoldJudgment, i )
2005-08-26 20:52:03 +00:00
this->AddChild( &*i );
2004-10-23 23:41:49 +00:00
//
2005-01-15 03:10:08 +00:00
// Load keysounds. If sounds are already loaded (as in the editor), don't reload them.
// XXX: the editor will load several duplicate copies (in each NoteField), and each
// player will load duplicate sounds. Does this belong somewhere else (perhaps in
// a separate object, used alongside ScreenGameplay::m_pSoundMusic and ScreenEdit::m_pSoundMusic?)
// We don't have to load separate copies to set player fade: always make a copy, and set the
// fade on the copy.
2004-10-23 23:41:49 +00:00
//
const Song* pSong = GAMESTATE->m_pCurSong;
2004-10-23 23:41:49 +00:00
CString sSongDir = pSong->GetSongDir();
m_vKeysounds.resize( pSong->m_vsKeysoundFile.size() );
RageSoundParams p;
GameSoundManager::SetPlayerBalance( pn, p );
2004-10-23 23:41:49 +00:00
for( unsigned i=0; i<m_vKeysounds.size(); i++ )
{
CString sKeysoundFilePath = sSongDir + pSong->m_vsKeysoundFile[i];
RageSound& sound = m_vKeysounds[i];
2005-01-15 03:10:08 +00:00
if( sound.GetLoadedFilePath() != sKeysoundFilePath )
sound.Load( sKeysoundFilePath, true );
2004-10-23 23:41:49 +00:00
sound.SetParams( p );
}
}
2001-11-03 10:52:42 +00:00
2005-01-15 01:03:48 +00:00
void Player::Update( float fDeltaTime )
{
2005-07-22 21:04:27 +00:00
// Don't update if we havn't been loaded yet.
if( !m_bLoaded )
return;
2005-01-15 01:03:48 +00:00
//LOG->Trace( "Player::Update(%f)", fDeltaTime );
if( GAMESTATE->m_pCurSong==NULL )
return;
2005-03-11 03:54:40 +00:00
ActorFrame::Update( fDeltaTime );
if( m_pPlayerState->m_bAttackBeganThisUpdate )
2004-03-24 07:37:35 +00:00
m_soundAttackLaunch.Play();
if( m_pPlayerState->m_bAttackEndedThisUpdate )
2003-11-20 06:50:05 +00:00
m_soundAttackEnding.Play();
2002-07-28 20:28:37 +00:00
const float fSongBeat = GAMESTATE->m_fSongBeat;
2003-12-16 04:00:39 +00:00
const int iSongRow = BeatToNoteRow( fSongBeat );
2002-06-23 11:43:53 +00:00
if( m_pNoteField )
m_pNoteField->Update( fDeltaTime );
2003-08-14 08:13:08 +00:00
//
// Update Y positions
//
{
2004-06-28 07:26:00 +00:00
for( int c=0; c<GAMESTATE->GetCurrentStyle()->m_iColsPerPlayer; c++ )
{
float fPercentReverse = m_pPlayerState->m_CurrentPlayerOptions.GetReversePercentForColumn(c);
float fHoldJudgeYPos = SCALE( fPercentReverse, 0.f, 1.f, HOLD_JUDGMENT_Y_STANDARD, HOLD_JUDGMENT_Y_REVERSE );
2004-02-26 03:18:51 +00:00
// float fGrayYPos = SCALE( fPercentReverse, 0.f, 1.f, GRAY_ARROWS_Y_STANDARD, GRAY_ARROWS_Y_REVERSE );
const float fX = ArrowEffects::GetXPos( m_pPlayerState, c, 0 );
const float fZ = ArrowEffects::GetZPos( m_pPlayerState, c, 0 );
m_vHoldJudgment[c].SetX( fX );
m_vHoldJudgment[c].SetY( fHoldJudgeYPos );
m_vHoldJudgment[c].SetZ( fZ );
}
}
// NoteField accounts for reverse on its own now.
//if( m_pNoteField )
// m_pNoteField->SetY( fGrayYPos );
2003-08-14 08:13:08 +00:00
2005-04-05 21:40:30 +00:00
const bool bReverse = m_pPlayerState->m_PlayerOptions.GetReversePercentForColumn(0) == 1;
float fPercentCentered = m_pPlayerState->m_CurrentPlayerOptions.m_fScrolls[PlayerOptions::SCROLL_CENTERED];
if( m_pCombo )
{
m_pCombo->SetY(
bReverse ?
COMBO_Y_REVERSE + fPercentCentered * COMBO_CENTERED_ADDY_REVERSE :
COMBO_Y + fPercentCentered * COMBO_CENTERED_ADDY );
}
if( m_pJudgment )
{
const Actor::TweenState &ts1 = m_tsJudgment[bReverse?1:0][0];
const Actor::TweenState &ts2 = m_tsJudgment[bReverse?1:0][1];
Actor::TweenState::MakeWeightedAverage( m_pJudgment->DestTweenState(), ts1, ts2, fPercentCentered );
if( m_sprJudgmentFrame.IsLoaded() )
Actor::TweenState::MakeWeightedAverage( m_sprJudgmentFrame->DestTweenState(), ts1, ts2, fPercentCentered );
}
2005-04-05 21:40:30 +00:00
float fMiniPercent = m_pPlayerState->m_CurrentPlayerOptions.m_fEffects[PlayerOptions::EFFECT_MINI];
float fNoteFieldZoom = 1 - fMiniPercent*0.5f;
float fJudgmentZoom = 1 - fMiniPercent*0.25f;
if( m_pNoteField )
m_pNoteField->SetZoom( fNoteFieldZoom );
if( m_pJudgment )
m_pJudgment->SetZoom( m_pJudgment->GetZoom() * fJudgmentZoom );
if( m_sprJudgmentFrame.IsLoaded() )
m_sprJudgmentFrame->SetZoom( m_sprJudgmentFrame->GetZoom() * fJudgmentZoom );
2003-08-14 08:13:08 +00:00
2005-03-11 03:54:40 +00:00
// If we're paused, don't update tap or hold note logic, so hold notes can be released
// during pause.
if( m_bPaused )
return;
2002-02-24 01:43:11 +00:00
//
// Check for TapNote misses
2002-02-24 01:43:11 +00:00
//
2004-05-24 04:26:54 +00:00
UpdateTapNotesMissedOlderThan( GetMaxStepDistanceSeconds() );
2003-12-22 10:30:10 +00:00
//
// update pressed flag
//
2004-06-28 07:26:00 +00:00
const int iNumCols = GAMESTATE->GetCurrentStyle()->m_iColsPerPlayer;
2004-05-31 01:03:45 +00:00
ASSERT_M( iNumCols < MAX_COLS_PER_PLAYER, ssprintf("%i >= %i", iNumCols, MAX_COLS_PER_PLAYER) );
for( int col=0; col < iNumCols; ++col )
{
// TODO: Remove use of PlayerNumber.
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
2004-05-31 01:03:45 +00:00
CHECKPOINT_M( ssprintf("%i %i", col, iNumCols) );
const StyleInput StyleI( pn, col );
2004-06-28 07:26:00 +00:00
const GameInput GameI = GAMESTATE->GetCurrentStyle()->StyleInputToGameInput( StyleI );
bool bIsHoldingButton = INPUTMAPPER->IsButtonDown( GameI );
2004-01-01 04:06:37 +00:00
// TODO: Make this work for non-human-controlled players
if( bIsHoldingButton && !GAMESTATE->m_bDemonstrationOrJukebox && m_pPlayerState->m_PlayerController==PC_HUMAN )
if( m_pNoteField )
m_pNoteField->SetPressed( col );
}
2004-05-30 22:55:05 +00:00
//
// handle Autoplay for rolls
//
if( m_pPlayerState->m_PlayerController != PC_HUMAN )
{
for( int iTrack=0; iTrack<m_NoteData.GetNumTracks(); ++iTrack )
{
// TODO: Make the CPU miss sometimes.
int iHeadRow;
2005-07-22 00:14:24 +00:00
if( !m_NoteData.IsHoldNoteAtRow(iTrack, iSongRow, &iHeadRow) )
continue;
const TapNote &tn = m_NoteData.GetTapNote( iTrack, iHeadRow );
if( tn.type != TapNote::hold_head || tn.subType != TapNote::hold_head_roll )
continue;
if( tn.HoldResult.hns != HNS_NONE )
continue;
if( tn.HoldResult.fLife >= 0.5f )
continue;
RageTimer now;
Step( iTrack, now, false );
}
}
2002-02-24 01:43:11 +00:00
//
// update HoldNotes logic
2002-02-24 01:43:11 +00:00
//
2005-01-25 05:02:35 +00:00
for( int iTrack=0; iTrack<m_NoteData.GetNumTracks(); ++iTrack )
2001-12-19 01:50:57 +00:00
{
2005-01-25 05:02:35 +00:00
// Since this is being called every frame, let's not check the whole array every time.
// Instead, only check 1 beat back. Even 1 is overkill.
const int iStartCheckingAt = max( 0, iSongRow-BeatToNoteRow(1) );
NoteData::iterator begin, end;
m_NoteData.GetTapNoteRangeInclusive( iTrack, iStartCheckingAt, iSongRow+1, begin, end );
2005-01-25 05:02:35 +00:00
for( ; begin != end; ++begin )
{
TapNote &tn = begin->second;
if( tn.type != TapNote::hold_head )
continue;
int iRow = begin->first;
2005-01-25 05:02:35 +00:00
// set hold flags so NoteField can do intelligent drawing
tn.HoldResult.bHeld = false;
tn.HoldResult.bActive = false;
2005-01-25 05:02:35 +00:00
HoldNoteScore hns = tn.HoldResult.hns;
if( hns != HNS_NONE ) // if this HoldNote already has a result
continue; // we don't need to update the logic for this one
2001-12-19 01:50:57 +00:00
2005-01-25 05:02:35 +00:00
// TODO: Remove use of PlayerNumber.
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
2005-01-25 05:02:35 +00:00
// if they got a bad score or haven't stepped on the corresponding tap yet
const TapNoteScore tns = tn.result.tns;
const bool bSteppedOnTapNote = tns != TNS_NONE && tns != TNS_MISS; // did they step on the start of this hold?
2005-04-26 20:21:13 +00:00
bool bIsHoldingButton = false;
2005-01-25 05:02:35 +00:00
if( m_pPlayerState->m_PlayerController != PC_HUMAN )
{
// TODO: Make the CPU miss sometimes.
bIsHoldingButton = true;
2005-01-25 05:02:35 +00:00
}
else
{
const StyleInput StyleI( pn, iTrack );
const GameInput GameI = GAMESTATE->GetCurrentStyle()->StyleInputToGameInput( StyleI );
bIsHoldingButton = INPUTMAPPER->IsButtonDown( GameI );
}
2005-01-25 05:02:35 +00:00
int iEndRow = iRow + tn.iDuration;
2002-04-16 17:31:00 +00:00
2005-01-25 05:02:35 +00:00
float fLife = tn.HoldResult.fLife;
if( bSteppedOnTapNote && fLife != 0 )
2001-12-19 01:50:57 +00:00
{
2005-01-25 05:02:35 +00:00
/* This hold note is not judged and we stepped on its head. Update iLastHeldRow.
* Do this even if we're a little beyond the end of the hold note, to make sure
* iLastHeldRow is clamped to iEndRow if the hold note is held all the way. */
tn.HoldResult.iLastHeldRow = min( iSongRow, iEndRow );
2001-12-19 01:50:57 +00:00
}
2005-01-25 05:02:35 +00:00
// If the song beat is in the range of this hold:
if( iRow <= iSongRow && iRow <= iEndRow )
2001-12-19 01:50:57 +00:00
{
2005-04-20 01:52:55 +00:00
switch( tn.subType )
2005-01-25 05:02:35 +00:00
{
2005-04-20 01:52:55 +00:00
case TapNote::hold_head_hold:
// set hold flag so NoteField can do intelligent drawing
tn.HoldResult.bHeld = bIsHoldingButton && bSteppedOnTapNote;
tn.HoldResult.bActive = bSteppedOnTapNote;
if( bSteppedOnTapNote && bIsHoldingButton )
{
// Increase life
fLife = 1;
}
else
{
// Decrease life
fLife -= fDeltaTime/ADJUSTED_WINDOW_HOLD(HW_OK);
2005-04-20 01:52:55 +00:00
fLife = max( fLife, 0 ); // clamp
}
break;
case TapNote::hold_head_roll:
tn.HoldResult.bHeld = true;
tn.HoldResult.bActive = bSteppedOnTapNote;
// give positive life in Step(), not here.
2005-01-25 05:02:35 +00:00
2002-06-14 22:25:22 +00:00
// Decrease life
fLife -= fDeltaTime/ADJUSTED_WINDOW_HOLD(HW_Roll);
2002-06-14 22:25:22 +00:00
fLife = max( fLife, 0 ); // clamp
2005-04-20 01:52:55 +00:00
break;
default:
ASSERT(0);
2005-01-25 05:02:35 +00:00
}
2001-12-19 01:50:57 +00:00
}
2005-01-25 05:02:35 +00:00
/* 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;
2001-12-19 01:50:57 +00:00
2005-01-25 05:02:35 +00:00
// check for OK
if( iSongRow >= iEndRow && bSteppedOnTapNote && fLife > 0 ) // if this HoldNote is in the past
{
fLife = 1;
hns = HNS_OK;
2005-04-26 05:28:32 +00:00
bool bBright = m_pPlayerStageStats && m_pPlayerStageStats->iCurCombo>(int)BRIGHT_GHOST_COMBO_THRESHOLD;
2005-01-25 05:02:35 +00:00
if( m_pNoteField )
2005-04-26 05:28:32 +00:00
m_pNoteField->DidHoldNote( iTrack, HNS_OK, bBright ); // bright ghost flash
2005-01-25 05:02:35 +00:00
}
2002-12-18 22:25:24 +00:00
2005-01-25 05:02:35 +00:00
if( hns != HNS_NONE )
{
/* this note has been judged */
HandleHoldScore( hns, tns );
m_vHoldJudgment[iTrack].SetHoldJudgment( hns );
2003-08-01 01:18:08 +00:00
2005-01-25 05:02:35 +00:00
int ms_error = (hns == HNS_OK)? 0:MAX_PRO_TIMING_ERROR;
2003-08-01 01:18:08 +00:00
2005-01-25 05:02:35 +00:00
if( m_pPlayerStageStats )
m_pPlayerStageStats->iTotalError += ms_error;
}
2002-12-18 22:25:24 +00:00
2005-01-25 05:02:35 +00:00
tn.HoldResult.fLife = fLife;
tn.HoldResult.hns = hns;
}
2001-12-19 01:50:57 +00:00
}
// TODO: Remove use of PlayerNumber.
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
{
// Why was this originally "BeatToNoteRowNotRounded"? It should be rounded. -Chris
/* We want to send the crossed row message exactly when we cross the row--not
* .5 before the row. Use a very slow song (around 2 BPM) as a test case: without
* rounding, autoplay steps early. -glenn */
const int iRowNow = BeatToNoteRowNotRounded( GAMESTATE->m_fSongBeat );
if( iRowNow >= 0 )
{
// for each index we crossed since the last update
if( GAMESTATE->IsPlayerEnabled(pn) )
2005-01-28 04:37:48 +00:00
FOREACH_NONEMPTY_ROW_ALL_TRACKS_RANGE( m_NoteData, r, m_iRowLastCrossed, iRowNow+1 )
CrossedRow( r );
2005-01-28 04:37:48 +00:00
m_iRowLastCrossed = iRowNow+1;
}
}
{
// TRICKY:
float fPositionSeconds = GAMESTATE->m_fMusicSeconds;
fPositionSeconds -= PREFSMAN->m_fPadStickSeconds;
const float fSongBeat = GAMESTATE->m_pCurSong ? GAMESTATE->m_pCurSong->GetBeatFromElapsedTime( fPositionSeconds ) : 0;
const int iRowNow = BeatToNoteRowNotRounded( fSongBeat );
if( iRowNow >= 0 )
{
// for each index we crossed since the last update
if( GAMESTATE->IsPlayerEnabled(pn) )
2005-01-28 04:37:48 +00:00
FOREACH_NONEMPTY_ROW_ALL_TRACKS_RANGE( m_NoteData, r, m_iMineRowLastCrossed, iRowNow+1 )
CrossedMineRow( r );
2005-01-28 04:37:48 +00:00
m_iMineRowLastCrossed = iRowNow+1;
}
}
2003-08-18 02:37:43 +00:00
// process transforms that are waiting to be applied
2003-12-23 02:17:28 +00:00
ApplyWaitingTransforms();
}
2005-01-15 01:03:48 +00:00
void Player::ApplyWaitingTransforms()
2003-12-23 02:17:28 +00:00
{
for( unsigned j=0; j<m_pPlayerState->m_ModsToApply.size(); j++ )
2003-08-18 02:37:43 +00:00
{
const Attack &mod = m_pPlayerState->m_ModsToApply[j];
2003-10-25 07:58:10 +00:00
PlayerOptions po;
/* Should this default to "" always? need it blank so we know if mod.sModifier
* changes the note skin. */
po.m_sNoteSkin = "";
2005-05-20 08:57:59 +00:00
po.FromString( mod.sModifiers );
2003-08-18 02:37:43 +00:00
2003-10-25 07:58:10 +00:00
float fStartBeat, fEndBeat;
mod.GetAttackBeats( GAMESTATE->m_pCurSong, m_pPlayerState, fStartBeat, fEndBeat );
2004-10-23 17:43:49 +00:00
fEndBeat = min( fEndBeat, m_NoteData.GetLastBeat() );
2003-10-25 07:58:10 +00:00
2005-05-20 08:57:59 +00:00
LOG->Trace( "Applying transform '%s' from %f to %f to '%s'", mod.sModifiers.c_str(), fStartBeat, fEndBeat,
2003-10-27 22:26:34 +00:00
GAMESTATE->m_pCurSong->GetTranslitMainTitle().c_str() );
2003-10-25 07:58:10 +00:00
if( po.m_sNoteSkin != "" )
GAMESTATE->SetNoteSkinForBeatRange( m_pPlayerState, po.m_sNoteSkin, fStartBeat, fEndBeat );
NoteDataUtil::TransformNoteData( m_NoteData, po, GAMESTATE->GetCurrentStyle()->m_StepsType, BeatToNoteRow(fStartBeat), BeatToNoteRow(fEndBeat) );
2003-08-18 02:37:43 +00:00
}
m_pPlayerState->m_ModsToApply.clear();
// Cache used NoteSkins now, not on the next update.
if( m_pNoteField )
m_pNoteField->RefreshBeatToNoteSkin();
2001-11-03 10:52:42 +00:00
}
2005-01-15 01:03:48 +00:00
void Player::DrawPrimitives()
2002-02-24 10:31:20 +00:00
{
// TODO: Remove use of PlayerNumber.
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
2003-06-13 04:31:01 +00:00
// May have both players in doubles (for battle play); only draw primary player.
if( GAMESTATE->GetCurrentStyle()->m_StyleType == ONE_PLAYER_TWO_SIDES &&
pn != GAMESTATE->m_MasterPlayerNumber )
2003-05-05 06:48:20 +00:00
return;
// Draw these below everything else.
if( m_pPlayerState->m_PlayerOptions.m_fBlind == 0 )
{
if( m_pCombo )
m_pCombo->Draw();
}
if( m_pAttackDisplay )
m_pAttackDisplay->Draw();
2003-11-26 08:25:45 +00:00
if( TAP_JUDGMENTS_UNDER_FIELD )
DrawTapJudgments();
if( HOLD_JUDGMENTS_UNDER_FIELD )
DrawHoldJudgments();
float fTilt = m_pPlayerState->m_CurrentPlayerOptions.m_fPerspectiveTilt;
float fSkew = m_pPlayerState->m_CurrentPlayerOptions.m_fSkew;
bool bReverse = m_pPlayerState->m_CurrentPlayerOptions.GetReversePercentForColumn(0)>0.5;
2003-09-21 18:07:29 +00:00
2003-04-01 19:31:27 +00:00
DISPLAY->CameraPushMatrix();
2003-09-21 18:07:29 +00:00
DISPLAY->PushMatrix();
2004-11-07 10:38:20 +00:00
float fCenterY = this->GetY()+(GRAY_ARROWS_Y_STANDARD+GRAY_ARROWS_Y_REVERSE)/2;
2002-11-15 08:02:27 +00:00
2004-11-05 06:35:10 +00:00
DISPLAY->LoadMenuPerspective( 45, SCALE(fSkew,0.f,1.f,this->GetX(),SCREEN_CENTER_X), fCenterY );
2002-11-15 08:02:27 +00:00
if( m_pNoteField )
{
float fOriginalY = m_pNoteField->GetY();
2002-02-24 10:31:20 +00:00
float fTiltDegrees = SCALE(fTilt,-1.f,+1.f,+30,-30) * (bReverse?-1:1);
2003-09-21 18:07:29 +00:00
float fZoom = SCALE( m_pPlayerState->m_CurrentPlayerOptions.m_fEffects[PlayerOptions::EFFECT_MINI], 0.f, 1.f, 1.f, 0.5f );
if( fTilt > 0 )
fZoom *= SCALE( fTilt, 0.f, 1.f, 1.f, 0.9f );
else
fZoom *= SCALE( fTilt, 0.f, -1.f, 1.f, 0.9f );
2003-09-21 18:07:29 +00:00
float fYOffset;
if( fTilt > 0 )
fYOffset = SCALE( fTilt, 0.f, 1.f, 0.f, -45.f ) * (bReverse?-1:1);
else
fYOffset = SCALE( fTilt, 0.f, -1.f, 0.f, -20.f ) * (bReverse?-1:1);
2003-09-21 18:07:29 +00:00
m_pNoteField->SetY( fOriginalY + fYOffset );
m_pNoteField->SetZoom( fZoom );
m_pNoteField->SetRotationX( fTiltDegrees );
m_pNoteField->Draw();
2002-02-24 10:31:20 +00:00
m_pNoteField->SetY( fOriginalY );
}
DISPLAY->CameraPopMatrix();
2003-09-21 18:07:29 +00:00
DISPLAY->PopMatrix();
2002-02-24 10:31:20 +00:00
if( !(bool)TAP_JUDGMENTS_UNDER_FIELD )
DrawTapJudgments();
if( !(bool)TAP_JUDGMENTS_UNDER_FIELD )
DrawHoldJudgments();
}
2005-01-15 01:03:48 +00:00
void Player::DrawTapJudgments()
{
if( m_pPlayerState->m_PlayerOptions.m_fBlind > 0 )
return;
if( m_sprJudgmentFrame.IsLoaded() )
m_sprJudgmentFrame->Draw();
if( m_pJudgment )
m_pJudgment->Draw();
}
2005-01-15 01:03:48 +00:00
void Player::DrawHoldJudgments()
{
if( m_pPlayerState->m_PlayerOptions.m_fBlind > 0 )
return;
// TODO: Remove use of PlayerNumber.
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
2004-10-23 17:43:49 +00:00
for( int c=0; c<m_NoteData.GetNumTracks(); c++ )
{
2005-01-15 02:01:26 +00:00
NoteFieldMode::BeginDrawTrack( pn, c );
m_vHoldJudgment[c].Draw();
2005-01-15 02:01:26 +00:00
NoteFieldMode::EndDrawTrack(c);
}
2001-12-28 10:15:59 +00:00
}
int Player::GetClosestNoteDirectional( int col, int iStartRow, int iEndRow, bool bAllowGraded, bool bForward ) const
{
NoteData::const_iterator begin, end;
m_NoteData.GetTapNoteRange( col, iStartRow, iEndRow, begin, end );
if( !bForward )
swap( begin, end );
while( begin != end )
{
if( !bForward )
--begin;
/* Is this the row we want? */
do {
const TapNote &tn = begin->second;
2005-01-22 02:10:54 +00:00
if( tn.type == TapNote::empty )
break;
2005-01-22 02:10:54 +00:00
if( !bAllowGraded && tn.result.tns != TNS_NONE )
break;
return begin->first;
2004-11-08 07:14:47 +00:00
} while(0);
2002-05-29 09:47:24 +00:00
if( bForward )
++begin;
2002-05-29 09:47:24 +00:00
}
2003-02-09 03:33:36 +00:00
return -1;
}
/* Find the closest note to fBeat. */
int Player::GetClosestNote( int col, int iNoteRow, int iMaxRowsAhead, int iMaxRowsBehind, bool bAllowGraded ) const
2003-02-09 03:33:36 +00:00
{
// Start at iIndexStartLookingAt and search outward.
int iNextIndex = GetClosestNoteDirectional( col, iNoteRow, iNoteRow+iMaxRowsAhead, bAllowGraded, true );
int iPrevIndex = GetClosestNoteDirectional( col, iNoteRow-iMaxRowsBehind, iNoteRow, bAllowGraded, false );
2003-02-09 03:33:36 +00:00
if( iNextIndex == -1 && iPrevIndex == -1 )
return -1;
if( iNextIndex == -1 )
return iPrevIndex;
if( iPrevIndex == -1 )
return iNextIndex;
2003-02-09 03:33:36 +00:00
/* Figure out which row is closer. */
if( abs(iNoteRow-iNextIndex) > abs(iNoteRow-iPrevIndex) )
return iPrevIndex;
else
return iNextIndex;
2003-02-09 02:07:30 +00:00
}
2003-07-13 00:34:30 +00:00
void Player::Step( int col, const RageTimer &tm, bool bHeld )
2003-02-09 02:07:30 +00:00
{
bool bOniDead =
GAMESTATE->m_SongOptions.m_LifeType == SongOptions::LIFE_BATTERY &&
m_pPlayerStageStats &&
m_pPlayerStageStats->bFailed;
if( bOniDead )
2003-02-09 02:07:30 +00:00
return; // do nothing
HandleStep( col, tm, bHeld );
2005-02-15 07:09:07 +00:00
MESSAGEMAN->Broadcast( m_sMessageToSendOnStep );
}
void Player::HandleStep( int col, const RageTimer &tm, bool bHeld )
2005-02-15 07:09:07 +00:00
{
2005-01-15 01:03:48 +00:00
//LOG->Trace( "Player::HandlePlayerStep()" );
2003-02-09 02:07:30 +00:00
ASSERT_M( col >= 0 && col <= m_NoteData.GetNumTracks(), ssprintf("%i, %i", col, m_NoteData.GetNumTracks()) );
float fPositionSeconds = GAMESTATE->m_fMusicSeconds;
fPositionSeconds -= tm.Ago();
const float fSongBeat = GAMESTATE->m_pCurSong ? GAMESTATE->m_pCurSong->GetBeatFromElapsedTime( fPositionSeconds ) : GAMESTATE->m_fSongBeat;
{
// Update roll life
// Let's not check the whole array every time.
// Instead, only check 1 beat back. Even 1 is overkill.
// Just update the life here and let Update judge the roll.
const int iSongRow = BeatToNoteRow( fSongBeat );
const int iStartCheckingAt = max( 0, iSongRow-BeatToNoteRow(1) );
NoteData::iterator begin, end;
m_NoteData.GetTapNoteRangeInclusive( col, iStartCheckingAt, iSongRow+1, begin, end );
for( ; begin != end; ++begin )
{
TapNote &tn = begin->second;
if( tn.type != TapNote::hold_head )
continue;
int iRow = begin->first;
HoldNoteScore hns = tn.HoldResult.hns;
if( hns != HNS_NONE ) // if this HoldNote already has a result
continue; // we don't need to update the logic for this one
// if they got a bad score or haven't stepped on the corresponding tap yet
const TapNoteScore tns = tn.result.tns;
const bool bSteppedOnTapNote = tns != TNS_NONE && tns != TNS_MISS; // did they step on the start of this roll?
int iEndRow = iRow + tn.iDuration;
if( bSteppedOnTapNote && tn.HoldResult.fLife != 0 )
{
/* This hold note is not judged and we stepped on its head. Update iLastHeldRow.
* Do this even if we're a little beyond the end of the hold note, to make sure
* iLastHeldRow is clamped to iEndRow if the hold note is held all the way. */
tn.HoldResult.iLastHeldRow = min( iSongRow, iEndRow );
}
// If the song beat is in the range of this hold:
if( iRow <= iSongRow && iRow <= iEndRow )
{
switch( tn.subType )
{
case TapNote::hold_head_hold:
// this is handled in Update
break;
case TapNote::hold_head_roll:
{
// Increase life
tn.HoldResult.fLife = 1;
bool bBright = m_pPlayerStageStats && m_pPlayerStageStats->iCurCombo>(int)BRIGHT_GHOST_COMBO_THRESHOLD;
if( m_pNoteField )
m_pNoteField->DidHoldNote( col, HNS_OK, bBright );
}
break;
default:
ASSERT(0);
}
}
}
}
2005-02-15 07:09:07 +00:00
//
// Count calories for this step, unless we're being called because a button is
// held over a mine.
2005-02-15 07:09:07 +00:00
//
if( m_pPlayerStageStats && m_pPlayerState && !bHeld )
2005-02-15 07:09:07 +00:00
{
// TODO: remove use of PlayerNumber
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
Profile* pProfile = PROFILEMAN->GetProfile(pn);
if( pProfile )
{
int iNumTracksHeld = 0;
for( int t=0; t<m_NoteData.GetNumTracks(); t++ )
{
const StyleInput StyleI( pn, t );
const GameInput GameI = GAMESTATE->GetCurrentStyle()->StyleInputToGameInput( StyleI );
float fSecsHeld = INPUTMAPPER->GetSecsHeld( GameI );
if( fSecsHeld > 0 )
iNumTracksHeld++;
}
float fCals = 0;
switch( iNumTracksHeld )
{
case 0:
// autoplay is on, or this is a computer player
iNumTracksHeld = 1;
// fall through
default:
{
float fCalsFor100Lbs = SCALE( iNumTracksHeld, 1, 2, 0.029f, 0.193f );
float fCalsFor200Lbs = SCALE( iNumTracksHeld, 1, 2, 0.052f, 0.334f );
fCals = SCALE( pProfile->GetCalculatedWeightPounds(), 100.f, 200.f, fCalsFor100Lbs, fCalsFor200Lbs );
}
break;
}
m_pPlayerStageStats->fCaloriesBurned += fCals;
}
2005-02-15 07:09:07 +00:00
}
2003-02-09 02:07:30 +00:00
2003-11-15 23:19:13 +00:00
//
// Check for step on a TapNote
//
const int iStepSearchRows = BeatToNoteRow( StepSearchDistance * GAMESTATE->m_fCurBPS * GAMESTATE->m_SongOptions.m_fMusicRate );
int iIndexOverlappingNote = GetClosestNote( col, BeatToNoteRow(fSongBeat),
iStepSearchRows, iStepSearchRows, false );
2003-02-09 02:07:30 +00:00
2004-07-02 01:24:11 +00:00
// calculate TapNoteScore
TapNoteScore score = TNS_NONE;
2002-05-29 09:47:24 +00:00
if( iIndexOverlappingNote != -1 )
{
2002-06-24 22:04:31 +00:00
// compute the score for this hit
2005-02-07 06:02:15 +00:00
float fNoteOffset;
{
const float fStepBeat = NoteRowToBeat( iIndexOverlappingNote );
const float fStepSeconds = GAMESTATE->m_pCurSong->GetElapsedTimeFromBeat(fStepBeat);
2003-02-17 05:25:20 +00:00
2005-02-07 06:02:15 +00:00
/* We actually stepped on the note this long ago: */
const float fTimeSinceStep = tm.Ago();
2005-02-07 06:02:15 +00:00
/* GAMESTATE->m_fMusicSeconds is the music time as of GAMESTATE->m_LastBeatUpdate. Figure
* out what the music time is as of now. */
const float fCurrentMusicSeconds = GAMESTATE->m_fMusicSeconds + (GAMESTATE->m_LastBeatUpdate.Ago()*GAMESTATE->m_SongOptions.m_fMusicRate);
2005-02-07 06:02:15 +00:00
/* ... which means it happened at this point in the music: */
const float fMusicSeconds = fCurrentMusicSeconds - fTimeSinceStep * GAMESTATE->m_SongOptions.m_fMusicRate;
2003-02-17 05:25:20 +00:00
2005-02-07 06:02:15 +00:00
// The offset from the actual step in seconds:
fNoteOffset = (fStepSeconds - fMusicSeconds) / GAMESTATE->m_SongOptions.m_fMusicRate; // account for music rate
// LOG->Trace("step was %.3f ago, music is off by %f: %f vs %f, step was %f off",
// fTimeSinceStep, GAMESTATE->m_LastBeatUpdate.Ago()/GAMESTATE->m_SongOptions.m_fMusicRate,
// fStepSeconds, fMusicSeconds, fNoteOffset );
}
2002-10-01 01:53:56 +00:00
const float fSecondsFromPerfect = fabsf( fNoteOffset );
2002-06-24 22:04:31 +00:00
2004-10-23 23:41:49 +00:00
TapNote tn = m_NoteData.GetTapNote( col, iIndexOverlappingNote );
switch( m_pPlayerState->m_PlayerController )
2003-04-07 21:24:14 +00:00
{
case PC_HUMAN:
2003-11-10 01:03:47 +00:00
switch( tn.type )
{
case TapNote::mine:
2003-11-10 01:03:47 +00:00
// stepped too close to mine?
if( fSecondsFromPerfect <= ADJUSTED_WINDOW_TAP(TW_Mine) )
2004-01-02 08:43:14 +00:00
score = TNS_HIT_MINE;
2003-11-15 23:19:13 +00:00
break;
case TapNote::attack:
if( fSecondsFromPerfect <= ADJUSTED_WINDOW_TAP(TW_Attack) && !tn.result.bHidden )
2005-02-07 03:54:45 +00:00
score = TNS_PERFECT; /* sentinel */
break;
default:
if( fSecondsFromPerfect <= ADJUSTED_WINDOW_TAP(TW_Marvelous) ) score = TNS_MARVELOUS;
else if( fSecondsFromPerfect <= ADJUSTED_WINDOW_TAP(TW_Perfect) ) score = TNS_PERFECT;
else if( fSecondsFromPerfect <= ADJUSTED_WINDOW_TAP(TW_Great) ) score = TNS_GREAT;
else if( fSecondsFromPerfect <= ADJUSTED_WINDOW_TAP(TW_Good) ) score = TNS_GOOD;
else if( fSecondsFromPerfect <= ADJUSTED_WINDOW_TAP(TW_Boo) ) score = TNS_BOO;
else score = TNS_NONE;
2003-11-15 23:19:13 +00:00
break;
}
break;
case PC_CPU:
2004-01-12 01:48:50 +00:00
case PC_AUTOPLAY:
2005-02-07 04:43:39 +00:00
score = PlayerAI::GetTapNoteScore( m_pPlayerState );
// GetTapNoteScore always returns TNS_MARVELOUS in autoplay.
// If the step is far away, don't judge it.
if( m_pPlayerState->m_PlayerController == PC_AUTOPLAY &&
fSecondsFromPerfect > ADJUSTED_WINDOW_TAP(TW_Boo) )
{
score = TNS_NONE;
break;
}
// TRICKY: We're asking the AI to judge mines. consider TNS_GOOD and below
// as "mine was hit" and everything else as "mine was avoided"
if( tn.type == TapNote::mine )
{
2004-01-11 23:31:37 +00:00
// The CPU hits a lot of mines. Only consider hitting the
// first mine for a row. We know we're the first mine if
// there are are no mines to the left of us.
for( int t=0; t<col; t++ )
{
2004-10-23 17:43:49 +00:00
if( m_NoteData.GetTapNote(t,iIndexOverlappingNote).type == TapNote::mine ) // there's a mine to the left of us
2004-01-11 23:31:37 +00:00
return; // avoid
}
// The CPU hits a lot of mines. Make it less likely to hit
// mines that don't have a tap note on the same row.
2004-10-23 17:43:49 +00:00
bool bTapsOnRow = m_NoteData.IsThereATapOrHoldHeadAtRow( iIndexOverlappingNote );
2004-01-11 23:31:37 +00:00
TapNoteScore get_to_avoid = bTapsOnRow ? TNS_GREAT : TNS_GOOD;
if( score >= get_to_avoid )
return; // avoided
else
score = TNS_HIT_MINE;
}
2005-02-07 03:54:45 +00:00
if( tn.type == TapNote::attack && score > TNS_GOOD )
score = TNS_PERFECT; /* sentinel */
/* AI will generate misses here. Don't handle a miss like a regular note because
* we want the judgment animation to appear delayed. Instead, return early if
* AI generated a miss, and let UpdateMissedTapNotesOlderThan() detect and handle the
* misses. */
if( score == TNS_MISS )
return;
// Put some small, random amount in fNoteOffset so that demonstration
// show a mix of late and early instead of always late.
fNoteOffset = randomf( -0.1f, 0.1f );
2005-02-07 03:45:25 +00:00
break;
default:
ASSERT(0);
score = TNS_NONE;
break;
}
2005-02-07 03:54:45 +00:00
if( tn.type == TapNote::attack && score == TNS_PERFECT )
{
score = TNS_NONE; // don't score this as anything
m_soundAttackLaunch.Play();
// put attack in effect
Attack attack(
ATTACK_LEVEL_1,
-1, // now
tn.fAttackDurationSeconds,
tn.sAttackModifiers,
true,
false
);
// TODO: Remove use of PlayerNumber
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
GAMESTATE->LaunchAttack( (MultiPlayer)OPPOSITE_PLAYER[pn], attack );
2005-02-07 03:54:45 +00:00
// remove all TapAttacks on this row
for( int t=0; t<m_NoteData.GetNumTracks(); t++ )
{
TapNote tn = m_NoteData.GetTapNote(t, iIndexOverlappingNote);
if( tn.type == TapNote::attack )
{
tn.result.bHidden = true;
m_NoteData.SetTapNote( col, iIndexOverlappingNote, tn );
}
}
}
2005-02-07 03:45:25 +00:00
if( score == TNS_HIT_MINE )
{
2005-03-01 15:46:05 +00:00
if( tn.bKeysound && tn.iKeysoundIndex < (int) m_vKeysounds.size() )
2005-03-01 15:14:03 +00:00
m_vKeysounds[tn.iKeysoundIndex].Play();
else
m_soundMine.Play();
2005-02-07 03:45:25 +00:00
if( m_pLifeMeter )
2005-02-07 06:17:07 +00:00
m_pLifeMeter->ChangeLife( score );
2005-04-21 11:16:01 +00:00
if( m_pScoreDisplay )
m_pScoreDisplay->OnJudgment( score );
if( m_pSecondaryScoreDisplay )
m_pSecondaryScoreDisplay->OnJudgment( score );
2005-02-07 03:45:25 +00:00
// TODO: Remove use of PlayerNumber
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
if( m_pCombinedLifeMeter )
2005-02-07 06:17:07 +00:00
m_pCombinedLifeMeter->ChangeLife( pn, score );
2005-04-21 11:16:01 +00:00
2005-02-07 03:45:25 +00:00
tn.result.bHidden = true;
m_NoteData.SetTapNote( col, iIndexOverlappingNote, tn );
if( m_pNoteField )
m_pNoteField->DidTapNote( col, score, false );
}
if( m_pPlayerState->m_PlayerController == PC_HUMAN && score >= TNS_GREAT )
HandleAutosync( fNoteOffset );
2005-02-07 06:20:22 +00:00
// Do game-specific and mode-specific score mapping.
2005-02-07 06:05:42 +00:00
score = GAMESTATE->GetCurrentGame()->MapTapNoteScore( score );
2005-02-07 06:20:22 +00:00
if( score == TNS_MARVELOUS && !GAMESTATE->ShowMarvelous() )
score = TNS_PERFECT;
bool bSteppedEarly = -fNoteOffset < 0;
if( IsPlayingBeginner() && PREFSMAN->m_bMercifulBeginner && score==TNS_BOO && bSteppedEarly )
2003-08-01 00:12:10 +00:00
{
if( m_pJudgment )
m_pJudgment->SetJudgment( score, bSteppedEarly );
2003-08-01 00:12:10 +00:00
}
else
{
tn.result.tns = score;
2003-08-01 00:12:10 +00:00
if( score != TNS_NONE )
tn.result.fTapNoteOffset = -fNoteOffset;
if( score != TNS_NONE && score != TNS_MISS )
{
int ms_error = (int) roundf( fSecondsFromPerfect * 1000 );
ms_error = min( ms_error, MAX_PRO_TIMING_ERROR.GetValue() );
2003-04-11 00:12:07 +00:00
if( m_pPlayerStageStats )
m_pPlayerStageStats->iTotalError += ms_error;
}
2005-01-22 02:10:54 +00:00
//LOG->Trace("XXX: %i col %i, at %f, music at %f, step was at %f, off by %f",
// score, col, fStepSeconds, fCurrentMusicSeconds, fMusicSeconds, fNoteOffset );
// LOG->Trace("Note offset: %f (fSecondsFromPerfect = %f), Score: %i", fNoteOffset, fSecondsFromPerfect, score);
m_NoteData.SetTapNote( col, iIndexOverlappingNote, tn );
2002-12-17 22:07:59 +00:00
// TODO: Remove use of PlayerNumber.
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
// Keep this here so we get the same data as Autosync
NSMAN->ReportTiming( fNoteOffset,pn );
if( m_pPrimaryScoreKeeper )
m_pPrimaryScoreKeeper->HandleTapScore( score );
if( m_pSecondaryScoreKeeper )
m_pSecondaryScoreKeeper->HandleTapScore( score );
2003-11-11 07:36:28 +00:00
switch( tn.type )
{
case TapNote::tap:
case TapNote::hold_head:
// don't judge the row if this note is a mine or tap attack
if( NoteDataWithScoring::IsRowCompletelyJudged( m_NoteData, iIndexOverlappingNote ) )
OnRowCompletelyJudged( iIndexOverlappingNote );
}
m_LastTapNoteScore = score;
}
2004-10-23 17:43:49 +00:00
}
2004-10-25 00:46:08 +00:00
2005-02-07 03:20:52 +00:00
if( m_pNoteField )
m_pNoteField->Step( col, score );
bool bSteppedOnATap = score != TNS_NONE;
if( bSteppedOnATap )
2004-10-25 00:46:08 +00:00
{
/* Search for keyed sounds separately. If we can't find a nearby note, search
* backwards indefinitely, and ignore grading. */
if( iIndexOverlappingNote == -1 )
iIndexOverlappingNote = GetClosestNote( col, BeatToNoteRow(fSongBeat),
iStepSearchRows, MAX_NOTE_ROW, true );
if( iIndexOverlappingNote != -1 )
{
TapNote tn = m_NoteData.GetTapNote( col, iIndexOverlappingNote );
if( tn.bKeysound && tn.iKeysoundIndex < (int) m_vKeysounds.size() )
m_vKeysounds[tn.iKeysoundIndex].Play();
}
2004-10-25 00:46:08 +00:00
}
}
2001-11-03 10:52:42 +00:00
2005-01-15 01:03:48 +00:00
void Player::HandleAutosync(float fNoteOffset)
{
2005-04-11 04:50:39 +00:00
if( GAMESTATE->m_SongOptions.m_AutosyncType == SongOptions::AUTOSYNC_OFF )
return;
2005-05-19 23:29:39 +00:00
m_fOffset[m_iOffsetSample] = fNoteOffset;
m_iOffsetSample++;
2005-04-11 04:50:39 +00:00
if( m_iOffsetSample < SAMPLE_COUNT )
return; /* need more */
2005-04-11 04:50:39 +00:00
const float mean = calc_mean( m_fOffset, m_fOffset+SAMPLE_COUNT );
const float stddev = calc_stddev( m_fOffset, m_fOffset+SAMPLE_COUNT );
2005-05-18 04:43:41 +00:00
CString sAutosyncType;
switch( GAMESTATE->m_SongOptions.m_AutosyncType )
{
case SongOptions::AUTOSYNC_SONG:
sAutosyncType = "Autosync Song";
break;
case SongOptions::AUTOSYNC_MACHINE:
sAutosyncType = "Autosync Machine";
break;
default:
ASSERT(0);
}
2005-04-11 04:50:39 +00:00
if( stddev < .03 && stddev < fabsf(mean) ) // If they stepped with less than .03 error
{
switch( GAMESTATE->m_SongOptions.m_AutosyncType )
{
case SongOptions::AUTOSYNC_SONG:
GAMESTATE->m_pCurSong->m_Timing.m_fBeat0OffsetInSeconds += mean;
break;
case SongOptions::AUTOSYNC_MACHINE:
2005-05-06 20:41:05 +00:00
PREFSMAN->m_fGlobalOffsetSeconds.Set( PREFSMAN->m_fGlobalOffsetSeconds + mean );
2005-04-11 04:50:39 +00:00
break;
default:
ASSERT(0);
}
2005-05-19 23:29:39 +00:00
SCREENMAN->SystemMessage( "Autosync: Correction applied." );
2005-04-11 04:50:39 +00:00
}
else
{
2005-05-19 23:29:39 +00:00
SCREENMAN->SystemMessage( "Autosync: Correction NOT applied.\n Timing deviation too high." );
2005-04-11 04:50:39 +00:00
}
m_iOffsetSample = 0;
}
void Player::DisplayJudgedRow( int iIndexThatWasSteppedOn, TapNoteScore score, int iTrack )
{
TapNote tn = m_NoteData.GetTapNote(iTrack, iIndexThatWasSteppedOn);
// If the score is great or better, remove the note from the screen to
// indicate success. (Or always if blind is on.)
if( score >= TNS_GREAT || m_pPlayerState->m_PlayerOptions.m_fBlind )
{
tn.result.bHidden = true;
m_NoteData.SetTapNote( iTrack, iIndexThatWasSteppedOn, tn );
}
// show the ghost arrow for this column
bool bBright = m_pPlayerStageStats && m_pPlayerStageStats->iCurCombo>(int)BRIGHT_GHOST_COMBO_THRESHOLD;
if (m_pPlayerState->m_PlayerOptions.m_fBlind)
{
if( m_pNoteField )
m_pNoteField->DidTapNote( iTrack, TNS_MARVELOUS, bBright );
}
else
{
bool bBright = m_pPlayerStageStats && m_pPlayerStageStats->iCurCombo>(int)BRIGHT_GHOST_COMBO_THRESHOLD;
if( m_pNoteField )
m_pNoteField->DidTapNote( iTrack, score, bBright );
}
}
2005-01-15 01:03:48 +00:00
void Player::OnRowCompletelyJudged( int iIndexThatWasSteppedOn )
{
2005-01-15 01:03:48 +00:00
// LOG->Trace( "Player::OnRowCompletelyJudged" );
2002-06-24 22:04:31 +00:00
/* Find the minimum score of the row. This will never be TNS_NONE, since this
* function is only called when a row is completed. */
2003-08-10 19:07:54 +00:00
/* Instead, use the last tap score (ala DDR). Using the minimum results in
* slightly more harsh scoring than DDR */
2003-09-19 20:07:58 +00:00
/* I'm not sure this is right, either. Can you really jump a boo and a perfect
* and get scored for a perfect? (That's so loose, you can gallop jumps.) -glenn */
2004-01-02 08:43:14 +00:00
/* Instead of grading individual columns, DDR sets a "was pressed recently"
* countdown every time you step on a column. When you step on the first note of
* the jump, it sets the first "was pressed recently" timer. Then, when you do
* the 2nd step of the jump, it sets another column's timer then AND's the jump
* columns with the "was pressed recently" columns to see whether or not you hit
* all the columns of the jump. -Chris */
const Game *pCurGame = GAMESTATE->m_pCurGame;
if( !pCurGame->m_bCountNotesSeparately )
2002-02-24 01:43:11 +00:00
{
TapNoteResult tnr = NoteDataWithScoring::LastTapNoteResult( m_NoteData, iIndexThatWasSteppedOn );
TapNoteScore score = tnr.tns;
2003-08-10 19:07:54 +00:00
ASSERT(score != TNS_NONE);
ASSERT(score != TNS_HIT_MINE);
ASSERT(score != TNS_AVOIDED_MINE);
2003-08-10 19:07:54 +00:00
/* If the whole row was hit with perfects or greats, remove the row
* from the NoteField, so it disappears. */
for( int c=0; c<m_NoteData.GetNumTracks(); c++ ) // for each column
{
TapNote tn = m_NoteData.GetTapNote(c, iIndexThatWasSteppedOn);
if( tn.type == TapNote::empty ) continue; /* no note in this col */
if( tn.type == TapNote::mine ) continue; /* don't flash on mines b/c they're supposed to be missed */
DisplayJudgedRow( iIndexThatWasSteppedOn, score, c );
}
if( m_pJudgment )
m_pJudgment->SetJudgment( score, tnr.fTapNoteOffset < 0 );
}
else
{
for( int c=0; c<m_NoteData.GetNumTracks(); c++ ) // for each column
{
TapNote tn = m_NoteData.GetTapNote(c, iIndexThatWasSteppedOn);
TapNoteResult tnr = tn.result;
TapNoteScore score = tnr.tns;
if( tn.type == TapNote::empty ) continue; /* no note in this col */
if( tn.type == TapNote::mine ) continue; /* don't flash on mines b/c they're supposed to be missed */
DisplayJudgedRow( iIndexThatWasSteppedOn, score, c );
if( m_pJudgment )
m_pJudgment->SetJudgment( score, tnr.fTapNoteOffset < 0 );
}
2002-10-24 05:24:56 +00:00
}
2002-05-19 01:59:48 +00:00
HandleTapRowScore( iIndexThatWasSteppedOn ); // update score
2001-11-03 10:52:42 +00:00
}
2005-01-15 01:03:48 +00:00
void Player::UpdateTapNotesMissedOlderThan( float fMissIfOlderThanSeconds )
{
2004-05-24 04:26:54 +00:00
//LOG->Trace( "Steps::UpdateTapNotesMissedOlderThan(%f)", fMissIfOlderThanThisBeat );
2003-09-19 20:07:58 +00:00
int iMissIfOlderThanThisIndex;
{
2003-09-19 20:07:58 +00:00
const float fEarliestTime = GAMESTATE->m_fMusicSeconds - fMissIfOlderThanSeconds;
bool bFreeze;
float fMissIfOlderThanThisBeat;
float fThrowAway;
GAMESTATE->m_pCurSong->GetBeatAndBPSFromElapsedTime( fEarliestTime, fMissIfOlderThanThisBeat, fThrowAway, bFreeze );
iMissIfOlderThanThisIndex = BeatToNoteRow( fMissIfOlderThanThisBeat );
if( bFreeze )
{
/* If there is a freeze on iMissIfOlderThanThisIndex, include this index too.
* Otherwise we won't show misses for tap notes on freezes until the
2003-09-19 20:07:58 +00:00
* freeze finishes. */
iMissIfOlderThanThisIndex++;
}
}
2001-11-20 11:49:10 +00:00
2002-04-16 17:31:00 +00:00
// Since this is being called every frame, let's not check the whole array every time.
2005-01-25 05:02:35 +00:00
// Instead, only check 1 beat back. Even 1 is overkill.
const int iStartCheckingAt = max( 0, iMissIfOlderThanThisIndex-BeatToNoteRow(1) );
2001-12-19 01:50:57 +00:00
//LOG->Trace( "iStartCheckingAt: %d iMissIfOlderThanThisIndex: %d", iStartCheckingAt, iMissIfOlderThanThisIndex );
2003-02-09 04:05:31 +00:00
int iNumMissesFound = 0;
FOREACH_NONEMPTY_ROW_ALL_TRACKS_RANGE( m_NoteData, r, iStartCheckingAt, iMissIfOlderThanThisIndex-1 )
2001-12-19 01:50:57 +00:00
{
bool MissedNoteOnThisRow = false;
2004-10-23 17:43:49 +00:00
for( int t=0; t<m_NoteData.GetNumTracks(); t++ )
{
2005-01-08 15:30:57 +00:00
/* XXX: cleaner to pick the things we do want to apply misses to, instead of
* the things we don't? */
2005-01-22 02:10:54 +00:00
TapNote tn = m_NoteData.GetTapNote(t, r);
switch( tn.type )
{
case TapNote::empty:
case TapNote::attack:
continue; /* no note here */
}
2005-01-22 02:10:54 +00:00
if( tn.result.tns != TNS_NONE ) /* note here is already hit */
continue;
2005-01-22 02:10:54 +00:00
if( tn.type == TapNote::mine )
{
tn.result.tns = TNS_AVOIDED_MINE;
}
else
{
// A normal note. Penalize for not stepping on it.
MissedNoteOnThisRow = true;
tn.result.tns = TNS_MISS;
if( m_pPlayerStageStats )
m_pPlayerStageStats->iTotalError += MAX_PRO_TIMING_ERROR;
}
2005-01-22 02:10:54 +00:00
m_NoteData.SetTapNote( t, r, tn );
}
2003-08-10 19:07:54 +00:00
if( MissedNoteOnThisRow )
{
iNumMissesFound++;
HandleTapRowScore( r );
}
2001-12-19 01:50:57 +00:00
}
2002-05-29 09:47:24 +00:00
if( iNumMissesFound > 0 )
{
if( m_pJudgment )
m_pJudgment->SetJudgment( TNS_MISS, false );
}
}
2005-01-15 01:03:48 +00:00
void Player::CrossedRow( int iNoteRow )
2002-06-24 22:04:31 +00:00
{
// If we're doing random vanish, randomise notes on the fly.
if(m_pPlayerState->m_CurrentPlayerOptions.m_fAppearances[PlayerOptions::APPEARANCE_RANDOMVANISH]==1)
2004-10-23 17:43:49 +00:00
RandomizeNotes( iNoteRow );
2004-10-23 17:43:49 +00:00
// check to see if there's a note at the crossed row
2003-07-13 00:34:30 +00:00
RageTimer now;
if( m_pPlayerState->m_PlayerController != PC_HUMAN )
2003-07-13 00:34:30 +00:00
{
2004-10-23 17:43:49 +00:00
for( int t=0; t<m_NoteData.GetNumTracks(); t++ )
{
2005-01-22 02:10:54 +00:00
TapNote tn = m_NoteData.GetTapNote(t, iNoteRow);
if( tn.type != TapNote::empty && tn.result.tns == TNS_NONE )
2005-10-01 00:18:13 +00:00
{
2005-01-22 02:10:54 +00:00
Step( t, now );
2005-10-01 00:18:13 +00:00
STATSMAN->m_CurStageStats.bUsedAutoplay = true;
}
2004-10-23 17:43:49 +00:00
}
2003-07-13 00:34:30 +00:00
}
}
2005-01-15 01:03:48 +00:00
void Player::CrossedMineRow( int iNoteRow )
{
// Hold the panel while crossing a mine will cause the mine to explode
RageTimer now;
2004-10-23 17:43:49 +00:00
for( int t=0; t<m_NoteData.GetNumTracks(); t++ )
{
2004-10-23 17:43:49 +00:00
if( m_NoteData.GetTapNote(t,iNoteRow).type == TapNote::mine )
{
// TODO: Remove use of PlayerNumber.
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
const StyleInput StyleI( pn, t );
2004-06-28 07:26:00 +00:00
const GameInput GameI = GAMESTATE->GetCurrentStyle()->StyleInputToGameInput( StyleI );
if( PREFSMAN->m_fPadStickSeconds > 0 )
{
float fSecsHeld = INPUTMAPPER->GetSecsHeld( GameI );
if( fSecsHeld >= PREFSMAN->m_fPadStickSeconds )
Step( t, now+(-PREFSMAN->m_fPadStickSeconds), true );
}
else
{
bool bIsDown = INPUTMAPPER->IsButtonDown( GameI );
if( bIsDown )
Step( t, now, true );
}
}
}
2002-06-24 22:04:31 +00:00
}
2005-01-15 01:03:48 +00:00
void Player::RandomizeNotes( int iNoteRow )
{
// change the row to look ahead from based upon their speed mod
/* This is incorrect: if m_fScrollSpeed is 0.5, we'll never change
* any odd rows, and if it's 2, we'll shuffle each row twice. */
int iNewNoteRow = iNoteRow + ROWS_PER_BEAT*2;
iNewNoteRow = int( iNewNoteRow / m_pPlayerState->m_PlayerOptions.m_fScrollSpeed );
2004-10-23 17:43:49 +00:00
int iNumOfTracks = m_NoteData.GetNumTracks();
2003-10-24 22:57:47 +00:00
for( int t=0; t+1 < iNumOfTracks; t++ )
{
const int iSwapWith = RandomInt( 0, iNumOfTracks-1 );
2003-10-24 22:57:47 +00:00
/* Only swap a tap and an empty. */
2004-10-23 17:43:49 +00:00
const TapNote t1 = m_NoteData.GetTapNote(t, iNewNoteRow);
if( t1.type != TapNote::tap )
continue;
2004-10-23 17:43:49 +00:00
const TapNote t2 = m_NoteData.GetTapNote( iSwapWith, iNewNoteRow );
if( t2.type != TapNote::empty )
continue;
/* Make sure the destination row isn't in the middle of a hold. */
2005-07-22 00:14:24 +00:00
if( m_NoteData.IsHoldNoteAtRow(iSwapWith, iNoteRow) )
continue;
2004-10-23 17:43:49 +00:00
m_NoteData.SetTapNote( t, iNewNoteRow, t2 );
m_NoteData.SetTapNote( iSwapWith, iNewNoteRow, t1 );
}
}
2002-06-24 22:04:31 +00:00
2005-01-15 01:03:48 +00:00
void Player::HandleTapRowScore( unsigned row )
2002-06-24 22:04:31 +00:00
{
2005-04-28 00:13:01 +00:00
TapNoteScore scoreOfLastTap = NoteDataWithScoring::LastTapNoteResult( m_NoteData, row ).tns;
2004-10-23 17:43:49 +00:00
int iNumTapsInRow = m_NoteData.GetNumTracksWithTapOrHoldHead(row);
ASSERT(iNumTapsInRow > 0);
bool NoCheating = true;
2003-04-22 04:54:04 +00:00
#ifdef DEBUG
NoCheating = false;
#endif
if(GAMESTATE->m_bDemonstrationOrJukebox)
NoCheating = false;
// don't accumulate points if AutoPlay is on.
if( NoCheating && m_pPlayerState->m_PlayerController == PC_AUTOPLAY )
return;
/* Update miss combo, and handle "combo stopped" messages. */
2005-01-08 15:34:22 +00:00
/* When is m_pPlayerStageStats NULL? Would it be cleaner to pass Player a dummy
* PlayerStageStats in this case, instead of having to carefully check for NULL
* every time we use it? -glenn */
int iDummy = 0;
2005-01-08 15:34:22 +00:00
int &iCurCombo = m_pPlayerStageStats ? m_pPlayerStageStats->iCurCombo : iDummy;
int &iCurMissCombo = m_pPlayerStageStats ? m_pPlayerStageStats->iCurMissCombo : iDummy;
switch( scoreOfLastTap )
{
case TNS_MARVELOUS:
case TNS_PERFECT:
case TNS_GREAT:
iCurMissCombo = 0;
SCREENMAN->PostMessageToTopScreen( SM_MissComboAborted, 0 );
break;
case TNS_MISS:
++iCurMissCombo;
m_LastTapNoteScore = TNS_MISS;
case TNS_GOOD:
case TNS_BOO:
2005-01-08 15:34:22 +00:00
if( iCurCombo > 50 )
SCREENMAN->PostMessageToTopScreen( SM_ComboStopped, 0 );
2005-01-09 05:24:46 +00:00
iCurCombo = 0;
break;
2004-01-02 08:43:14 +00:00
default:
ASSERT( 0 );
}
2005-01-08 15:34:22 +00:00
/* The score keeper updates the hit combo. Remember the old combo for handling announcers. */
const int iOldCombo = iCurCombo;
2005-01-08 15:31:55 +00:00
if( m_pPrimaryScoreKeeper != NULL )
m_pPrimaryScoreKeeper->HandleTapRowScore( scoreOfLastTap, iNumTapsInRow );
if( m_pSecondaryScoreKeeper != NULL )
m_pSecondaryScoreKeeper->HandleTapRowScore( scoreOfLastTap, iNumTapsInRow );
if( m_pPlayerStageStats )
{
if( m_pCombo )
m_pCombo->SetCombo( m_pPlayerStageStats->iCurCombo, m_pPlayerStageStats->iCurMissCombo );
}
2003-09-19 20:15:15 +00:00
#define CROSSED( x ) (iOldCombo<x && iCurCombo>=x)
if ( CROSSED(100) )
SCREENMAN->PostMessageToTopScreen( SM_100Combo, 0 );
else if( CROSSED(200) )
SCREENMAN->PostMessageToTopScreen( SM_200Combo, 0 );
else if( CROSSED(300) )
SCREENMAN->PostMessageToTopScreen( SM_300Combo, 0 );
else if( CROSSED(400) )
SCREENMAN->PostMessageToTopScreen( SM_400Combo, 0 );
else if( CROSSED(500) )
SCREENMAN->PostMessageToTopScreen( SM_500Combo, 0 );
else if( CROSSED(600) )
SCREENMAN->PostMessageToTopScreen( SM_600Combo, 0 );
else if( CROSSED(700) )
SCREENMAN->PostMessageToTopScreen( SM_700Combo, 0 );
else if( CROSSED(800) )
SCREENMAN->PostMessageToTopScreen( SM_800Combo, 0 );
else if( CROSSED(900) )
SCREENMAN->PostMessageToTopScreen( SM_900Combo, 0 );
else if( CROSSED(1000))
SCREENMAN->PostMessageToTopScreen( SM_1000Combo, 0 );
else if( (iOldCombo / 100) < (iCurCombo / 100) && iCurCombo > 1000 )
SCREENMAN->PostMessageToTopScreen( SM_ComboContinuing, 0 );
#undef CROSSED
// new max combo
if( m_pPlayerStageStats )
m_pPlayerStageStats->iMaxCombo = max(m_pPlayerStageStats->iMaxCombo, iCurCombo);
/*
* Use the real current beat, not the beat we've been passed. That's because we
2003-10-24 05:19:08 +00:00
* want to record the current life/combo to the current time; eg. if it's a MISS,
* the beat we're registering is in the past, but the life is changing now.
*
* We need to include time from previous songs in a course, so we can't use
* GAMESTATE->m_fMusicSeconds. Use fGameplaySeconds instead.
*/
if( m_pPlayerStageStats )
m_pPlayerStageStats->UpdateComboList( STATSMAN->m_CurStageStats.fStepsSeconds, false );
2003-10-24 05:19:08 +00:00
// TODO: Remove use of PlayerNumber.
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
2003-10-26 08:37:25 +00:00
float life = -1;
2003-10-24 05:19:08 +00:00
if( m_pLifeMeter )
{
2003-10-24 05:19:08 +00:00
life = m_pLifeMeter->GetLife();
}
2003-12-17 03:59:08 +00:00
else if( m_pCombinedLifeMeter )
{
life = GAMESTATE->m_fTugLifePercentP1;
if( pn == PLAYER_2 )
2003-12-17 03:59:08 +00:00
life = 1.0f - life;
}
2003-10-26 08:37:25 +00:00
if( life != -1 )
if( m_pPlayerStageStats )
m_pPlayerStageStats->SetLifeRecordAt( life, STATSMAN->m_CurStageStats.fStepsSeconds );
2003-10-23 06:17:40 +00:00
2005-04-21 11:16:01 +00:00
if( m_pScoreDisplay )
{
if( m_pPlayerStageStats )
2005-04-21 11:16:01 +00:00
m_pScoreDisplay->SetScore( m_pPlayerStageStats->iScore );
m_pScoreDisplay->OnJudgment( scoreOfLastTap );
}
if( m_pSecondaryScoreDisplay )
{
if( m_pPlayerStageStats )
2005-04-21 11:16:01 +00:00
m_pSecondaryScoreDisplay->SetScore( m_pPlayerStageStats->iScore );
m_pSecondaryScoreDisplay->OnJudgment( scoreOfLastTap );
}
2005-04-21 11:16:01 +00:00
if( m_pLifeMeter )
{
2003-03-16 17:45:32 +00:00
m_pLifeMeter->ChangeLife( scoreOfLastTap );
m_pLifeMeter->OnDancePointsChange(); // update oni life meter
}
2005-04-21 11:16:01 +00:00
if( m_pCombinedLifeMeter )
{
m_pCombinedLifeMeter->ChangeLife( pn, scoreOfLastTap );
m_pCombinedLifeMeter->OnDancePointsChange( pn ); // update oni life meter
2003-06-30 18:08:27 +00:00
}
2002-06-24 22:04:31 +00:00
}
2002-07-28 20:28:37 +00:00
2005-01-15 01:03:48 +00:00
void Player::HandleHoldScore( HoldNoteScore holdScore, TapNoteScore tapScore )
2002-07-28 20:28:37 +00:00
{
bool NoCheating = true;
2003-06-18 04:25:49 +00:00
#ifdef DEBUG
NoCheating = false;
#endif
2005-04-21 11:16:01 +00:00
if( GAMESTATE->m_bDemonstrationOrJukebox )
NoCheating = false;
// don't accumulate points if AutoPlay is on.
if( NoCheating && m_pPlayerState->m_PlayerController == PC_AUTOPLAY )
return;
2003-06-30 18:08:27 +00:00
if(m_pPrimaryScoreKeeper)
2003-07-08 20:56:25 +00:00
m_pPrimaryScoreKeeper->HandleHoldScore(holdScore, tapScore );
2003-06-30 18:08:27 +00:00
if(m_pSecondaryScoreKeeper)
2003-07-08 20:56:25 +00:00
m_pSecondaryScoreKeeper->HandleHoldScore(holdScore, tapScore );
// TODO: Remove use of PlayerNumber.
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
2005-04-21 11:16:01 +00:00
if( m_pScoreDisplay )
{
if( m_pPlayerStageStats )
m_pScoreDisplay->SetScore( m_pPlayerStageStats->iScore );
m_pScoreDisplay->OnJudgment( holdScore, tapScore );
}
if( m_pSecondaryScoreDisplay )
{
if( m_pPlayerStageStats )
m_pSecondaryScoreDisplay->SetScore( m_pPlayerStageStats->iScore );
m_pSecondaryScoreDisplay->OnJudgment( holdScore, tapScore );
}
2002-11-03 20:52:13 +00:00
if( m_pLifeMeter )
{
2003-03-16 17:45:32 +00:00
m_pLifeMeter->ChangeLife( holdScore, tapScore );
m_pLifeMeter->OnDancePointsChange();
}
if( m_pCombinedLifeMeter )
{
m_pCombinedLifeMeter->ChangeLife( pn, holdScore, tapScore );
m_pCombinedLifeMeter->OnDancePointsChange( pn );
2003-06-30 18:08:27 +00:00
}
2002-07-28 20:28:37 +00:00
}
2002-09-05 03:45:07 +00:00
2005-01-15 01:03:48 +00:00
float Player::GetMaxStepDistanceSeconds()
2002-07-28 20:28:37 +00:00
{
return GAMESTATE->m_SongOptions.m_fMusicRate * ADJUSTED_WINDOW_TAP(TW_Boo);
2002-09-05 03:45:07 +00:00
}
2005-01-15 01:03:48 +00:00
void Player::FadeToFail()
2002-09-05 03:45:07 +00:00
{
if( m_pNoteField )
m_pNoteField->FadeToFail();
// clear miss combo
if( m_pCombo )
m_pCombo->SetCombo( 0, 0 );
}
2005-04-28 00:13:01 +00:00
bool Player::IsPlayingBeginner() const
{
2005-04-28 05:08:15 +00:00
if( m_pPlayerStageStats != NULL && !m_pPlayerStageStats->vpPossibleSteps.empty() )
2005-04-28 00:13:01 +00:00
{
Steps *pSteps = m_pPlayerStageStats->vpPossibleSteps[0];
return pSteps->GetDifficulty() == DIFFICULTY_BEGINNER;
}
if( m_pPlayerState == NULL )
return false;
if( m_pPlayerState->m_PlayerNumber == PLAYER_INVALID )
return false;
Steps *pSteps = GAMESTATE->m_pCurSteps[ m_pPlayerState->m_PlayerNumber ];
return pSteps && pSteps->GetDifficulty() == DIFFICULTY_BEGINNER;
2005-04-28 00:13:01 +00:00
}
2005-05-19 23:29:39 +00:00
void Player::HandleMessage( const CString& sMessage )
{
// Reset autosync samples when toggling
if( sMessage == MessageToString(Message_AutosyncChanged) )
2005-05-19 23:29:39 +00:00
m_iOffsetSample = 0;
}
2004-06-08 00:08:04 +00:00
/*
* (c) 2001-2004 Chris Danford
* 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.
*/