3316 lines
103 KiB
C++
3316 lines
103 KiB
C++
#include "global.h"
|
|
#include "Player.h"
|
|
#include "GameConstantsAndTypes.h"
|
|
#include "RageUtil.h"
|
|
#include "RageTimer.h"
|
|
#include "PrefsManager.h"
|
|
#include "GameManager.h"
|
|
#include "InputMapper.h"
|
|
#include "SongManager.h"
|
|
#include "GameState.h"
|
|
#include "ScoreKeeperNormal.h"
|
|
#include "RageLog.h"
|
|
#include "RageDisplay.h"
|
|
#include "ThemeManager.h"
|
|
#include "ScoreDisplay.h"
|
|
#include "LifeMeter.h"
|
|
#include "CombinedLifeMeter.h"
|
|
#include "PlayerAI.h"
|
|
#include "NoteField.h"
|
|
#include "NoteDataUtil.h"
|
|
#include "ScreenMessage.h"
|
|
#include "ScreenManager.h"
|
|
#include "StageStats.h"
|
|
#include "ActorUtil.h"
|
|
#include "ArrowEffects.h"
|
|
#include "Game.h"
|
|
#include "NetworkSyncManager.h" //used for sending timing offset
|
|
#include "DancingCharacters.h"
|
|
#include "ScreenDimensions.h"
|
|
#include "RageSoundManager.h"
|
|
#include "ThemeMetric.h"
|
|
#include "PlayerState.h"
|
|
#include "GameSoundManager.h"
|
|
#include "Style.h"
|
|
#include "MessageManager.h"
|
|
#include "ProfileManager.h"
|
|
#include "Profile.h"
|
|
#include "StatsManager.h"
|
|
#include "Song.h"
|
|
#include "Steps.h"
|
|
#include "GameCommand.h"
|
|
#include "LocalizedString.h"
|
|
#include "AdjustSync.h"
|
|
|
|
/**
|
|
* @brief Helper class to ensure that each row is only judged once without taking too much memory.
|
|
*/
|
|
class JudgedRows
|
|
{
|
|
vector<bool> m_vRows;
|
|
int m_iStart;
|
|
int m_iOffset;
|
|
|
|
void Resize( size_t iMin )
|
|
{
|
|
size_t iNewSize = max( 2*m_vRows.size(), iMin );
|
|
vector<bool> vNewRows( m_vRows.begin() + m_iOffset, m_vRows.end() );
|
|
vNewRows.reserve( iNewSize );
|
|
vNewRows.insert( vNewRows.end(), m_vRows.begin(), m_vRows.begin() + m_iOffset );
|
|
vNewRows.resize( iNewSize, false );
|
|
m_vRows.swap( vNewRows );
|
|
m_iOffset = 0;
|
|
}
|
|
public:
|
|
JudgedRows() : m_iStart(0), m_iOffset(0) { Resize( 32 ); }
|
|
// Returns true if the row has already been judged.
|
|
bool JudgeRow( int iRow )
|
|
{
|
|
if( iRow < m_iStart )
|
|
return true;
|
|
if( iRow >= m_iStart+int(m_vRows.size()) )
|
|
Resize( iRow+1-m_iStart );
|
|
const int iIndex = (iRow - m_iStart + m_iOffset) % m_vRows.size();
|
|
const bool ret = m_vRows[iIndex];
|
|
m_vRows[iIndex] = true;
|
|
while( m_vRows[m_iOffset] )
|
|
{
|
|
m_vRows[m_iOffset] = false;
|
|
++m_iStart;
|
|
if( ++m_iOffset >= int(m_vRows.size()) )
|
|
m_iOffset -= m_vRows.size();
|
|
}
|
|
return ret;
|
|
}
|
|
void Reset( int iStart )
|
|
{
|
|
m_iStart = iStart;
|
|
m_iOffset = 0;
|
|
m_vRows.assign( m_vRows.size(), false );
|
|
}
|
|
};
|
|
|
|
|
|
RString ATTACK_DISPLAY_X_NAME( size_t p, size_t both_sides ) { return "AttackDisplayXOffset" + (both_sides ? RString("BothSides") : ssprintf("OneSideP%d",int(p+1)) ); }
|
|
|
|
/**
|
|
* @brief Distance to search for a note in Step(), in seconds.
|
|
*
|
|
* TODO: This should be calculated based on the max size of the current judgment windows. */
|
|
static const float StepSearchDistance = 1.0f;
|
|
|
|
void TimingWindowSecondsInit( size_t /*TimingWindow*/ i, RString &sNameOut, float &defaultValueOut )
|
|
{
|
|
sNameOut = "TimingWindowSeconds" + TimingWindowToString( (TimingWindow)i );
|
|
switch( i )
|
|
{
|
|
default: ASSERT(0);
|
|
case TW_W1: defaultValueOut = 0.0225f; break;
|
|
case TW_W2: defaultValueOut = 0.045f; break;
|
|
case TW_W3: defaultValueOut = 0.090f; break;
|
|
case TW_W4: defaultValueOut = 0.135f; break;
|
|
case TW_W5: defaultValueOut = 0.180f; break;
|
|
case TW_Mine: defaultValueOut = 0.090f; break; // same as great
|
|
case TW_Hold: defaultValueOut = 0.500f; break; // allow enough time to take foot off and put back on
|
|
case TW_Roll: defaultValueOut = 0.350f; break;
|
|
case TW_Attack: defaultValueOut = 0.135f; break;
|
|
}
|
|
}
|
|
|
|
static Preference<float> m_fTimingWindowScale ( "TimingWindowScale", 1.0f );
|
|
static Preference<float> m_fTimingWindowAdd ( "TimingWindowAdd", 0 );
|
|
static Preference1D<float> m_fTimingWindowSeconds( TimingWindowSecondsInit, NUM_TimingWindow );
|
|
static Preference<float> m_fTimingWindowJump ( "TimingWindowJump", 0.25 );
|
|
static Preference<float> m_fMaxInputLatencySeconds ( "MaxInputLatencySeconds", 0.0 );
|
|
static Preference<bool> g_bEnableAttackSoundPlayback ( "EnableAttackSounds", true );
|
|
static Preference<bool> g_bEnableMineSoundPlayback ( "EnableMineHitSound", true );
|
|
|
|
Preference<float> g_fTimingWindowHopo ( "TimingWindowHopo", 0.25 ); // max time between notes in a hopo chain
|
|
Preference<float> g_fTimingWindowStrum ( "TimingWindowStrum", 0.1f ); // max time between strum and when the frets must match
|
|
ThemeMetric<float> INITIAL_HOLD_LIFE ( "Player", "InitialHoldLife" );
|
|
ThemeMetric<float> MAX_HOLD_LIFE ( "Player", "MaxHoldLife" ); // sm-ssc addition
|
|
ThemeMetric<bool> PENALIZE_TAP_SCORE_NONE ( "Player", "PenalizeTapScoreNone" );
|
|
ThemeMetric<bool> JUDGE_HOLD_NOTES_ON_SAME_ROW_TOGETHER ( "Player", "JudgeHoldNotesOnSameRowTogether" );
|
|
/**
|
|
* @brief Does StepMania use checkpoint judgments when interacting with holds?
|
|
*
|
|
* If set to true, checkpoint judgments are in use, similar to the Pump It Up
|
|
* series. If set to false, no such judgments are used. */
|
|
ThemeMetric<bool> HOLD_CHECKPOINTS ( "Player", "HoldCheckpoints" );
|
|
/**
|
|
* @brief If using checkpoints, are the song's TickcountSegments used?
|
|
*
|
|
* If set to true, the checkpoints rely on the data found in the TickcountSegments.
|
|
* This is used via the #%TICKCOUNTS tag. */
|
|
ThemeMetric<bool> CHECKPOINTS_USE_TICKCOUNTS ( "Player", "CheckpointsUseTickcounts" );
|
|
/**
|
|
* @brief If using checkpoints, are the song's TimeSignatureSegments used?
|
|
*
|
|
* If set to true AND CheckpointsUseTickcounts is set to false, the TimeSignatureSegments
|
|
* are used to determine how often the checkpoints are found.
|
|
*
|
|
* It should be noted that this is a deprecated metric. */
|
|
ThemeMetric<bool> CHECKPOINTS_USE_TIME_SIGNATURES ( "Player", "CheckpointsUseTimeSignatures" );
|
|
ThemeMetric<bool> CHECKPOINTS_FLASH_ON_HOLD ( "Player", "CheckpointsFlashOnHold" ); // sm-ssc addition
|
|
ThemeMetric<bool> IMMEDIATE_HOLD_LET_GO ( "Player", "ImmediateHoldLetGo" );
|
|
/**
|
|
* @brief Must a Player step on a hold head for a hold to activate?
|
|
*
|
|
* If set to true, the Player must step on a hold head in order for the hold to activate.
|
|
* If set to false, merely holding your foot down as the hold head approaches will suffice. */
|
|
ThemeMetric<bool> REQUIRE_STEP_ON_HOLD_HEADS ( "Player", "RequireStepOnHoldHeads" );
|
|
/**
|
|
* @brief Must a Player step on a mine for it to activate?
|
|
*
|
|
* If set to true, the Player must step on a mine for it to blow up.
|
|
* If set to false, merely holding your foot down as the mine approaches will suffice. */
|
|
ThemeMetric<bool> REQUIRE_STEP_ON_MINES ( "Player", "RequireStepOnMines" );
|
|
//ThemeMetric<bool> HOLD_TRIGGERS_TAP_NOTES ( "Player", "HoldTriggersTapNotes" ); // parastar stuff; leave in though
|
|
/**
|
|
* @brief Does repeatedly stepping on a roll to keep it alive increment the combo?
|
|
*
|
|
* If set to true, repeatedly stepping on a roll will increment the combo.
|
|
* If set to false, only the roll head causes the combo to be incremented.
|
|
*
|
|
* For those wishing to make a theme very accurate to In The Groove 2, set this to false. */
|
|
ThemeMetric<bool> ROLL_BODY_INCREMENTS_COMBO ( "Player", "RollBodyIncrementsCombo" );
|
|
ThemeMetric<bool> CHECKPOINTS_TAPS_SEPARATE_JUDGMENT ( "Player", "CheckpointsTapsSeparateJudgment" );
|
|
/**
|
|
* @brief Do we score missed holds and rolls with HoldNoteScores?
|
|
*
|
|
* If set to true, missed holds and rolls are given LetGo judgments.
|
|
* If set to false, missed holds and rolls are given no judgment on the hold side of things. */
|
|
ThemeMetric<bool> SCORE_MISSED_HOLDS_AND_ROLLS ( "Player", "ScoreMissedHoldsAndRolls" );
|
|
/** @brief How much of the song/course must have gone by before a Player's combo is colored? */
|
|
ThemeMetric<float> PERCENT_UNTIL_COLOR_COMBO ( "Player", "PercentUntilColorCombo" );
|
|
/** @brief How much combo must be earned before the announcer says "Combo Stopped"? */
|
|
ThemeMetric<int> COMBO_STOPPED_AT ( "Player", "ComboStoppedAt" );
|
|
ThemeMetric<float> ATTACK_RUN_TIME_RANDOM ( "Player", "AttackRunTimeRandom" );
|
|
ThemeMetric<float> ATTACK_RUN_TIME_MINE ( "Player", "AttackRunTimeMine" );
|
|
|
|
float Player::GetWindowSeconds( TimingWindow tw )
|
|
{
|
|
float fSecs = m_fTimingWindowSeconds[tw];
|
|
fSecs *= m_fTimingWindowScale;
|
|
fSecs += m_fTimingWindowAdd;
|
|
return fSecs;
|
|
}
|
|
|
|
Player::Player( NoteData &nd, bool bVisibleParts ) : m_NoteData(nd)
|
|
{
|
|
m_bLoaded = false;
|
|
|
|
m_pPlayerState = NULL;
|
|
m_pPlayerStageStats = NULL;
|
|
m_fNoteFieldHeight = 0;
|
|
|
|
m_pLifeMeter = NULL;
|
|
m_pCombinedLifeMeter = NULL;
|
|
m_pScoreDisplay = NULL;
|
|
m_pSecondaryScoreDisplay = NULL;
|
|
m_pPrimaryScoreKeeper = NULL;
|
|
m_pSecondaryScoreKeeper = NULL;
|
|
m_pInventory = NULL;
|
|
m_pIterNeedsTapJudging = NULL;
|
|
m_pIterNeedsHoldJudging = NULL;
|
|
m_pIterUncrossedRows = NULL;
|
|
m_pIterUnjudgedRows = NULL;
|
|
m_pIterUnjudgedMineRows = NULL;
|
|
|
|
m_bPaused = false;
|
|
m_bDelay = false;
|
|
|
|
m_pAttackDisplay = NULL;
|
|
if( bVisibleParts )
|
|
{
|
|
m_pAttackDisplay = new AttackDisplay;
|
|
this->AddChild( m_pAttackDisplay );
|
|
}
|
|
|
|
PlayerAI::InitFromDisk();
|
|
|
|
m_pNoteField = NULL;
|
|
if( bVisibleParts )
|
|
{
|
|
m_pNoteField = new NoteField;
|
|
m_pNoteField->SetName( "NoteField" );
|
|
}
|
|
m_pJudgedRows = new JudgedRows;
|
|
|
|
m_bSendJudgmentAndComboMessages = true;
|
|
}
|
|
|
|
Player::~Player()
|
|
{
|
|
SAFE_DELETE( m_pAttackDisplay );
|
|
SAFE_DELETE( m_pNoteField );
|
|
for( unsigned i = 0; i < m_vpHoldJudgment.size(); ++i )
|
|
SAFE_DELETE( m_vpHoldJudgment[i] );
|
|
SAFE_DELETE( m_pJudgedRows );
|
|
SAFE_DELETE( m_pIterNeedsTapJudging );
|
|
SAFE_DELETE( m_pIterNeedsHoldJudging );
|
|
SAFE_DELETE( m_pIterUncrossedRows );
|
|
SAFE_DELETE( m_pIterUnjudgedRows );
|
|
SAFE_DELETE( m_pIterUnjudgedMineRows );
|
|
|
|
}
|
|
|
|
/* Init() does the expensive stuff: load sounds and noteskins. Load() just loads a NoteData. */
|
|
void Player::Init(
|
|
const RString &sType,
|
|
PlayerState* pPlayerState,
|
|
PlayerStageStats* pPlayerStageStats,
|
|
LifeMeter* pLM,
|
|
CombinedLifeMeter* pCombinedLM,
|
|
ScoreDisplay* pScoreDisplay,
|
|
ScoreDisplay* pSecondaryScoreDisplay,
|
|
Inventory* pInventory,
|
|
ScoreKeeper* pPrimaryScoreKeeper,
|
|
ScoreKeeper* pSecondaryScoreKeeper )
|
|
{
|
|
GRAY_ARROWS_Y_STANDARD.Load( sType, "ReceptorArrowsYStandard" );
|
|
GRAY_ARROWS_Y_REVERSE.Load( sType, "ReceptorArrowsYReverse" );
|
|
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" );
|
|
COMBO_UNDER_FIELD.Load( sType, "ComboUnderField" );
|
|
DRAW_DISTANCE_AFTER_TARGET_PIXELS.Load( sType, "DrawDistanceAfterTargetsPixels" );
|
|
DRAW_DISTANCE_BEFORE_TARGET_PIXELS.Load( sType, "DrawDistanceBeforeTargetsPixels" );
|
|
|
|
{
|
|
// Init judgment positions
|
|
bool bPlayerUsingBothSides = GAMESTATE->GetCurrentStyle()->GetUsesCenteredArrows();
|
|
Actor TempJudgment;
|
|
TempJudgment.SetName( "Judgment" );
|
|
ActorUtil::LoadCommand( TempJudgment, sType, "Transform" );
|
|
|
|
Actor TempCombo;
|
|
TempCombo.SetName( "Combo" );
|
|
ActorUtil::LoadCommand( TempCombo, sType, "Transform" );
|
|
|
|
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++ )
|
|
{
|
|
Message msg( "Transform" );
|
|
msg.SetParam( "Player", pPlayerState->m_PlayerNumber );
|
|
msg.SetParam( "MultiPlayer", pPlayerState->m_mp );
|
|
msg.SetParam( "iEnabledPlayerIndex", iEnabledPlayerIndex );
|
|
msg.SetParam( "iNumEnabledPlayers", iNumEnabledPlayers );
|
|
msg.SetParam( "bPlayerUsingBothSides", bPlayerUsingBothSides );
|
|
msg.SetParam( "bReverse", !!i );
|
|
msg.SetParam( "bCentered", !!j );
|
|
|
|
TempJudgment.HandleMessage( msg );
|
|
m_tsJudgment[i][j] = TempJudgment.DestTweenState();
|
|
|
|
TempCombo.HandleMessage( msg );
|
|
m_tsCombo[i][j] = TempCombo.DestTweenState();
|
|
}
|
|
}
|
|
}
|
|
|
|
this->SortByDrawOrder();
|
|
|
|
m_pPlayerState = pPlayerState;
|
|
m_pPlayerStageStats = pPlayerStageStats;
|
|
m_pLifeMeter = pLM;
|
|
m_pCombinedLifeMeter = pCombinedLM;
|
|
m_pScoreDisplay = pScoreDisplay;
|
|
m_pSecondaryScoreDisplay = pSecondaryScoreDisplay;
|
|
m_pInventory = pInventory;
|
|
m_pPrimaryScoreKeeper = pPrimaryScoreKeeper;
|
|
m_pSecondaryScoreKeeper = pSecondaryScoreKeeper;
|
|
|
|
m_iLastSeenCombo = -1;
|
|
|
|
// set initial life
|
|
if( m_pLifeMeter && m_pPlayerStageStats )
|
|
{
|
|
float fLife = m_pLifeMeter->GetLife();
|
|
m_pPlayerStageStats->SetLifeRecordAt( fLife, STATSMAN->m_CurStageStats.m_fStepsSeconds );
|
|
}
|
|
|
|
// TODO: Remove use of PlayerNumber.
|
|
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
|
|
|
|
RageSoundLoadParams SoundParams;
|
|
SoundParams.m_bSupportPan = true;
|
|
m_soundMine.Load( THEME->GetPathS(sType,"mine"), true, &SoundParams );
|
|
|
|
/* 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:
|
|
m_soundAttackLaunch.Load( THEME->GetPathS(sType,"battle attack launch"), true, &SoundParams );
|
|
m_soundAttackEnding.Load( THEME->GetPathS(sType,"battle attack ending"), true, &SoundParams );
|
|
break;
|
|
default:
|
|
m_soundAttackLaunch.Load( THEME->GetPathS(sType,"course attack launch"), true, &SoundParams );
|
|
m_soundAttackEnding.Load( THEME->GetPathS(sType,"course attack ending"), true, &SoundParams );
|
|
break;
|
|
}
|
|
|
|
|
|
float fBalance = GameSoundManager::GetPlayerBalance( pn );
|
|
m_soundMine.SetProperty( "Pan", fBalance );
|
|
m_soundAttackLaunch.SetProperty( "Pan", fBalance );
|
|
m_soundAttackEnding.SetProperty( "Pan", fBalance );
|
|
|
|
|
|
if( HasVisibleParts() )
|
|
{
|
|
LuaThreadVariable var( "Player", LuaReference::Create(m_pPlayerState->m_PlayerNumber) );
|
|
LuaThreadVariable var2( "MultiPlayer", LuaReference::Create(m_pPlayerState->m_mp) );
|
|
|
|
m_sprCombo.Load( THEME->GetPathG(sType,"combo") );
|
|
m_sprCombo->SetName( "Combo" );
|
|
m_pActorWithComboPosition = &*m_sprCombo;
|
|
this->AddChild( m_sprCombo );
|
|
|
|
// todo: allow for judgments to be loaded per-column a la pop'n?
|
|
// see how HoldJudgments are handled below for an example, though
|
|
// it would need more work. -aj
|
|
m_sprJudgment.Load( THEME->GetPathG(sType,"judgment") );
|
|
m_sprJudgment->SetName( "Judgment" );
|
|
m_pActorWithJudgmentPosition = &*m_sprJudgment;
|
|
this->AddChild( m_sprJudgment );
|
|
}
|
|
else
|
|
{
|
|
m_pActorWithComboPosition = NULL;
|
|
m_pActorWithJudgmentPosition = NULL;
|
|
}
|
|
|
|
// Load HoldJudgments
|
|
m_vpHoldJudgment.resize( GAMESTATE->GetCurrentStyle()->m_iColsPerPlayer );
|
|
for( int i = 0; i < GAMESTATE->GetCurrentStyle()->m_iColsPerPlayer; ++i )
|
|
m_vpHoldJudgment[i] = NULL;
|
|
|
|
if( HasVisibleParts() )
|
|
{
|
|
for( int i = 0; i < GAMESTATE->GetCurrentStyle()->m_iColsPerPlayer; ++i )
|
|
{
|
|
HoldJudgment *pJudgment = new HoldJudgment;
|
|
// xxx: assumes sprite; todo: don't force 1x2 -aj
|
|
pJudgment->Load( THEME->GetPathG("HoldJudgment","label 1x2") );
|
|
m_vpHoldJudgment[i] = pJudgment;
|
|
this->AddChild( m_vpHoldJudgment[i] );
|
|
}
|
|
}
|
|
|
|
m_fNoteFieldHeight = GRAY_ARROWS_Y_REVERSE-GRAY_ARROWS_Y_STANDARD;
|
|
if( m_pNoteField )
|
|
{
|
|
m_pNoteField->Init( m_pPlayerState, m_fNoteFieldHeight );
|
|
ActorUtil::LoadAllCommands( *m_pNoteField, sType );
|
|
}
|
|
|
|
m_vbFretIsDown.resize( GAMESTATE->GetCurrentStyle()->m_iColsPerPlayer );
|
|
FOREACH( bool, m_vbFretIsDown, b )
|
|
*b = false;
|
|
|
|
m_fActiveRandomAttackStart = -1.0f;
|
|
}
|
|
/**
|
|
* @brief Determine if a TapNote needs a tap note style judgment.
|
|
* @param tn the TapNote in question.
|
|
* @return true if it does, false otherwise. */
|
|
static bool NeedsTapJudging( const TapNote &tn )
|
|
{
|
|
switch( tn.type )
|
|
{
|
|
DEFAULT_FAIL( tn.type );
|
|
case TapNote::tap:
|
|
case TapNote::hold_head:
|
|
case TapNote::mine:
|
|
case TapNote::lift:
|
|
return tn.result.tns == TNS_None;
|
|
case TapNote::hold_tail:
|
|
case TapNote::attack:
|
|
case TapNote::autoKeysound:
|
|
case TapNote::fake:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Determine if a TapNote needs a hold note style judgment.
|
|
* @param tn the TapNote in question.
|
|
* @return true if it does, false otherwise. */
|
|
static bool NeedsHoldJudging( const TapNote &tn )
|
|
{
|
|
switch( tn.type )
|
|
{
|
|
DEFAULT_FAIL( tn.type );
|
|
case TapNote::hold_head:
|
|
return tn.HoldResult.hns == HNS_None;
|
|
case TapNote::tap:
|
|
case TapNote::hold_tail:
|
|
case TapNote::mine:
|
|
case TapNote::lift:
|
|
case TapNote::attack:
|
|
case TapNote::autoKeysound:
|
|
case TapNote::fake:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void Player::Load()
|
|
{
|
|
m_bLoaded = true;
|
|
|
|
m_LastTapNoteScore = TNS_None;
|
|
// The editor can start playing in the middle of the song.
|
|
const int iNoteRow = BeatToNoteRowNotRounded( GAMESTATE->m_fSongBeat );
|
|
m_iFirstUncrossedRow = iNoteRow - 1;
|
|
m_pJudgedRows->Reset( iNoteRow );
|
|
|
|
// TODO: Remove use of PlayerNumber.
|
|
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
|
|
|
|
bool bOniDead = GAMESTATE->m_SongOptions.GetStage().m_LifeType == SongOptions::LIFE_BATTERY &&
|
|
(m_pPlayerStageStats == NULL || m_pPlayerStageStats->m_bFailed);
|
|
|
|
/* 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 */
|
|
// hurr why don't you just set m_bPropagateCommands on it then -aj
|
|
if( m_sprJudgment )
|
|
m_sprJudgment->PlayCommand("Reset");
|
|
if( m_pPlayerStageStats )
|
|
{
|
|
SetCombo( m_pPlayerStageStats->m_iCurCombo, m_pPlayerStageStats->m_iCurMissCombo ); // combo can persist between songs and games
|
|
}
|
|
if( m_pAttackDisplay )
|
|
m_pAttackDisplay->Init( m_pPlayerState );
|
|
|
|
/* 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 );
|
|
|
|
/* Apply transforms. */
|
|
NoteDataUtil::TransformNoteData( m_NoteData, m_pPlayerState->m_PlayerOptions.GetStage(), GAMESTATE->GetCurrentStyle()->m_StepsType );
|
|
|
|
const Song* pSong = GAMESTATE->m_pCurSong;
|
|
if( GAMESTATE->m_pCurGame->m_bAllowHopos )
|
|
NoteDataUtil::SetHopoPossibleFlags( pSong, m_NoteData );
|
|
|
|
switch( GAMESTATE->m_PlayMode )
|
|
{
|
|
case PLAY_MODE_RAVE:
|
|
case PLAY_MODE_BATTLE:
|
|
{
|
|
// ugly, ugly, ugly. Works only w/ dance.
|
|
// Why does this work only with dance? - Steve
|
|
// it has to do with there only being four cases. This is a lame
|
|
// workaround, but since only DDR has ever really implemented those
|
|
// modes, it's stayed like this. -aj
|
|
StepsType st = GAMESTATE->GetCurrentStyle()->m_StepsType;
|
|
NoteDataUtil::TransformNoteData( m_NoteData, m_pPlayerState->m_PlayerOptions.GetStage(), st );
|
|
|
|
// shuffle either p1 or p2
|
|
static int count = 0;
|
|
switch( count )
|
|
{
|
|
case 0:
|
|
case 3:
|
|
NoteDataUtil::Turn( m_NoteData, st, NoteDataUtil::left);
|
|
break;
|
|
case 1:
|
|
case 2:
|
|
NoteDataUtil::Turn( m_NoteData, st, NoteDataUtil::right);
|
|
break;
|
|
default:
|
|
ASSERT(0);
|
|
}
|
|
count++;
|
|
count %= 4;
|
|
}
|
|
break;
|
|
}
|
|
|
|
int iDrawDistanceAfterTargetsPixels = GAMESTATE->IsEditing() ? -100 : DRAW_DISTANCE_AFTER_TARGET_PIXELS;
|
|
int iDrawDistanceBeforeTargetsPixels = GAMESTATE->IsEditing() ? 400 : DRAW_DISTANCE_BEFORE_TARGET_PIXELS;
|
|
|
|
float fNoteFieldMiddle = (GRAY_ARROWS_Y_STANDARD+GRAY_ARROWS_Y_REVERSE)/2;
|
|
|
|
if( m_pNoteField && !bOniDead )
|
|
{
|
|
m_pNoteField->SetY( fNoteFieldMiddle );
|
|
m_pNoteField->Load( &m_NoteData, iDrawDistanceAfterTargetsPixels, iDrawDistanceBeforeTargetsPixels );
|
|
}
|
|
|
|
bool bPlayerUsingBothSides = GAMESTATE->GetCurrentStyle()->GetUsesCenteredArrows();
|
|
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 );
|
|
|
|
// Need to set Y positions of all these elements in Update since
|
|
// they change depending on PlayerOptions.
|
|
|
|
|
|
// 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.
|
|
RString sSongDir = pSong->GetSongDir();
|
|
m_vKeysounds.resize( pSong->m_vsKeysoundFile.size() );
|
|
|
|
// parameters are invalid somehow... -aj
|
|
RageSoundLoadParams SoundParams;
|
|
SoundParams.m_bSupportPan = true;
|
|
|
|
float fBalance = GameSoundManager::GetPlayerBalance( pn );
|
|
for( unsigned i=0; i<m_vKeysounds.size(); i++ )
|
|
{
|
|
RString sKeysoundFilePath = sSongDir + pSong->m_vsKeysoundFile[i];
|
|
RageSound& sound = m_vKeysounds[i];
|
|
if( sound.GetLoadedFilePath() != sKeysoundFilePath )
|
|
sound.Load( sKeysoundFilePath, true, &SoundParams );
|
|
sound.SetProperty( "Pan", fBalance );
|
|
sound.SetStopModeFromString( "stop" );
|
|
}
|
|
|
|
if( m_pPlayerStageStats )
|
|
SendComboMessages( m_pPlayerStageStats->m_iCurCombo, m_pPlayerStageStats->m_iCurMissCombo );
|
|
|
|
SAFE_DELETE( m_pIterNeedsTapJudging );
|
|
m_pIterNeedsTapJudging = new NoteData::all_tracks_iterator( m_NoteData.GetTapNoteRangeAllTracks(iNoteRow, MAX_NOTE_ROW) );
|
|
|
|
SAFE_DELETE( m_pIterNeedsHoldJudging );
|
|
m_pIterNeedsHoldJudging = new NoteData::all_tracks_iterator( m_NoteData.GetTapNoteRangeAllTracks(iNoteRow, MAX_NOTE_ROW ) );
|
|
|
|
SAFE_DELETE( m_pIterUncrossedRows );
|
|
m_pIterUncrossedRows = new NoteData::all_tracks_iterator( m_NoteData.GetTapNoteRangeAllTracks(iNoteRow, MAX_NOTE_ROW ) );
|
|
|
|
SAFE_DELETE( m_pIterUnjudgedRows );
|
|
m_pIterUnjudgedRows = new NoteData::all_tracks_iterator( m_NoteData.GetTapNoteRangeAllTracks(iNoteRow, MAX_NOTE_ROW ) );
|
|
|
|
SAFE_DELETE( m_pIterUnjudgedMineRows );
|
|
m_pIterUnjudgedMineRows = new NoteData::all_tracks_iterator( m_NoteData.GetTapNoteRangeAllTracks(iNoteRow, MAX_NOTE_ROW ) );
|
|
}
|
|
|
|
void Player::SendComboMessages( int iOldCombo, int iOldMissCombo )
|
|
{
|
|
const int iCurCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurCombo : 0;
|
|
if( iOldCombo > COMBO_STOPPED_AT && iCurCombo < COMBO_STOPPED_AT )
|
|
{
|
|
SCREENMAN->PostMessageToTopScreen( SM_ComboStopped, 0 );
|
|
}
|
|
|
|
if( m_bSendJudgmentAndComboMessages )
|
|
{
|
|
Message msg( "ComboChanged" );
|
|
msg.SetParam( "Player", m_pPlayerState->m_PlayerNumber );
|
|
msg.SetParam( "OldCombo", iOldCombo );
|
|
msg.SetParam( "OldMissCombo", iOldMissCombo );
|
|
if( m_pPlayerState )
|
|
msg.SetParam( "PlayerState", LuaReference::CreateFromPush(*m_pPlayerState) );
|
|
if( m_pPlayerStageStats )
|
|
msg.SetParam( "PlayerStageStats", LuaReference::CreateFromPush(*m_pPlayerStageStats) );
|
|
MESSAGEMAN->Broadcast( msg );
|
|
}
|
|
}
|
|
|
|
void Player::Update( float fDeltaTime )
|
|
{
|
|
const RageTimer now;
|
|
// Don't update if we haven't been loaded yet.
|
|
if( !m_bLoaded )
|
|
return;
|
|
|
|
//LOG->Trace( "Player::Update(%f)", fDeltaTime );
|
|
|
|
if( GAMESTATE->m_pCurSong==NULL )
|
|
return;
|
|
|
|
ActorFrame::Update( fDeltaTime );
|
|
|
|
if(m_pPlayerState->m_mp != MultiPlayer_Invalid)
|
|
{
|
|
/* In multiplayer, it takes too long to run player updates for every player each frame;
|
|
* with 32 players and three difficulties, we have 96 Players to update. Stagger these
|
|
* updates, by only updating a few players each update; since we don't have screen elements
|
|
* tightly tied to user actions in this mode, this doesn't degrade gameplay. Run 4 players
|
|
* per update, which means 12 Players in 3-difficulty mode.
|
|
*/
|
|
static int iCycle = 0;
|
|
iCycle = (iCycle + 1) % 8;
|
|
|
|
if((m_pPlayerState->m_mp % 8) != iCycle)
|
|
return;
|
|
}
|
|
|
|
const float fSongBeat = GAMESTATE->m_fSongBeat;
|
|
const int iSongRow = BeatToNoteRow( fSongBeat );
|
|
|
|
// Optimization: Don't spend time processing the things below that won't show
|
|
// if the Player doesn't show anything on the screen.
|
|
if( HasVisibleParts() )
|
|
{
|
|
// Random Attack Mod
|
|
if( m_pPlayerState->m_PlayerOptions.GetCurrent().m_fRandAttack )
|
|
{
|
|
float fCurrentGameTime = STATSMAN->m_CurStageStats.m_fGameplaySeconds;
|
|
|
|
const float fAttackRunTime = ATTACK_RUN_TIME_RANDOM;
|
|
|
|
// Don't start until 1 seconds into game, minimum
|
|
if( fCurrentGameTime > 1.0f )
|
|
{
|
|
/* Update the attack if there are no others currently running.
|
|
* Note that we have a new one activate a little early; This is
|
|
* to have a bit of overlap rather than an abrupt change. */
|
|
if( (fCurrentGameTime - m_fActiveRandomAttackStart) > (fAttackRunTime - 0.5f) )
|
|
{
|
|
m_fActiveRandomAttackStart = fCurrentGameTime;
|
|
|
|
Attack attRandomAttack;
|
|
attRandomAttack.sModifiers = ApplyRandomAttack();
|
|
attRandomAttack.fSecsRemaining = fAttackRunTime;
|
|
m_pPlayerState->LaunchAttack( attRandomAttack );
|
|
}
|
|
}
|
|
}
|
|
|
|
if( g_bEnableAttackSoundPlayback )
|
|
{
|
|
if( m_pPlayerState->m_bAttackBeganThisUpdate )
|
|
m_soundAttackLaunch.Play();
|
|
if( m_pPlayerState->m_bAttackEndedThisUpdate )
|
|
m_soundAttackEnding.Play();
|
|
}
|
|
|
|
if( m_pNoteField )
|
|
m_pNoteField->Update( fDeltaTime );
|
|
|
|
float fMiniPercent = m_pPlayerState->m_PlayerOptions.GetCurrent().m_fEffects[PlayerOptions::EFFECT_MINI];
|
|
float fTinyPercent = m_pPlayerState->m_PlayerOptions.GetCurrent().m_fEffects[PlayerOptions::EFFECT_TINY];
|
|
float fJudgmentZoom = min( powf(0.5f, fMiniPercent+fTinyPercent), 1.0f );
|
|
|
|
// Update Y positions
|
|
{
|
|
for( int c=0; c<GAMESTATE->GetCurrentStyle()->m_iColsPerPlayer; c++ )
|
|
{
|
|
float fPercentReverse = m_pPlayerState->m_PlayerOptions.GetCurrent().GetReversePercentForColumn(c);
|
|
float fHoldJudgeYPos = SCALE( fPercentReverse, 0.f, 1.f, HOLD_JUDGMENT_Y_STANDARD, HOLD_JUDGMENT_Y_REVERSE );
|
|
//float fGrayYPos = SCALE( fPercentReverse, 0.f, 1.f, GRAY_ARROWS_Y_STANDARD, GRAY_ARROWS_Y_REVERSE );
|
|
|
|
float fX = ArrowEffects::GetXPos( m_pPlayerState, c, 0 );
|
|
const float fZ = ArrowEffects::GetZPos( m_pPlayerState, c, 0 );
|
|
|
|
m_vpHoldJudgment[c]->SetX( fX );
|
|
m_vpHoldJudgment[c]->SetY( fHoldJudgeYPos );
|
|
m_vpHoldJudgment[c]->SetZ( fZ );
|
|
m_vpHoldJudgment[c]->SetZoom( fJudgmentZoom );
|
|
}
|
|
}
|
|
|
|
// NoteField accounts for reverse on its own now.
|
|
//if( m_pNoteField )
|
|
// m_pNoteField->SetY( fGrayYPos );
|
|
|
|
const bool bReverse = m_pPlayerState->m_PlayerOptions.GetCurrent().GetReversePercentForColumn(0) == 1;
|
|
float fPercentCentered = m_pPlayerState->m_PlayerOptions.GetCurrent().m_fScrolls[PlayerOptions::SCROLL_CENTERED];
|
|
|
|
if( m_pActorWithJudgmentPosition != NULL )
|
|
{
|
|
const Actor::TweenState &ts1 = m_tsJudgment[bReverse?1:0][0];
|
|
const Actor::TweenState &ts2 = m_tsJudgment[bReverse?1:0][1];
|
|
Actor::TweenState::MakeWeightedAverage( m_pActorWithJudgmentPosition->DestTweenState(), ts1, ts2, fPercentCentered );
|
|
}
|
|
|
|
if( m_pActorWithComboPosition != NULL )
|
|
{
|
|
const Actor::TweenState &ts1 = m_tsCombo[bReverse?1:0][0];
|
|
const Actor::TweenState &ts2 = m_tsCombo[bReverse?1:0][1];
|
|
Actor::TweenState::MakeWeightedAverage( m_pActorWithComboPosition->DestTweenState(), ts1, ts2, fPercentCentered );
|
|
}
|
|
|
|
float fNoteFieldZoom = 1 - fTinyPercent*0.5f;
|
|
if( m_pNoteField )
|
|
m_pNoteField->SetZoom( fNoteFieldZoom );
|
|
if( m_pActorWithJudgmentPosition != NULL )
|
|
m_pActorWithJudgmentPosition->SetZoom( m_pActorWithJudgmentPosition->GetZoom() * fJudgmentZoom );
|
|
if( m_pActorWithComboPosition != NULL )
|
|
m_pActorWithComboPosition->SetZoom( m_pActorWithComboPosition->GetZoom() * fJudgmentZoom );
|
|
}
|
|
|
|
// If we're paused, don't update tap or hold note logic, so hold notes can be released
|
|
// during pause.
|
|
if( m_bPaused )
|
|
return;
|
|
|
|
// Check for a strum miss
|
|
if( m_pPlayerState->m_fLastStrumMusicSeconds != -1 &&
|
|
m_pPlayerState->m_fLastStrumMusicSeconds + g_fTimingWindowStrum < GAMESTATE->m_fMusicSeconds )
|
|
{
|
|
DoStrumMiss();
|
|
}
|
|
|
|
// update pressed flag
|
|
const int iNumCols = GAMESTATE->GetCurrentStyle()->m_iColsPerPlayer;
|
|
ASSERT_M( iNumCols <= MAX_COLS_PER_PLAYER, ssprintf("%i > %i", iNumCols, MAX_COLS_PER_PLAYER) );
|
|
for( int col=0; col < iNumCols; ++col )
|
|
{
|
|
ASSERT( m_pPlayerState );
|
|
|
|
// TODO: Remove use of PlayerNumber.
|
|
GameInput GameI = GAMESTATE->GetCurrentStyle()->StyleInputToGameInput( col, m_pPlayerState->m_PlayerNumber );
|
|
|
|
bool bIsHoldingButton = INPUTMAPPER->IsBeingPressed( GameI );
|
|
|
|
// 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 );
|
|
}
|
|
|
|
// 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;
|
|
if( !m_NoteData.IsHoldNoteAtRow(iTrack, iSongRow, &iHeadRow) )
|
|
iHeadRow = iSongRow;
|
|
|
|
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;
|
|
|
|
Step( iTrack, iHeadRow, now, false, false );
|
|
if( m_pPlayerState->m_PlayerController == PC_AUTOPLAY )
|
|
{
|
|
STATSMAN->m_CurStageStats.m_bUsedAutoplay = true;
|
|
if( m_pPlayerStageStats )
|
|
m_pPlayerStageStats->m_bDisqualified = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// update HoldNotes logic
|
|
{
|
|
|
|
// Fast forward to the first that needs hold judging.
|
|
{
|
|
NoteData::all_tracks_iterator &iter = *m_pIterNeedsHoldJudging;
|
|
while( !iter.IsAtEnd() && iter.Row() <= iSongRow && !NeedsHoldJudging(*iter) )
|
|
++iter;
|
|
}
|
|
|
|
vector<TrackRowTapNote> vHoldNotesToGradeTogether;
|
|
int iRowOfLastHoldNote = -1;
|
|
NoteData::all_tracks_iterator iter = *m_pIterNeedsHoldJudging; // copy
|
|
for( ; !iter.IsAtEnd() && iter.Row() <= iSongRow; ++iter )
|
|
{
|
|
TapNote &tn = *iter;
|
|
if( tn.type != TapNote::hold_head )
|
|
continue;
|
|
|
|
int iTrack = iter.Track();
|
|
int iRow = iter.Row();
|
|
TrackRowTapNote trtn = { iTrack, iRow, &tn };
|
|
|
|
/* All holds must be of the same subType because fLife is handled
|
|
* in different ways depending on the SubType. Handle Rolls one at
|
|
* a time and don't mix with holds. */
|
|
switch( tn.subType )
|
|
{
|
|
DEFAULT_FAIL( tn.subType );
|
|
case TapNote::hold_head_hold:
|
|
break;
|
|
case TapNote::hold_head_roll:
|
|
{
|
|
vector<TrackRowTapNote> v;
|
|
v.push_back( trtn );
|
|
UpdateHoldNotes( iSongRow, fDeltaTime, v );
|
|
}
|
|
continue; // don't process this below
|
|
}
|
|
/*
|
|
case TapNote::hold_head_mine:
|
|
break;
|
|
*/
|
|
|
|
if( iRow != iRowOfLastHoldNote || !JUDGE_HOLD_NOTES_ON_SAME_ROW_TOGETHER )
|
|
{
|
|
if( !vHoldNotesToGradeTogether.empty() )
|
|
{
|
|
//LOG->Trace( ssprintf("UpdateHoldNotes; %i != %i || !judge holds on same row together",iRow,iRowOfLastHoldNote) );
|
|
UpdateHoldNotes( iSongRow, fDeltaTime, vHoldNotesToGradeTogether );
|
|
vHoldNotesToGradeTogether.clear();
|
|
}
|
|
}
|
|
iRowOfLastHoldNote = iRow;
|
|
vHoldNotesToGradeTogether.push_back( trtn );
|
|
}
|
|
|
|
if( !vHoldNotesToGradeTogether.empty() )
|
|
{
|
|
//LOG->Trace("UpdateHoldNotes since !vHoldNotesToGradeTogether.empty()");
|
|
UpdateHoldNotes( iSongRow, fDeltaTime, vHoldNotesToGradeTogether );
|
|
vHoldNotesToGradeTogether.clear();
|
|
}
|
|
}
|
|
|
|
{
|
|
// 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 )
|
|
{
|
|
if( GAMESTATE->IsPlayerEnabled(m_pPlayerState) )
|
|
{
|
|
if(GAMESTATE->m_bDelay)
|
|
{
|
|
if( !m_bDelay )
|
|
m_bDelay = true;
|
|
}
|
|
else
|
|
{
|
|
if(m_bDelay)
|
|
{
|
|
if(m_pPlayerState->m_PlayerController != PC_HUMAN)
|
|
{
|
|
CrossedRows( iRowNow-1, now );
|
|
}
|
|
m_bDelay = false;
|
|
}
|
|
CrossedRows( iRowNow, now );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for completely judged rows.
|
|
UpdateJudgedRows();
|
|
|
|
// Check for TapNote misses
|
|
UpdateTapNotesMissedOlderThan( GetMaxStepDistanceSeconds() );
|
|
|
|
// process transforms that are waiting to be applied
|
|
ApplyWaitingTransforms();
|
|
}
|
|
|
|
// Update a group of holds with shared scoring/life. All of these holds will have the same start row.
|
|
void Player::UpdateHoldNotes( int iSongRow, float fDeltaTime, vector<TrackRowTapNote> &vTN )
|
|
{
|
|
ASSERT( !vTN.empty() );
|
|
|
|
//LOG->Trace("--------------------------------");
|
|
/*
|
|
LOG->Trace("[Player::UpdateHoldNotes] begins");
|
|
LOG->Trace( ssprintf("song row %i, deltaTime = %f",iSongRow,fDeltaTime) );
|
|
*/
|
|
|
|
int iStartRow = vTN[0].iRow;
|
|
int iMaxEndRow = INT_MIN;
|
|
int iFirstTrackWithMaxEndRow = -1;
|
|
|
|
TapNote::SubType subType = TapNote::SubType_Invalid;
|
|
FOREACH( TrackRowTapNote, vTN, trtn )
|
|
{
|
|
int iTrack = trtn->iTrack;
|
|
ASSERT( iStartRow == trtn->iRow );
|
|
TapNote &tn = *trtn->pTN;
|
|
int iEndRow = iStartRow + tn.iDuration;
|
|
if( subType == TapNote::SubType_Invalid )
|
|
subType = tn.subType;
|
|
|
|
/* All holds must be of the same subType because fLife is handled
|
|
* in different ways depending on the SubType. */
|
|
ASSERT( tn.subType == subType );
|
|
|
|
if( iEndRow > iMaxEndRow )
|
|
{
|
|
iMaxEndRow = iEndRow;
|
|
iFirstTrackWithMaxEndRow = iTrack;
|
|
}
|
|
}
|
|
|
|
ASSERT( iFirstTrackWithMaxEndRow != -1 );
|
|
//LOG->Trace( ssprintf("start row: %i; max/end row: = %i",iStartRow,iMaxEndRow) );
|
|
//LOG->Trace( ssprintf("first track with max end row = %i",iFirstTrackWithMaxEndRow) );
|
|
//LOG->Trace( ssprintf("max end row - start row (in beats) = %f",NoteRowToBeat(iMaxEndRow)-NoteRowToBeat(iStartRow)) );
|
|
|
|
FOREACH( TrackRowTapNote, vTN, trtn )
|
|
{
|
|
TapNote &tn = *trtn->pTN;
|
|
|
|
// set hold flags so NoteField can do intelligent drawing
|
|
tn.HoldResult.bHeld = false;
|
|
tn.HoldResult.bActive = false;
|
|
|
|
int iRow = trtn->iRow;
|
|
//LOG->Trace( ssprintf("this row: %i",iRow) );
|
|
|
|
// If the song beat is in the range of this hold:
|
|
if( iRow <= iSongRow && iRow <= iMaxEndRow )
|
|
{
|
|
//LOG->Trace( ssprintf("overlap time before: %f",tn.HoldResult.fOverlappedTime) );
|
|
tn.HoldResult.fOverlappedTime += fDeltaTime;
|
|
//LOG->Trace( ssprintf("overlap time after: %f",tn.HoldResult.fOverlappedTime) );
|
|
}
|
|
else
|
|
{
|
|
//LOG->Trace( "overlap time = 0" );
|
|
tn.HoldResult.fOverlappedTime = 0;
|
|
}
|
|
}
|
|
|
|
HoldNoteScore hns = vTN[0].pTN->HoldResult.hns;
|
|
float fLife = vTN[0].pTN->HoldResult.fLife;
|
|
|
|
if( hns != HNS_None ) // if this HoldNote already has a result
|
|
{
|
|
//LOG->Trace("hold note has a result, skipping.");
|
|
return; // we don't need to update the logic for this group
|
|
}
|
|
|
|
//LOG->Trace("hold note doesn't already have result, let's check.");
|
|
|
|
//LOG->Trace( ssprintf("[C++] hold note score: %s",HoldNoteScoreToString(hns).c_str()) );
|
|
//LOG->Trace(ssprintf("[Player::UpdateHoldNotes] fLife = %f",fLife));
|
|
|
|
bool bSteppedOnHead = true;
|
|
bool bHeadJudged = true;
|
|
FOREACH( TrackRowTapNote, vTN, trtn )
|
|
{
|
|
TapNote &tn = *trtn->pTN;
|
|
TapNoteScore tns = tn.result.tns;
|
|
//LOG->Trace( ssprintf("[C++] tap note score: %s",StringConversion::ToString(tns).c_str()) );
|
|
|
|
// TODO: When using JUDGE_HOLD_NOTES_ON_SAME_ROW_TOGETHER, require that the whole row of
|
|
// taps was hit before activating this group of holds.
|
|
/* Something about the logic in this section is causing 192nd steps to
|
|
* fail for some odd reason. -aj */
|
|
bSteppedOnHead &= (tns != TNS_Miss && tns != TNS_None); // did they step on the start of this hold?
|
|
bHeadJudged &= (tns != TNS_None); // has this hold really even started yet?
|
|
|
|
/*
|
|
if(bSteppedOnHead)
|
|
LOG->Trace("[Player::UpdateHoldNotes] player stepped on head");
|
|
else
|
|
LOG->Trace("[Player::UpdateHoldNotes] player didn't step on the head");
|
|
*/
|
|
}
|
|
|
|
bool bInitiatedNote;
|
|
if( REQUIRE_STEP_ON_HOLD_HEADS )
|
|
{
|
|
// XXX HACK: Miniholds (a 64th or 192nd length hold) will not always
|
|
// register as Held, even if you hit the note. This is considered a
|
|
// major roadblock to adoption, so until a proper fix is found,
|
|
// DON'T REMOVE THIS HACK! -aj
|
|
/*if( iMaxEndRow-iStartRow <= 4 )
|
|
bInitiatedNote = true;
|
|
else*/
|
|
bInitiatedNote = bSteppedOnHead;
|
|
}
|
|
else
|
|
{
|
|
bInitiatedNote = true;
|
|
bHeadJudged = true;
|
|
}
|
|
|
|
bool bIsHoldingButton = true;
|
|
FOREACH( TrackRowTapNote, vTN, trtn )
|
|
{
|
|
int iTrack = trtn->iTrack;
|
|
|
|
// TODO: Remove use of PlayerNumber.
|
|
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
|
|
|
|
if( m_pPlayerState->m_PlayerController != PC_HUMAN )
|
|
{
|
|
// TODO: Make the CPU miss sometimes.
|
|
if( m_pPlayerState->m_PlayerController == PC_AUTOPLAY )
|
|
{
|
|
STATSMAN->m_CurStageStats.m_bUsedAutoplay = true;
|
|
if( m_pPlayerStageStats != NULL )
|
|
m_pPlayerStageStats->m_bDisqualified = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
GameInput GameI = GAMESTATE->GetCurrentStyle()->StyleInputToGameInput( iTrack, pn );
|
|
// this previously read as bIsHoldingButton &=
|
|
// was there a specific reason for this? - Friez
|
|
bIsHoldingButton &= INPUTMAPPER->IsBeingPressed( GameI, m_pPlayerState->m_mp );
|
|
}
|
|
}
|
|
|
|
if( bInitiatedNote && fLife != 0 && bHeadJudged )
|
|
{
|
|
//LOG->Trace("[Player::UpdateHoldNotes] initiated note, 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. */
|
|
FOREACH( TrackRowTapNote, vTN, trtn )
|
|
{
|
|
TapNote &tn = *trtn->pTN;
|
|
int iEndRow = iStartRow + tn.iDuration;
|
|
|
|
//LOG->Trace(ssprintf("trying for min between iSongRow (%i) and iEndRow (%i) (duration %i)",iSongRow,iEndRow,tn.iDuration));
|
|
trtn->pTN->HoldResult.iLastHeldRow = min( iSongRow, iEndRow );
|
|
}
|
|
}
|
|
|
|
// If the song beat is in the range of this hold:
|
|
if( iStartRow <= iSongRow && iStartRow <= iMaxEndRow && bHeadJudged )
|
|
{
|
|
switch( subType )
|
|
{
|
|
case TapNote::hold_head_hold:
|
|
FOREACH( TrackRowTapNote, vTN, trtn )
|
|
{
|
|
TapNote &tn = *trtn->pTN;
|
|
|
|
// set hold flag so NoteField can do intelligent drawing
|
|
tn.HoldResult.bHeld = bIsHoldingButton && bInitiatedNote;
|
|
tn.HoldResult.bActive = bInitiatedNote;
|
|
}
|
|
|
|
if( bInitiatedNote && bIsHoldingButton )
|
|
{
|
|
//LOG->Trace("bInitiatedNote && bIsHoldingButton; Increasing hold life to MAX_HOLD_LIFE");
|
|
// Increase life
|
|
fLife = MAX_HOLD_LIFE; // was 1 -aj
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
LOG->Trace("Checklist:");
|
|
if(bInitiatedNote)
|
|
LOG->Trace("[X] Initiated Note");
|
|
else
|
|
LOG->Trace("[ ] Initiated Note");
|
|
|
|
if(bIsHoldingButton)
|
|
LOG->Trace("[X] Holding Button");
|
|
else
|
|
LOG->Trace("[ ] Holding Button");
|
|
*/
|
|
|
|
// Decrease life
|
|
//LOG->Trace("fLife before minus: %f",fLife);
|
|
fLife -= fDeltaTime/GetWindowSeconds(TW_Hold);
|
|
//LOG->Trace("fLife before clamp: %f",fLife);
|
|
fLife = max( fLife, 0 ); // clamp
|
|
//LOG->Trace("fLife after: %f",fLife);
|
|
}
|
|
break;
|
|
case TapNote::hold_head_roll:
|
|
FOREACH( TrackRowTapNote, vTN, trtn )
|
|
{
|
|
TapNote &tn = *trtn->pTN;
|
|
tn.HoldResult.bHeld = true;
|
|
tn.HoldResult.bActive = bInitiatedNote;
|
|
}
|
|
|
|
// give positive life in Step(), not here.
|
|
|
|
// Decrease life
|
|
fLife -= fDeltaTime/GetWindowSeconds(TW_Roll);
|
|
fLife = max( fLife, 0 ); // clamp
|
|
break;
|
|
/*
|
|
case TapNote::hold_head_mine:
|
|
break;
|
|
*/
|
|
default:
|
|
ASSERT(0);
|
|
}
|
|
}
|
|
|
|
// TODO: Cap the active time passed to the score keeper to the actual start time and end time of the hold.
|
|
if( vTN[0].pTN->HoldResult.bActive )
|
|
{
|
|
float fSecondsActiveSinceLastUpdate = fDeltaTime * GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate;
|
|
if( m_pPrimaryScoreKeeper )
|
|
m_pPrimaryScoreKeeper->HandleHoldActiveSeconds( fSecondsActiveSinceLastUpdate );
|
|
if( m_pSecondaryScoreKeeper )
|
|
m_pSecondaryScoreKeeper->HandleHoldActiveSeconds( fSecondsActiveSinceLastUpdate );
|
|
}
|
|
|
|
// check for LetGo. If the head was missed completely, don't count an LetGo.
|
|
/* Why? If you never step on the head, then it will be left as HNS_None,
|
|
* which doesn't seem correct. */
|
|
if( IMMEDIATE_HOLD_LET_GO )
|
|
{
|
|
if( bInitiatedNote && fLife == 0 && bHeadJudged ) // the player has not pressed the button for a long time!
|
|
{
|
|
//LOG->Trace("LetGo from life == 0 (did initiate hold)");
|
|
hns = HNS_LetGo;
|
|
}
|
|
}
|
|
|
|
// score hold notes that have passed
|
|
if( iSongRow >= iMaxEndRow && bHeadJudged )
|
|
{
|
|
bool bLetGoOfHoldNote = false;
|
|
|
|
/* Score rolls that end with fLife == 0 as LetGo, even if
|
|
* HOLD_CHECKPOINTS is on. Rolls don't have iCheckpointsMissed set, so,
|
|
* unless we check Life == 0, rolls would always be scored as Held. */
|
|
bool bAllowHoldCheckpoints;
|
|
switch( subType )
|
|
{
|
|
DEFAULT_FAIL( subType );
|
|
case TapNote::hold_head_hold:
|
|
bAllowHoldCheckpoints = true;
|
|
break;
|
|
case TapNote::hold_head_roll:
|
|
bAllowHoldCheckpoints = false;
|
|
break;
|
|
/*
|
|
case TapNote::hold_head_mine:
|
|
bAllowHoldCheckpoints = true;
|
|
break;
|
|
*/
|
|
}
|
|
|
|
if( HOLD_CHECKPOINTS && bAllowHoldCheckpoints )
|
|
{
|
|
//LOG->Trace("(hold checkpoints are allowed and enabled.)");
|
|
int iCheckpointsHit = 0;
|
|
int iCheckpointsMissed = 0;
|
|
FOREACH( TrackRowTapNote, vTN, v )
|
|
{
|
|
iCheckpointsHit += v->pTN->HoldResult.iCheckpointsHit;
|
|
iCheckpointsMissed += v->pTN->HoldResult.iCheckpointsMissed;
|
|
}
|
|
bLetGoOfHoldNote = iCheckpointsMissed > 0 || iCheckpointsHit == 0;
|
|
|
|
// TRICKY: If the hold is so short that it has no checkpoints,
|
|
// then mark it as Held if the head was stepped on.
|
|
if( iCheckpointsHit == 0 && iCheckpointsMissed == 0 )
|
|
bLetGoOfHoldNote = !bSteppedOnHead;
|
|
|
|
/*
|
|
if(bLetGoOfHoldNote)
|
|
LOG->Trace("let go of hold note, life is 0");
|
|
else
|
|
LOG->Trace("did not let go of hold note :D");
|
|
*/
|
|
}
|
|
else
|
|
{
|
|
//LOG->Trace("(hold checkpoints disabled.)");
|
|
bLetGoOfHoldNote = fLife == 0;
|
|
/*
|
|
if(bLetGoOfHoldNote)
|
|
LOG->Trace("let go of hold note, life is 0");
|
|
else
|
|
LOG->Trace("did not let go of hold note :D");
|
|
*/
|
|
}
|
|
|
|
if( bInitiatedNote )
|
|
{
|
|
if(!bLetGoOfHoldNote)
|
|
{
|
|
//LOG->Trace("initiated note and didn't let go");
|
|
fLife = 1; // xxx: should be MAX_HOLD_LIFE instead? -aj
|
|
hns = HNS_Held;
|
|
bool bBright = m_pPlayerStageStats && m_pPlayerStageStats->m_iCurCombo>(int)BRIGHT_GHOST_COMBO_THRESHOLD;
|
|
if( m_pNoteField )
|
|
{
|
|
FOREACH( TrackRowTapNote, vTN, trtn )
|
|
{
|
|
int iTrack = trtn->iTrack;
|
|
m_pNoteField->DidHoldNote( iTrack, HNS_Held, bBright ); // bright ghost flash
|
|
}
|
|
}
|
|
}
|
|
|
|
else
|
|
{
|
|
//LOG->Trace("initiated note and let go :(");
|
|
}
|
|
}
|
|
else if( SCORE_MISSED_HOLDS_AND_ROLLS )
|
|
{
|
|
hns = HNS_LetGo;
|
|
}
|
|
else
|
|
{
|
|
hns = HNS_None;
|
|
}
|
|
}
|
|
|
|
FOREACH( TrackRowTapNote, vTN, trtn )
|
|
{
|
|
TapNote &tn = *trtn->pTN;
|
|
tn.HoldResult.fLife = fLife;
|
|
tn.HoldResult.hns = hns;
|
|
// Stop the playing keysound for the hold note.
|
|
// I think this causes crashes too -aj
|
|
// Possibly fixed.
|
|
if( tn.iKeysoundIndex >= 0 && tn.iKeysoundIndex < (int) m_vKeysounds.size() )
|
|
{
|
|
if( tn.subType == TapNote::hold_head_roll )
|
|
{
|
|
m_vKeysounds[tn.iKeysoundIndex].SetProperty ("Volume", max(0.0, min(1.0, fLife * 2.0)));
|
|
}
|
|
else
|
|
{
|
|
m_vKeysounds[tn.iKeysoundIndex].SetProperty ("Volume", max(0.0, min(1.0, fLife * 10.0 - 8.5)));
|
|
}
|
|
if (tn.HoldResult.fLife == 0)
|
|
{
|
|
m_vKeysounds[tn.iKeysoundIndex].StopPlaying();
|
|
}
|
|
}
|
|
}
|
|
|
|
if( hns != HNS_None )
|
|
{
|
|
//LOG->Trace("tap note scoring time.");
|
|
TapNote &tn = *vTN[0].pTN;
|
|
HandleHoldScore( tn );
|
|
//LOG->Trace("hold result = %s",StringConversion::ToString(tn.HoldResult.hns).c_str());
|
|
SetHoldJudgment( tn.result.tns, tn.HoldResult.hns, iFirstTrackWithMaxEndRow );
|
|
}
|
|
//LOG->Trace("[Player::UpdateHoldNotes] ends");
|
|
}
|
|
|
|
void Player::ApplyWaitingTransforms()
|
|
{
|
|
for( unsigned j=0; j<m_pPlayerState->m_ModsToApply.size(); j++ )
|
|
{
|
|
const Attack &mod = m_pPlayerState->m_ModsToApply[j];
|
|
PlayerOptions po;
|
|
// if re-adding noteskin changes, blank out po.m_sNoteSkin. -aj
|
|
po.FromString( mod.sModifiers );
|
|
|
|
float fStartBeat, fEndBeat;
|
|
mod.GetRealtimeAttackBeats( GAMESTATE->m_pCurSong, m_pPlayerState, fStartBeat, fEndBeat );
|
|
fEndBeat = min( fEndBeat, m_NoteData.GetLastBeat() );
|
|
|
|
LOG->Trace( "Applying transform '%s' from %f to %f to '%s'", mod.sModifiers.c_str(), fStartBeat, fEndBeat,
|
|
GAMESTATE->m_pCurSong->GetTranslitMainTitle().c_str() );
|
|
|
|
// if re-adding noteskin changes, this is one place to edit -aj
|
|
|
|
NoteDataUtil::TransformNoteData( m_NoteData, po, GAMESTATE->GetCurrentStyle()->m_StepsType, BeatToNoteRow(fStartBeat), BeatToNoteRow(fEndBeat) );
|
|
}
|
|
m_pPlayerState->m_ModsToApply.clear();
|
|
}
|
|
|
|
void Player::DrawPrimitives()
|
|
{
|
|
// TODO: Remove use of PlayerNumber.
|
|
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
|
|
|
|
// May have both players in doubles (for battle play); only draw primary player.
|
|
if( GAMESTATE->GetCurrentStyle()->m_StyleType == StyleType_OnePlayerTwoSides &&
|
|
pn != GAMESTATE->m_MasterPlayerNumber )
|
|
return;
|
|
|
|
// Draw these below everything else.
|
|
// xxx: if NoteField Board is enabled and COMBO_UNDER_FIELD, we really want
|
|
// the combo under the field but over the notefield board. -aj
|
|
if( COMBO_UNDER_FIELD && m_pPlayerState->m_PlayerOptions.GetCurrent().m_fBlind == 0 )
|
|
{
|
|
if( m_sprCombo )
|
|
m_sprCombo->Draw();
|
|
}
|
|
|
|
if( m_pAttackDisplay )
|
|
m_pAttackDisplay->Draw();
|
|
|
|
if( TAP_JUDGMENTS_UNDER_FIELD )
|
|
DrawTapJudgments();
|
|
|
|
if( HOLD_JUDGMENTS_UNDER_FIELD )
|
|
DrawHoldJudgments();
|
|
|
|
float fTilt = m_pPlayerState->m_PlayerOptions.GetCurrent().m_fPerspectiveTilt;
|
|
float fSkew = m_pPlayerState->m_PlayerOptions.GetCurrent().m_fSkew;
|
|
bool bReverse = m_pPlayerState->m_PlayerOptions.GetCurrent().GetReversePercentForColumn(0)>0.5;
|
|
|
|
DISPLAY->CameraPushMatrix();
|
|
DISPLAY->PushMatrix();
|
|
|
|
float fCenterY = this->GetY()+(GRAY_ARROWS_Y_STANDARD+GRAY_ARROWS_Y_REVERSE)/2;
|
|
|
|
DISPLAY->LoadMenuPerspective( 45, SCREEN_WIDTH, SCREEN_HEIGHT, SCALE(fSkew,0.f,1.f,this->GetX(),SCREEN_CENTER_X), fCenterY );
|
|
|
|
if( m_pNoteField )
|
|
{
|
|
float fOriginalY = m_pNoteField->GetY();
|
|
|
|
float fTiltDegrees = SCALE(fTilt,-1.f,+1.f,+30,-30) * (bReverse?-1:1);
|
|
|
|
float fZoom = SCALE( m_pPlayerState->m_PlayerOptions.GetCurrent().m_fEffects[PlayerOptions::EFFECT_TINY], 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 );
|
|
|
|
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);
|
|
|
|
m_pNoteField->SetY( fOriginalY + fYOffset );
|
|
m_pNoteField->SetZoom( fZoom );
|
|
m_pNoteField->SetRotationX( fTiltDegrees );
|
|
m_pNoteField->Draw();
|
|
|
|
m_pNoteField->SetY( fOriginalY );
|
|
}
|
|
|
|
DISPLAY->CameraPopMatrix();
|
|
DISPLAY->PopMatrix();
|
|
|
|
// m_pNoteField->m_sprBoard->GetVisible()
|
|
if( !(bool)COMBO_UNDER_FIELD && m_pPlayerState->m_PlayerOptions.GetCurrent().m_fBlind == 0 )
|
|
if( m_sprCombo )
|
|
m_sprCombo->Draw();
|
|
|
|
if( !(bool)TAP_JUDGMENTS_UNDER_FIELD )
|
|
DrawTapJudgments();
|
|
|
|
if( !(bool)HOLD_JUDGMENTS_UNDER_FIELD )
|
|
DrawHoldJudgments();
|
|
}
|
|
|
|
void Player::DrawTapJudgments()
|
|
{
|
|
if( m_pPlayerState->m_PlayerOptions.GetCurrent().m_fBlind > 0 )
|
|
return;
|
|
|
|
if( m_sprJudgment )
|
|
m_sprJudgment->Draw();
|
|
}
|
|
|
|
void Player::DrawHoldJudgments()
|
|
{
|
|
if( m_pPlayerState->m_PlayerOptions.GetCurrent().m_fBlind > 0 )
|
|
return;
|
|
|
|
for( int c=0; c<m_NoteData.GetNumTracks(); c++ )
|
|
if( m_vpHoldJudgment[c] )
|
|
m_vpHoldJudgment[c]->Draw();
|
|
}
|
|
|
|
|
|
void Player::ChangeLife( TapNoteScore tns )
|
|
{
|
|
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
|
|
if( m_pLifeMeter )
|
|
m_pLifeMeter->ChangeLife( tns );
|
|
if( m_pCombinedLifeMeter )
|
|
m_pCombinedLifeMeter->ChangeLife( pn, tns );
|
|
|
|
ChangeLifeRecord();
|
|
|
|
switch( tns )
|
|
{
|
|
case TNS_None:
|
|
case TNS_Miss:
|
|
case TNS_CheckpointMiss:
|
|
case TNS_HitMine:
|
|
++m_pPlayerState->m_iTapsMissedSinceLastHasteUpdate;
|
|
break;
|
|
default:
|
|
++m_pPlayerState->m_iTapsHitSinceLastHasteUpdate;
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Player::ChangeLife( HoldNoteScore hns, TapNoteScore tns )
|
|
{
|
|
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
|
|
if( m_pLifeMeter )
|
|
m_pLifeMeter->ChangeLife( hns, tns );
|
|
if( m_pCombinedLifeMeter )
|
|
m_pCombinedLifeMeter->ChangeLife( pn, hns, tns );
|
|
|
|
ChangeLifeRecord();
|
|
}
|
|
|
|
void Player::ChangeLifeRecord()
|
|
{
|
|
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
|
|
float fLife = -1;
|
|
if( m_pLifeMeter )
|
|
{
|
|
fLife = m_pLifeMeter->GetLife();
|
|
}
|
|
else if( m_pCombinedLifeMeter )
|
|
{
|
|
fLife = GAMESTATE->m_fTugLifePercentP1;
|
|
if( pn == PLAYER_2 )
|
|
fLife = 1.0f - fLife;
|
|
}
|
|
if( fLife != -1 )
|
|
if( m_pPlayerStageStats )
|
|
m_pPlayerStageStats->SetLifeRecordAt( fLife, STATSMAN->m_CurStageStats.m_fStepsSeconds );
|
|
}
|
|
|
|
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;
|
|
if( GAMESTATE->m_pCurSong->m_Timing.IsWarpAtRow( begin->first ) )
|
|
break;
|
|
if( tn.type == TapNote::empty )
|
|
break;
|
|
if( !bAllowGraded && tn.result.tns != TNS_None )
|
|
break;
|
|
|
|
return begin->first;
|
|
} while(0);
|
|
|
|
if( bForward )
|
|
++begin;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
// Find the closest note to fBeat.
|
|
int Player::GetClosestNote( int col, int iNoteRow, int iMaxRowsAhead, int iMaxRowsBehind, bool bAllowGraded ) const
|
|
{
|
|
// Start at iIndexStartLookingAt and search outward.
|
|
int iNextIndex = GetClosestNoteDirectional( col, iNoteRow, iNoteRow+iMaxRowsAhead, bAllowGraded, true );
|
|
int iPrevIndex = GetClosestNoteDirectional( col, iNoteRow-iMaxRowsBehind, iNoteRow, bAllowGraded, false );
|
|
|
|
if( iNextIndex == -1 && iPrevIndex == -1 )
|
|
return -1;
|
|
if( iNextIndex == -1 )
|
|
return iPrevIndex;
|
|
if( iPrevIndex == -1 )
|
|
return iNextIndex;
|
|
|
|
/* Figure out which row is closer. */
|
|
if( abs(iNoteRow-iNextIndex) > abs(iNoteRow-iPrevIndex) )
|
|
return iPrevIndex;
|
|
else
|
|
return iNextIndex;
|
|
}
|
|
|
|
int Player::GetClosestNonEmptyRowDirectional( int iStartRow, int iEndRow, bool bAllowGraded, bool bForward ) const
|
|
{
|
|
if( bForward )
|
|
{
|
|
NoteData::all_tracks_iterator iter = m_NoteData.GetTapNoteRangeAllTracks( iStartRow, iEndRow );
|
|
|
|
while( !iter.IsAtEnd() )
|
|
{
|
|
if( NoteDataWithScoring::IsRowCompletelyJudged(m_NoteData, iter.Row()) )
|
|
{
|
|
++iter;
|
|
continue;
|
|
}
|
|
if( GAMESTATE->m_pCurSong->m_Timing.IsWarpAtRow( iter.Row() ) )
|
|
{
|
|
++iter;
|
|
continue;
|
|
}
|
|
return iter.Row();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
NoteData::all_tracks_reverse_iterator iter = m_NoteData.GetTapNoteRangeAllTracksReverse( iStartRow, iEndRow );
|
|
|
|
while( !iter.IsAtEnd() )
|
|
{
|
|
if( NoteDataWithScoring::IsRowCompletelyJudged(m_NoteData, iter.Row()) )
|
|
{
|
|
++iter;
|
|
continue;
|
|
}
|
|
return iter.Row();
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
// Find the closest note to fBeat.
|
|
int Player::GetClosestNonEmptyRow( int iNoteRow, int iMaxRowsAhead, int iMaxRowsBehind, bool bAllowGraded ) const
|
|
{
|
|
// Start at iIndexStartLookingAt and search outward.
|
|
int iNextRow = GetClosestNonEmptyRowDirectional( iNoteRow, iNoteRow+iMaxRowsAhead, bAllowGraded, true );
|
|
int iPrevRow = GetClosestNonEmptyRowDirectional( iNoteRow-iMaxRowsBehind, iNoteRow, bAllowGraded, false );
|
|
|
|
if( iNextRow == -1 && iPrevRow == -1 )
|
|
return -1;
|
|
if( iNextRow == -1 )
|
|
return iPrevRow;
|
|
if( iPrevRow == -1 )
|
|
return iNextRow;
|
|
|
|
/* Figure out which row is closer. */
|
|
if( abs(iNoteRow-iNextRow) > abs(iNoteRow-iPrevRow) )
|
|
return iPrevRow;
|
|
else
|
|
return iNextRow;
|
|
}
|
|
|
|
bool Player::IsOniDead() const
|
|
{
|
|
// If we're playing on oni and we've died, do nothing.
|
|
return GAMESTATE->m_SongOptions.GetCurrent().m_LifeType == SongOptions::LIFE_BATTERY && m_pPlayerStageStats && m_pPlayerStageStats->m_bFailed;
|
|
}
|
|
|
|
void Player::Fret( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease )
|
|
{
|
|
if( IsOniDead() )
|
|
return;
|
|
|
|
DEBUG_ASSERT_M( col >= 0 && col <= m_NoteData.GetNumTracks(), ssprintf("%i, %i", col, m_NoteData.GetNumTracks()) );
|
|
|
|
m_vbFretIsDown[ col ] = !bRelease;
|
|
|
|
|
|
// Handle changing fret during a strum
|
|
if( m_pPlayerState->m_fLastStrumMusicSeconds != -1 )
|
|
{
|
|
LOG->Trace( "StrumTry" );
|
|
StepStrumHopo( col, row, tm, bHeld, bRelease, ButtonType_StrumFretsChanged );
|
|
}
|
|
|
|
// 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 + g_fTimingWindowHopo;
|
|
if( bDoHopo )
|
|
{
|
|
// do a Hopo:
|
|
// - on pressed fret is no higher fret is held
|
|
// - on next lowest held fret when the highest held fret is released
|
|
bool bHigherFretIsDown = false;
|
|
for( int i=col+1; i<m_NoteData.GetNumTracks(); i++ )
|
|
{
|
|
if( m_vbFretIsDown[i] )
|
|
{
|
|
bHigherFretIsDown = true;
|
|
break;
|
|
}
|
|
}
|
|
if( bHigherFretIsDown )
|
|
bDoHopo = false;
|
|
}
|
|
|
|
if( bDoHopo )
|
|
{
|
|
if( !bRelease )
|
|
{
|
|
// hammer-on
|
|
iHopoCol = col;
|
|
}
|
|
else
|
|
{
|
|
// pull-off
|
|
// find next lowest fret that is held
|
|
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 );
|
|
|
|
// Check if this fret breaks all active holds.
|
|
if( !bRelease )
|
|
{
|
|
const float fSongBeat = GAMESTATE->m_fSongBeat;
|
|
const int iSongRow = BeatToNoteRow( fSongBeat );
|
|
|
|
int iMaxHoldCol = -1;
|
|
int iNumColsHeld = 0;
|
|
|
|
// Score all active holds to NotHeld
|
|
for( int iTrack=0; iTrack<m_NoteData.GetNumTracks(); ++iTrack )
|
|
{
|
|
// 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::TrackMap::iterator begin, end;
|
|
m_NoteData.GetTapNoteRangeInclusive( iTrack, iStartCheckingAt, iSongRow+1, begin, end );
|
|
for( ; begin != end; ++begin )
|
|
{
|
|
TapNote &tn = begin->second;
|
|
if( tn.HoldResult.bActive )
|
|
{
|
|
iMaxHoldCol = iTrack;
|
|
iNumColsHeld++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Any frets to the right of an active hold will break the hold.
|
|
if( col > iMaxHoldCol || iNumColsHeld >= 2 )
|
|
ScoreAllActiveHoldsLetGo();
|
|
}
|
|
}
|
|
|
|
|
|
void Player::Strum( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease )
|
|
{
|
|
if( bRelease )
|
|
return;
|
|
|
|
if( m_pPlayerState->m_fLastStrumMusicSeconds != -1 )
|
|
{
|
|
DoStrumMiss();
|
|
}
|
|
|
|
m_pPlayerState->m_fLastStrumMusicSeconds = GAMESTATE->m_fMusicSeconds;
|
|
|
|
StepStrumHopo( col, row, tm, bHeld, bRelease, ButtonType_StrumFretsChanged );
|
|
}
|
|
|
|
void Player::DoTapScoreNone()
|
|
{
|
|
Message msg( "ScoreNone" );
|
|
MESSAGEMAN->Broadcast( msg );
|
|
|
|
const int iOldCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurCombo : 0;
|
|
const int iOldMissCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurMissCombo : 0;
|
|
|
|
/* The only real way to tell if a mine has been scored is if it has disappeared
|
|
* but this only works for hit mines so update the scores for avoided mines here. */
|
|
if( m_pPrimaryScoreKeeper )
|
|
m_pPrimaryScoreKeeper->HandleTapScoreNone();
|
|
if( m_pSecondaryScoreKeeper )
|
|
m_pSecondaryScoreKeeper->HandleTapScoreNone();
|
|
|
|
SendComboMessages( iOldCombo, iOldMissCombo );
|
|
|
|
|
|
if( m_pLifeMeter )
|
|
m_pLifeMeter->HandleTapScoreNone();
|
|
// TODO: Remove use of PlayerNumber
|
|
PlayerNumber pn = PLAYER_INVALID;
|
|
if( m_pCombinedLifeMeter )
|
|
m_pCombinedLifeMeter->HandleTapScoreNone( pn );
|
|
|
|
if( PENALIZE_TAP_SCORE_NONE )
|
|
{
|
|
SetJudgment( TNS_Miss, -1, 0 );
|
|
// the ScoreKeeper will subtract points later.
|
|
}
|
|
}
|
|
|
|
void Player::DoStrumMiss()
|
|
{
|
|
m_pPlayerState->m_fLastStrumMusicSeconds = -1;
|
|
DoTapScoreNone();
|
|
|
|
ScoreAllActiveHoldsLetGo();
|
|
}
|
|
|
|
void Player::ScoreAllActiveHoldsLetGo()
|
|
{
|
|
if( PENALIZE_TAP_SCORE_NONE )
|
|
{
|
|
const float fSongBeat = GAMESTATE->m_fSongBeat;
|
|
const int iSongRow = BeatToNoteRow( fSongBeat );
|
|
|
|
// Score all active holds to NotHeld
|
|
for( int iTrack=0; iTrack<m_NoteData.GetNumTracks(); ++iTrack )
|
|
{
|
|
// 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::TrackMap::iterator begin, end;
|
|
m_NoteData.GetTapNoteRangeInclusive( iTrack, iStartCheckingAt, iSongRow+1, begin, end );
|
|
for( ; begin != end; ++begin )
|
|
{
|
|
TapNote &tn = begin->second;
|
|
if( tn.HoldResult.bActive )
|
|
{
|
|
tn.HoldResult.hns = HNS_LetGo;
|
|
tn.HoldResult.fLife = 0;
|
|
|
|
HandleHoldScore( tn );
|
|
SetHoldJudgment( tn.result.tns, tn.HoldResult.hns, iTrack );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Player::StepStrumHopo( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease, Player::ButtonType pbt )
|
|
{
|
|
if( IsOniDead() )
|
|
return;
|
|
|
|
// Do everything that depends on a RageTimer here;
|
|
// set your breakpoints somewhere after this block.
|
|
const float fLastBeatUpdate = GAMESTATE->m_LastBeatUpdate.Ago();
|
|
const float fPositionSeconds = GAMESTATE->m_fMusicSeconds - tm.Ago();
|
|
const float fTimeSinceStep = tm.Ago();
|
|
|
|
switch( pbt )
|
|
{
|
|
DEFAULT_FAIL(pbt);
|
|
case ButtonType_Step:
|
|
break;
|
|
case ButtonType_StrumFretsChanged:
|
|
case ButtonType_Hopo:
|
|
// releasing should hit regular notes, not lifts
|
|
bRelease = false;
|
|
break;
|
|
}
|
|
|
|
const float fSongBeat = GAMESTATE->m_pCurSong ? GAMESTATE->m_pCurSong->GetBeatFromElapsedTime( fPositionSeconds ) : GAMESTATE->m_fSongBeat;
|
|
const int iSongRow = row == -1 ? BeatToNoteRow( fSongBeat ) : row;
|
|
|
|
if( col != -1 && !bRelease )
|
|
{
|
|
// 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 iStartCheckingAt = max( 0, iSongRow-BeatToNoteRow(1) );
|
|
NoteData::TrackMap::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;
|
|
|
|
switch( tn.subType )
|
|
{
|
|
DEFAULT_FAIL( tn.subType );
|
|
case TapNote::hold_head_hold:
|
|
continue;
|
|
case TapNote::hold_head_roll:
|
|
break;
|
|
}
|
|
|
|
const 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;
|
|
bool bInitiatedNote = true;
|
|
if( REQUIRE_STEP_ON_HOLD_HEADS )
|
|
bInitiatedNote = tns != TNS_None && tns != TNS_Miss; // did they step on the start?
|
|
const int iEndRow = iRow + tn.iDuration;
|
|
|
|
if( bInitiatedNote && 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. */
|
|
//LOG->Trace("setting iLastHeldRow to min of iSongRow (%i) and iEndRow (%i)",iSongRow,iEndRow);
|
|
tn.HoldResult.iLastHeldRow = min( iSongRow, iEndRow );
|
|
}
|
|
|
|
// If the song beat is in the range of this hold:
|
|
if( iRow <= iSongRow && iRow <= iEndRow )
|
|
{
|
|
if( bInitiatedNote )
|
|
{
|
|
// Increase life
|
|
tn.HoldResult.fLife = 1;
|
|
|
|
if( ROLL_BODY_INCREMENTS_COMBO )
|
|
{
|
|
// increment combo
|
|
const int iOldCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurCombo : 0;
|
|
const int iOldMissCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurMissCombo : 0;
|
|
|
|
if( m_pPlayerStageStats )
|
|
{
|
|
m_pPlayerStageStats->m_iCurCombo++;
|
|
m_pPlayerStageStats->m_iCurMissCombo = 0;
|
|
}
|
|
|
|
SendComboMessages( iOldCombo, iOldMissCombo );
|
|
if( m_pPlayerStageStats )
|
|
SetCombo( m_pPlayerStageStats->m_iCurCombo, m_pPlayerStageStats->m_iCurMissCombo );
|
|
|
|
bool bBright = m_pPlayerStageStats && m_pPlayerStageStats->m_iCurCombo>(int)BRIGHT_GHOST_COMBO_THRESHOLD;
|
|
if( m_pNoteField )
|
|
m_pNoteField->DidHoldNote( col, HNS_Held, bBright );
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Count calories for this step, unless we're being called because a button
|
|
// is held over a mine or being released.
|
|
// TODO: Move calorie counting into a ScoreKeeper?
|
|
if( m_pPlayerStageStats && m_pPlayerState && !bHeld && !bRelease )
|
|
{
|
|
// TODO: remove use of PlayerNumber
|
|
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
|
|
int iWeightPounds = 120;
|
|
Profile *pProfile = PROFILEMAN->GetProfile( pn );
|
|
if( pProfile )
|
|
iWeightPounds = pProfile->GetCalculatedWeightPounds();
|
|
|
|
int iNumTracksHeld = 0;
|
|
for( int t=0; t<m_NoteData.GetNumTracks(); t++ )
|
|
{
|
|
GameInput GameI = GAMESTATE->GetCurrentStyle()->StyleInputToGameInput( t, pn );
|
|
const float fSecsHeld = INPUTMAPPER->GetSecsHeld( GameI );
|
|
if( fSecsHeld > 0 && fSecsHeld < m_fTimingWindowJump )
|
|
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.023f, 0.077f );
|
|
float fCalsFor200Lbs = SCALE( iNumTracksHeld, 1, 2, 0.041f, 0.133f );
|
|
fCals = SCALE( pProfile->GetCalculatedWeightPounds(), 100.f, 200.f, fCalsFor100Lbs, fCalsFor200Lbs );
|
|
}
|
|
break;
|
|
}
|
|
|
|
m_pPlayerStageStats->m_fCaloriesBurned += fCals;
|
|
m_pPlayerStageStats->m_iNumControllerSteps ++;
|
|
}
|
|
|
|
// Check for step on a TapNote
|
|
/* XXX: This seems wrong. If a player steps twice quickly and two notes are
|
|
* close together in the same column then it is possible for the two notes
|
|
* to be graded out of order.
|
|
* Two possible fixes:
|
|
* 1. Adjust the fSongBeat (or the resulting note row) backward by
|
|
* iStepSearchRows and search forward two iStepSearchRows lengths,
|
|
* disallowing graded. This doesn't seem right because if a second note has
|
|
* passed, an earlier one should not be graded.
|
|
* 2. Clamp the distance searched backward to the previous row graded.
|
|
* Either option would fundamentally change the grading of two quick notes
|
|
* "jack hammers." Hmm.
|
|
*/
|
|
const int iStepSearchRows = max(
|
|
BeatToNoteRow( GAMESTATE->m_pCurSong->m_Timing.GetBeatFromElapsedTime( GAMESTATE->m_fMusicSeconds + StepSearchDistance ) ) - iSongRow,
|
|
iSongRow - BeatToNoteRow( GAMESTATE->m_pCurSong->m_Timing.GetBeatFromElapsedTime( GAMESTATE->m_fMusicSeconds - StepSearchDistance ) )
|
|
) + ROWS_PER_BEAT;
|
|
int iRowOfOverlappingNoteOrRow = row;
|
|
if( row == -1 )
|
|
{
|
|
switch( pbt )
|
|
{
|
|
DEFAULT_FAIL(pbt);
|
|
case ButtonType_StrumFretsChanged:
|
|
iRowOfOverlappingNoteOrRow = GetClosestNonEmptyRow( iSongRow, iStepSearchRows, iStepSearchRows, false );
|
|
break;
|
|
case ButtonType_Hopo:
|
|
case ButtonType_Step:
|
|
iRowOfOverlappingNoteOrRow = GetClosestNote( col, iSongRow, iStepSearchRows, iStepSearchRows, false );
|
|
break;
|
|
}
|
|
}
|
|
|
|
// calculate TapNoteScore
|
|
TapNoteScore score = TNS_None;
|
|
|
|
if( iRowOfOverlappingNoteOrRow != -1 )
|
|
{
|
|
// compute the score for this hit
|
|
float fNoteOffset = 0.0f;
|
|
// we need this later if we are autosyncing
|
|
const float fStepBeat = NoteRowToBeat( iRowOfOverlappingNoteOrRow );
|
|
const float fStepSeconds = GAMESTATE->m_pCurSong->GetElapsedTimeFromBeat(fStepBeat);
|
|
|
|
if( row == -1 )
|
|
{
|
|
// We actually stepped on the note this long ago:
|
|
//fTimeSinceStep
|
|
|
|
/* 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 + (fLastBeatUpdate*GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate);
|
|
|
|
// ... which means it happened at this point in the music:
|
|
const float fMusicSeconds = fCurrentMusicSeconds - fTimeSinceStep * GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate;
|
|
|
|
// The offset from the actual step in seconds:
|
|
fNoteOffset = (fStepSeconds - fMusicSeconds) / GAMESTATE->m_SongOptions.GetCurrent().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 );
|
|
*/
|
|
}
|
|
|
|
const float fSecondsFromExact = fabsf( fNoteOffset );
|
|
|
|
TapNote tnDummy = TAP_ORIGINAL_TAP;
|
|
TapNote *pTN = NULL;
|
|
switch( pbt )
|
|
{
|
|
DEFAULT_FAIL(pbt);
|
|
case ButtonType_StrumFretsChanged:
|
|
pTN = &tnDummy;
|
|
break;
|
|
case ButtonType_Hopo:
|
|
case ButtonType_Step:
|
|
NoteData::iterator iter = m_NoteData.FindTapNote( col, iRowOfOverlappingNoteOrRow );
|
|
DEBUG_ASSERT( iter!= m_NoteData.end(col) );
|
|
pTN = &iter->second;
|
|
break;
|
|
}
|
|
|
|
switch( m_pPlayerState->m_PlayerController )
|
|
{
|
|
case PC_HUMAN:
|
|
switch( pTN->type )
|
|
{
|
|
case TapNote::mine:
|
|
// Stepped too close to mine?
|
|
if( !bRelease && ( REQUIRE_STEP_ON_MINES == !bHeld ) &&
|
|
fSecondsFromExact <= GetWindowSeconds(TW_Mine) &&
|
|
!GAMESTATE->m_pCurSong->m_Timing.IsWarpAtRow(iSongRow) )
|
|
score = TNS_HitMine;
|
|
break;
|
|
|
|
case TapNote::attack:
|
|
if( !bRelease && fSecondsFromExact <= GetWindowSeconds(TW_Attack) && !pTN->result.bHidden )
|
|
score = TNS_W2; // sentinel
|
|
break;
|
|
|
|
case TapNote::hold_head:
|
|
// oh wow, this was causing the trigger before the hold heads
|
|
// bug. (It was fNoteOffset > 0.f before) -DaisuMaster
|
|
if( !REQUIRE_STEP_ON_HOLD_HEADS && ( fNoteOffset <= GetWindowSeconds( TW_W5 ) && GetWindowSeconds( TW_W5 ) != 0 ) )
|
|
{
|
|
score = TNS_W1;
|
|
break;
|
|
}
|
|
// Fall through to default.
|
|
default:
|
|
if( (pTN->type == TapNote::lift) == bRelease )
|
|
{
|
|
if( fSecondsFromExact <= GetWindowSeconds(TW_W1) ) score = TNS_W1;
|
|
else if( fSecondsFromExact <= GetWindowSeconds(TW_W2) ) score = TNS_W2;
|
|
else if( fSecondsFromExact <= GetWindowSeconds(TW_W3) ) score = TNS_W3;
|
|
else if( fSecondsFromExact <= GetWindowSeconds(TW_W4) ) score = TNS_W4;
|
|
else if( fSecondsFromExact <= GetWindowSeconds(TW_W5) ) score = TNS_W5;
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case PC_CPU:
|
|
case PC_AUTOPLAY:
|
|
score = PlayerAI::GetTapNoteScore( m_pPlayerState );
|
|
|
|
/* XXX: This doesn't make sense.
|
|
* Step should only be called in autoplay for hit notes. */
|
|
#if 0
|
|
// GetTapNoteScore always returns TNS_W1 in autoplay.
|
|
// If the step is far away, don't judge it.
|
|
if( m_pPlayerState->m_PlayerController == PC_AUTOPLAY &&
|
|
fSecondsFromExact > GetWindowSeconds(TW_W5) )
|
|
{
|
|
score = TNS_None;
|
|
break;
|
|
}
|
|
#endif
|
|
|
|
// TRICKY: We're asking the AI to judge mines. Consider TNS_W4 and
|
|
// below as "mine was hit" and everything else as "mine was avoided"
|
|
if( pTN->type == TapNote::mine )
|
|
{
|
|
// 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++ )
|
|
{
|
|
if( m_NoteData.GetTapNote(t,iRowOfOverlappingNoteOrRow).type == TapNote::mine ) // there's a mine to the left of us
|
|
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.
|
|
bool bTapsOnRow = m_NoteData.IsThereATapOrHoldHeadAtRow( iRowOfOverlappingNoteOrRow );
|
|
TapNoteScore get_to_avoid = bTapsOnRow ? TNS_W3 : TNS_W4;
|
|
|
|
if( score >= get_to_avoid )
|
|
return; // avoided
|
|
else
|
|
score = TNS_HitMine;
|
|
}
|
|
|
|
if( pTN->type == TapNote::attack && score > TNS_W4 )
|
|
score = TNS_W2; // 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
|
|
* UpdateTapNotesMissedOlderThan() 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. - Chris (StepMania r15628)
|
|
//fNoteOffset = randomf( -0.1f, 0.1f );
|
|
// Since themes may use the offset in a visual graph, the above
|
|
// behavior is not the best thing to do. Instead, random numbers
|
|
// should be generated based on the TapNoteScore, so that they can
|
|
// logically match up with the current timing windows. -aj
|
|
{
|
|
float fWindowW1 = GetWindowSeconds(TW_W1);
|
|
float fWindowW2 = GetWindowSeconds(TW_W2);
|
|
float fWindowW3 = GetWindowSeconds(TW_W3);
|
|
float fWindowW4 = GetWindowSeconds(TW_W4);
|
|
float fWindowW5 = GetWindowSeconds(TW_W5);
|
|
|
|
// W1 is the top judgment, there is no overlap.
|
|
if( score == TNS_W1 )
|
|
fNoteOffset = randomf(-fWindowW1, fWindowW1);
|
|
else
|
|
{
|
|
// figure out overlap.
|
|
float fLowerBound = 0.0f; // negative upper limit
|
|
float fUpperBound = 0.0f; // positive lower limit
|
|
float fCompareWindow = 0.0f; // filled in here:
|
|
if( score == TNS_W2 )
|
|
{
|
|
fLowerBound = -fWindowW1;
|
|
fUpperBound = fWindowW1;
|
|
fCompareWindow = fWindowW2;
|
|
}
|
|
else if( score == TNS_W3 )
|
|
{
|
|
fLowerBound = -fWindowW2;
|
|
fUpperBound = fWindowW2;
|
|
fCompareWindow = fWindowW3;
|
|
}
|
|
else if( score == TNS_W4 )
|
|
{
|
|
fLowerBound = -fWindowW3;
|
|
fUpperBound = fWindowW3;
|
|
fCompareWindow = fWindowW4;
|
|
}
|
|
else if( score == TNS_W5 )
|
|
{
|
|
fLowerBound = -fWindowW4;
|
|
fUpperBound = fWindowW4;
|
|
fCompareWindow = fWindowW5;
|
|
}
|
|
float f1 = randomf(-fCompareWindow, fLowerBound);
|
|
float f2 = randomf(fUpperBound, fCompareWindow);
|
|
|
|
if(randomf() * 100 >= 50)
|
|
fNoteOffset = f1;
|
|
else
|
|
fNoteOffset = f2;
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
/*
|
|
case PC_REPLAY:
|
|
// based on where we are, see what grade to get.
|
|
score = PlayerAI::GetTapNoteScore( m_pPlayerState );
|
|
// row is the current row, col is current column (track)
|
|
fNoteOffset = TapNoteOffset attribute
|
|
break;
|
|
*/
|
|
default:
|
|
ASSERT(0);
|
|
score = TNS_None;
|
|
break;
|
|
}
|
|
|
|
switch( pbt )
|
|
{
|
|
DEFAULT_FAIL(pbt);
|
|
case ButtonType_StrumFretsChanged:
|
|
{
|
|
bool bNoteRowMatchesFrets = true;
|
|
int iFirstNoteCol = -1;
|
|
for( int i=0; i<GAMESTATE->GetCurrentStyle()->m_iColsPerPlayer; i++ )
|
|
{
|
|
const TapNote &tn = m_NoteData.GetTapNote( i, iRowOfOverlappingNoteOrRow );
|
|
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
|
|
{
|
|
int iLastNoteCol = -1;
|
|
for( int i=GAMESTATE->GetCurrentStyle()->m_iColsPerPlayer-1; i>=0; i-- )
|
|
{
|
|
const TapNote &tn = m_NoteData.GetTapNote( i, iRowOfOverlappingNoteOrRow );
|
|
bool bIsNote = (tn.type != TapNote::empty);
|
|
if( bIsNote )
|
|
{
|
|
iLastNoteCol = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
m_pPlayerState->m_fLastHopoNoteMusicSeconds = fStepSeconds;
|
|
m_pPlayerState->m_iLastHopoNoteCol = iLastNoteCol;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
// con't hopo on the same note 2x in a row
|
|
if( col == m_pPlayerState->m_iLastHopoNoteCol )
|
|
{
|
|
bDidHopo = false;
|
|
goto done_checking_hopo;
|
|
}
|
|
|
|
const TapNote &tn = m_NoteData.GetTapNote( col, iRowOfOverlappingNoteOrRow );
|
|
ASSERT( tn.type != TapNote::empty );
|
|
|
|
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()) )
|
|
{
|
|
bDidHopo = false;
|
|
goto done_checking_hopo;
|
|
}
|
|
|
|
const TapNoteResult &lastTNR = NoteDataWithScoring::LastTapNoteWithResult( m_NoteData, iter.Row() ).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;
|
|
m_pPlayerState->m_iLastHopoNoteCol = col;
|
|
}
|
|
}
|
|
break;
|
|
case ButtonType_Step:
|
|
break;
|
|
}
|
|
|
|
// handle attack notes
|
|
if( pTN->type == TapNote::attack && score == TNS_W2 )
|
|
{
|
|
score = TNS_None; // don't score this as anything
|
|
|
|
m_soundAttackLaunch.Play();
|
|
|
|
// put attack in effect
|
|
Attack attack(
|
|
ATTACK_LEVEL_1,
|
|
-1, // now
|
|
pTN->fAttackDurationSeconds,
|
|
pTN->sAttackModifiers,
|
|
true,
|
|
false
|
|
);
|
|
|
|
// TODO: Remove use of PlayerNumber
|
|
PlayerNumber pnToAttack = OPPOSITE_PLAYER[m_pPlayerState->m_PlayerNumber];
|
|
PlayerState *pPlayerStateToAttack = GAMESTATE->m_pPlayerState[pnToAttack];
|
|
pPlayerStateToAttack->LaunchAttack( attack );
|
|
|
|
// remove all TapAttacks on this row
|
|
for( int t=0; t<m_NoteData.GetNumTracks(); t++ )
|
|
{
|
|
const TapNote &tn = m_NoteData.GetTapNote( t, iRowOfOverlappingNoteOrRow );
|
|
if( tn.type == TapNote::attack )
|
|
HideNote( t, iRowOfOverlappingNoteOrRow );
|
|
}
|
|
}
|
|
|
|
if( m_pPlayerState->m_PlayerController == PC_HUMAN && score >= TNS_W3 )
|
|
AdjustSync::HandleAutosync( fNoteOffset, fStepSeconds );
|
|
|
|
// Do game-specific and mode-specific score mapping.
|
|
score = GAMESTATE->GetCurrentGame()->MapTapNoteScore( score );
|
|
if( score == TNS_W1 && !GAMESTATE->ShowW1() )
|
|
score = TNS_W2;
|
|
|
|
|
|
if( score != TNS_None )
|
|
{
|
|
switch( pbt )
|
|
{
|
|
DEFAULT_FAIL(pbt);
|
|
case ButtonType_StrumFretsChanged:
|
|
for( int t=0; t<m_NoteData.GetNumTracks(); t++ )
|
|
{
|
|
TapNote tn = m_NoteData.GetTapNote( t, iRowOfOverlappingNoteOrRow );
|
|
if( tn.type != TapNote::empty )
|
|
{
|
|
tn.result.tns = score;
|
|
tn.result.fTapNoteOffset = -fNoteOffset;
|
|
m_NoteData.SetTapNote( t, iRowOfOverlappingNoteOrRow, tn );
|
|
}
|
|
}
|
|
break;
|
|
case ButtonType_Hopo:
|
|
case ButtonType_Step:
|
|
pTN->result.tns = score;
|
|
pTN->result.fTapNoteOffset = -fNoteOffset;
|
|
break;
|
|
}
|
|
}
|
|
|
|
m_LastTapNoteScore = score;
|
|
if( GAMESTATE->GetCurrentGame()->m_bCountNotesSeparately )
|
|
{
|
|
if( pTN->type != TapNote::mine )
|
|
{
|
|
const bool bBlind = (m_pPlayerState->m_PlayerOptions.GetCurrent().m_fBlind != 0);
|
|
// XXX: This is the wrong combo for shared players.
|
|
// STATSMAN->m_CurStageStats.m_Player[pn] might work, but could be wrong.
|
|
const bool bBright = ( m_pPlayerStageStats && m_pPlayerStageStats->m_iCurCombo > int(BRIGHT_GHOST_COMBO_THRESHOLD) ) || bBlind;
|
|
if( m_pNoteField )
|
|
m_pNoteField->DidTapNote( col, bBlind? TNS_W1:score, bBright );
|
|
if( score >= TNS_W3 || bBlind )
|
|
HideNote( col, iRowOfOverlappingNoteOrRow );
|
|
}
|
|
}
|
|
else if( NoteDataWithScoring::IsRowCompletelyJudged(m_NoteData, iRowOfOverlappingNoteOrRow) )
|
|
{
|
|
FlashGhostRow( iRowOfOverlappingNoteOrRow );
|
|
}
|
|
}
|
|
|
|
// check for hopo end
|
|
if( score <= TNS_Miss )
|
|
{
|
|
m_pPlayerState->ClearHopoState();
|
|
}
|
|
|
|
// check for strum end
|
|
if( score != TNS_None )
|
|
{
|
|
switch( pbt )
|
|
{
|
|
DEFAULT_FAIL(pbt);
|
|
case ButtonType_Step:
|
|
break;
|
|
case ButtonType_StrumFretsChanged:
|
|
m_pPlayerState->m_fLastStrumMusicSeconds = -1;
|
|
break;
|
|
case ButtonType_Hopo:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( score == TNS_None )
|
|
{
|
|
switch( pbt )
|
|
{
|
|
DEFAULT_FAIL(pbt);
|
|
case ButtonType_Step:
|
|
DoTapScoreNone();
|
|
break;
|
|
case ButtonType_StrumFretsChanged:
|
|
case ButtonType_Hopo:
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
if( !bRelease )
|
|
{
|
|
/* Search for keyed sounds separately. Play the nearest note. */
|
|
/* XXX: This isn't quite right. As per the above XXX for iRowOfOverlappingNote, if iRowOfOverlappingNote
|
|
* is set to a previous note, the keysound could have changed and this would cause the wrong one to play,
|
|
* in essence playing two sounds in the opposite order. Maybe this should always perform the search. Still,
|
|
* even that doesn't seem quite right since it would then play the same (new) keysound twice which would
|
|
* sound wrong even though the notes were judged as being correct, above. Fixing the above problem would
|
|
* fix this one as well. */
|
|
int iHeadRow;
|
|
if( iRowOfOverlappingNoteOrRow != -1 && score != TNS_None )
|
|
{
|
|
// just pressing a note, use that row.
|
|
// in other words, iRowOfOverlappingNoteOrRow = iRowOfOverlappingNoteOrRow
|
|
}
|
|
else if ( m_NoteData.IsHoldNoteAtRow( col, iSongRow, &iHeadRow ) )
|
|
{
|
|
// stepping on a hold, use it!
|
|
iRowOfOverlappingNoteOrRow = iHeadRow;
|
|
}
|
|
else
|
|
{
|
|
// or else find the closest note.
|
|
iRowOfOverlappingNoteOrRow = GetClosestNote( col, iSongRow, MAX_NOTE_ROW, MAX_NOTE_ROW, true );
|
|
}
|
|
if( iRowOfOverlappingNoteOrRow != -1 )
|
|
{
|
|
bool bShouldPlayNextKeysound = true;
|
|
|
|
if( bShouldPlayNextKeysound )
|
|
{
|
|
|
|
switch( pbt )
|
|
{
|
|
DEFAULT_FAIL(pbt);
|
|
case ButtonType_StrumFretsChanged:
|
|
for( int i=0; i<m_NoteData.GetNumTracks(); i++ )
|
|
{
|
|
const TapNote &tn = m_NoteData.GetTapNote( i, iRowOfOverlappingNoteOrRow );
|
|
if( tn.iKeysoundIndex >= 0 && tn.iKeysoundIndex < (int) m_vKeysounds.size() && (tn.type != TapNote::hold_head || (!tn.HoldResult.bActive || (tn.HoldResult.fOverlappedTime == 0 || (tn.HoldResult.hns == HNS_Held)))) )
|
|
{
|
|
m_vKeysounds[tn.iKeysoundIndex].Play();
|
|
m_vKeysounds[tn.iKeysoundIndex].SetProperty ("Volume", 1);
|
|
}
|
|
}
|
|
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() && (tn.type != TapNote::hold_head || (!tn.HoldResult.bActive || (tn.HoldResult.fOverlappedTime == 0 || (tn.HoldResult.hns == HNS_Held)))) )
|
|
{
|
|
m_vKeysounds[tn.iKeysoundIndex].Play();
|
|
m_vKeysounds[tn.iKeysoundIndex].SetProperty ("Volume", 1);
|
|
}
|
|
break;
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
// XXX:
|
|
if( !bRelease )
|
|
{
|
|
if( m_pNoteField )
|
|
{
|
|
switch( pbt )
|
|
{
|
|
DEFAULT_FAIL(pbt);
|
|
case ButtonType_StrumFretsChanged:
|
|
{
|
|
// only pulse the shortest string fret
|
|
int iLastFret = -1;
|
|
for( int i=0; i<m_NoteData.GetNumTracks(); i++ )
|
|
{
|
|
if( m_vbFretIsDown[i] )
|
|
iLastFret = i;
|
|
}
|
|
if( iLastFret != -1 )
|
|
m_pNoteField->Step( iLastFret, score );
|
|
}
|
|
break;
|
|
case ButtonType_Step:
|
|
m_pNoteField->Step( col, score );
|
|
break;
|
|
case ButtonType_Hopo:
|
|
// no animation
|
|
break;
|
|
}
|
|
}
|
|
Message msg( "Step" );
|
|
msg.SetParam( "PlayerNumber", m_pPlayerState->m_PlayerNumber );
|
|
msg.SetParam( "MultiPlayer", m_pPlayerState->m_mp );
|
|
MESSAGEMAN->Broadcast( msg );
|
|
}
|
|
}
|
|
|
|
void Player::UpdateTapNotesMissedOlderThan( float fMissIfOlderThanSeconds )
|
|
{
|
|
//LOG->Trace( "Steps::UpdateTapNotesMissedOlderThan(%f)", fMissIfOlderThanThisBeat );
|
|
int iMissIfOlderThanThisRow;
|
|
const float fEarliestTime = GAMESTATE->m_fMusicSeconds - fMissIfOlderThanSeconds;
|
|
{
|
|
bool bFreeze, bDelay;
|
|
float fMissIfOlderThanThisBeat;
|
|
float fThrowAway;
|
|
int iWarpBeginRow;
|
|
float fWarpLength;
|
|
GAMESTATE->m_pCurSong->m_Timing.GetBeatAndBPSFromElapsedTime( fEarliestTime, fMissIfOlderThanThisBeat, fThrowAway, bFreeze, bDelay, iWarpBeginRow, fWarpLength );
|
|
|
|
iMissIfOlderThanThisRow = BeatToNoteRow( fMissIfOlderThanThisBeat );
|
|
if( bFreeze || bDelay )
|
|
{
|
|
/* If there is a freeze on iMissIfOlderThanThisIndex, include this index too.
|
|
* Otherwise we won't show misses for tap notes on freezes until the
|
|
* freeze finishes. */
|
|
if( !bDelay )
|
|
iMissIfOlderThanThisRow++;
|
|
}
|
|
}
|
|
|
|
NoteData::all_tracks_iterator &iter = *m_pIterNeedsTapJudging;
|
|
|
|
for( ; !iter.IsAtEnd() && iter.Row() < iMissIfOlderThanThisRow; ++iter )
|
|
{
|
|
TapNote &tn = *iter;
|
|
|
|
if( !NeedsTapJudging(tn) )
|
|
continue;
|
|
|
|
// Ignore all notes that are skipped via WARPS.
|
|
if( GAMESTATE->m_pCurSong->m_Timing.IsWarpAtRow( iter.Row() ) )
|
|
continue;
|
|
|
|
if( tn.type == TapNote::mine )
|
|
{
|
|
tn.result.tns = TNS_AvoidMine;
|
|
|
|
/* The only real way to tell if a mine has been scored is if it has disappeared
|
|
* but this only works for hit mines so update the scores for avoided mines here. */
|
|
if( m_pPrimaryScoreKeeper )
|
|
m_pPrimaryScoreKeeper->HandleTapScore( tn );
|
|
if( m_pSecondaryScoreKeeper )
|
|
m_pSecondaryScoreKeeper->HandleTapScore( tn );
|
|
}
|
|
else
|
|
{
|
|
tn.result.tns = TNS_Miss;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Player::UpdateJudgedRows()
|
|
{
|
|
const int iEndRow = BeatToNoteRow( GAMESTATE->m_fSongBeat );
|
|
bool bAllJudged = true;
|
|
const bool bSeparately = GAMESTATE->GetCurrentGame()->m_bCountNotesSeparately;
|
|
|
|
{
|
|
NoteData::all_tracks_iterator iter = *m_pIterUnjudgedRows;
|
|
int iLastSeenRow = -1;
|
|
for( ; !iter.IsAtEnd() && iter.Row() <= iEndRow; ++iter )
|
|
{
|
|
int iRow = iter.Row();
|
|
|
|
// If row is within a warp section, ignore it. -aj
|
|
if( GAMESTATE->m_pCurSong->m_Timing.IsWarpAtRow(iRow) )
|
|
continue;
|
|
|
|
if( iLastSeenRow != iRow )
|
|
{
|
|
iLastSeenRow = iRow;
|
|
|
|
// crossed a nonempty row
|
|
if( !NoteDataWithScoring::IsRowCompletelyJudged(m_NoteData, iRow) )
|
|
{
|
|
bAllJudged = false;
|
|
continue;
|
|
}
|
|
if( bAllJudged )
|
|
*m_pIterUnjudgedRows = iter;
|
|
if( m_pJudgedRows->JudgeRow(iRow) )
|
|
continue;
|
|
const TapNoteResult &lastTNR = NoteDataWithScoring::LastTapNoteWithResult( m_NoteData, iRow ).result;
|
|
|
|
if( lastTNR.tns < TNS_Miss )
|
|
continue;
|
|
if( bSeparately )
|
|
{
|
|
for( int iTrack = 0; iTrack < m_NoteData.GetNumTracks(); ++iTrack )
|
|
{
|
|
const TapNote &tn = m_NoteData.GetTapNote( iTrack, iRow );
|
|
if( tn.type == TapNote::empty || tn.type == TapNote::mine ) continue;
|
|
SetJudgment( tn.result.tns, iTrack, tn.result.fTapNoteOffset );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SetJudgment( lastTNR.tns, m_NoteData.GetFirstTrackWithTapOrHoldHead(iRow), lastTNR.fTapNoteOffset );
|
|
}
|
|
HandleTapRowScore( iRow );
|
|
}
|
|
}
|
|
}
|
|
|
|
// handle mines.
|
|
{
|
|
bAllJudged = true;
|
|
set<RageSound *> setSounds;
|
|
NoteData::all_tracks_iterator iter = *m_pIterUnjudgedMineRows; // copy
|
|
int iLastSeenRow = -1;
|
|
for( ; !iter.IsAtEnd() && iter.Row() <= iEndRow; ++iter )
|
|
{
|
|
int iRow = iter.Row();
|
|
TapNote &tn = *iter;
|
|
|
|
if( iRow != iLastSeenRow )
|
|
{
|
|
iLastSeenRow = iRow;
|
|
if( bAllJudged )
|
|
*m_pIterUnjudgedMineRows = iter;
|
|
}
|
|
|
|
bool bMineNotHidden = tn.type == TapNote::mine && !tn.result.bHidden;
|
|
if( !bMineNotHidden )
|
|
continue;
|
|
|
|
switch( tn.result.tns )
|
|
{
|
|
DEFAULT_FAIL( tn.result.tns );
|
|
case TNS_None:
|
|
bAllJudged = false;
|
|
continue;
|
|
case TNS_AvoidMine:
|
|
continue;
|
|
case TNS_HitMine:
|
|
break;
|
|
}
|
|
if( m_pNoteField )
|
|
m_pNoteField->DidTapNote( iter.Track(), tn.result.tns, false );
|
|
|
|
if( tn.iKeysoundIndex >= 0 && tn.iKeysoundIndex < (int) m_vKeysounds.size() )
|
|
setSounds.insert( &m_vKeysounds[tn.iKeysoundIndex] );
|
|
else if( g_bEnableMineSoundPlayback )
|
|
setSounds.insert( &m_soundMine );
|
|
|
|
/* Attack Mines:
|
|
* Only difference is these launch an attack rather than affecting
|
|
* the lifebar. All the other mine impacts (score, dance points,
|
|
* etc.) are still applied. */
|
|
if( m_pPlayerState->m_PlayerOptions.GetCurrent().m_bTransforms[PlayerOptions::TRANSFORM_ATTACKMINES] )
|
|
{
|
|
const float fAttackRunTime = ATTACK_RUN_TIME_MINE;
|
|
|
|
Attack attMineAttack;
|
|
attMineAttack.sModifiers = ApplyRandomAttack();
|
|
attMineAttack.fStartSecond = -1.0f;
|
|
attMineAttack.fSecsRemaining = fAttackRunTime;
|
|
|
|
m_pPlayerState->LaunchAttack( attMineAttack );
|
|
}
|
|
else
|
|
ChangeLife( tn.result.tns );
|
|
|
|
if( m_pScoreDisplay )
|
|
m_pScoreDisplay->OnJudgment( tn.result.tns );
|
|
if( m_pSecondaryScoreDisplay )
|
|
m_pSecondaryScoreDisplay->OnJudgment( tn.result.tns );
|
|
|
|
// Make sure hit mines affect the dance points.
|
|
if( m_pPrimaryScoreKeeper )
|
|
m_pPrimaryScoreKeeper->HandleTapScore( tn );
|
|
if( m_pSecondaryScoreKeeper )
|
|
m_pSecondaryScoreKeeper->HandleTapScore( tn );
|
|
tn.result.bHidden = true;
|
|
}
|
|
|
|
FOREACHS( RageSound *, setSounds, s )
|
|
{
|
|
// Only play one copy of each mine sound at a time per player.
|
|
(*s)->Stop();
|
|
(*s)->Play();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Player::FlashGhostRow( int iRow )
|
|
{
|
|
TapNoteScore lastTNS = NoteDataWithScoring::LastTapNoteWithResult( m_NoteData, iRow ).result.tns;
|
|
const bool bBlind = (m_pPlayerState->m_PlayerOptions.GetCurrent().m_fBlind != 0);
|
|
const bool bBright = ( m_pPlayerStageStats && m_pPlayerStageStats->m_iCurCombo > int(BRIGHT_GHOST_COMBO_THRESHOLD) ) || bBlind;
|
|
|
|
for( int iTrack = 0; iTrack < m_NoteData.GetNumTracks(); ++iTrack )
|
|
{
|
|
const TapNote &tn = m_NoteData.GetTapNote( iTrack, iRow );
|
|
|
|
if( tn.type == TapNote::empty || tn.type == TapNote::mine || tn.type == TapNote::fake )
|
|
continue;
|
|
if( m_pNoteField )
|
|
m_pNoteField->DidTapNote( iTrack, lastTNS, bBright );
|
|
if( lastTNS >= TNS_W3 || bBlind )
|
|
HideNote( iTrack, iRow );
|
|
}
|
|
}
|
|
|
|
void Player::CrossedRows( int iLastRowCrossed, const RageTimer &now )
|
|
{
|
|
//LOG->Trace( "Player::CrossedRows %d %d", iFirstRowCrossed, iLastRowCrossed );
|
|
|
|
NoteData::all_tracks_iterator &iter = *m_pIterUncrossedRows;
|
|
int iLastSeenRow = -1;
|
|
for( ; !iter.IsAtEnd() && iter.Row() <= iLastRowCrossed; ++iter )
|
|
{
|
|
// Apply InitialHoldLife.
|
|
TapNote &tn = *iter;
|
|
int iRow = iter.Row();
|
|
int iTrack = iter.Track();
|
|
switch( tn.type )
|
|
{
|
|
case TapNote::hold_head:
|
|
tn.HoldResult.fLife = INITIAL_HOLD_LIFE;
|
|
if( !REQUIRE_STEP_ON_HOLD_HEADS )
|
|
{
|
|
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
|
|
GameInput GameI = GAMESTATE->GetCurrentStyle()->StyleInputToGameInput( iTrack, pn );
|
|
if( PREFSMAN->m_fPadStickSeconds > 0.f )
|
|
{
|
|
float fSecsHeld = INPUTMAPPER->GetSecsHeld( GameI, m_pPlayerState->m_mp );
|
|
if( fSecsHeld >= PREFSMAN->m_fPadStickSeconds )
|
|
Step( iTrack, -1, now - PREFSMAN->m_fPadStickSeconds, true, false );
|
|
}
|
|
else if( INPUTMAPPER->IsBeingPressed(GameI, m_pPlayerState->m_mp) )
|
|
{
|
|
Step( iTrack, -1, now, true, false );
|
|
}
|
|
}
|
|
break;
|
|
case TapNote::mine:
|
|
// Hold the panel while crossing a mine will cause the mine to explode
|
|
// TODO: Remove use of PlayerNumber.
|
|
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
|
|
GameInput GameI = GAMESTATE->GetCurrentStyle()->StyleInputToGameInput( iTrack, pn );
|
|
if( PREFSMAN->m_fPadStickSeconds > 0 )
|
|
{
|
|
float fSecsHeld = INPUTMAPPER->GetSecsHeld( GameI, m_pPlayerState->m_mp );
|
|
if( fSecsHeld >= PREFSMAN->m_fPadStickSeconds )
|
|
Step( iTrack, -1, now - PREFSMAN->m_fPadStickSeconds, true, false );
|
|
}
|
|
else
|
|
{
|
|
if( INPUTMAPPER->IsBeingPressed(GameI, m_pPlayerState->m_mp) )
|
|
Step( iTrack, iRow, now, true, false );
|
|
}
|
|
break;
|
|
}
|
|
|
|
if( iRow != iLastSeenRow )
|
|
{
|
|
// crossed a not-empty row
|
|
|
|
// If we're doing random vanish, randomise notes on the fly.
|
|
if( m_pPlayerState->m_PlayerOptions.GetCurrent().m_fAppearances[PlayerOptions::APPEARANCE_RANDOMVANISH]==1 )
|
|
RandomizeNotes( iRow );
|
|
|
|
// check to see if there's a note at the crossed row
|
|
if( m_pPlayerState->m_PlayerController != PC_HUMAN )
|
|
{
|
|
if( tn.type != TapNote::empty && tn.type != TapNote::fake && tn.result.tns == TNS_None )
|
|
{
|
|
Step( iTrack, iRow, now, false, false );
|
|
if( m_pPlayerState->m_PlayerController == PC_AUTOPLAY )
|
|
{
|
|
if( m_pPlayerStageStats )
|
|
m_pPlayerStageStats->m_bDisqualified = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Update hold checkpoints
|
|
if( HOLD_CHECKPOINTS )
|
|
{
|
|
int iCheckpointFrequencyRows = ROWS_PER_BEAT/2;
|
|
if( CHECKPOINTS_USE_TICKCOUNTS )
|
|
{
|
|
int tickCurrent = GAMESTATE->m_pCurSong->m_Timing.GetTickcountAtRow( iLastRowCrossed );
|
|
// There are some charts that don't want tickcounts involved at all.
|
|
iCheckpointFrequencyRows = (tickCurrent > 0 ? ROWS_PER_BEAT / tickCurrent : 0);
|
|
}
|
|
else if( CHECKPOINTS_USE_TIME_SIGNATURES )
|
|
{
|
|
TimeSignatureSegment tSignature = GAMESTATE->m_pCurSong->m_Timing.GetTimeSignatureSegmentAtBeat( NoteRowToBeat( iLastRowCrossed ) );
|
|
|
|
// Most songs are in 4/4 time. The frequency for checking tick counts should reflect that.
|
|
iCheckpointFrequencyRows = ROWS_PER_BEAT * tSignature.m_iDenominator / (tSignature.m_iNumerator * 4);
|
|
}
|
|
|
|
if( iCheckpointFrequencyRows > 0 )
|
|
{
|
|
// "the first row after the start of the range that lands on a beat"
|
|
int iFirstCheckpointInRange = ((m_iFirstUncrossedRow+iCheckpointFrequencyRows-1)/iCheckpointFrequencyRows) * iCheckpointFrequencyRows;
|
|
|
|
// "the last row or first row earlier that lands on a beat"
|
|
int iLastCheckpointInRange = ((iLastRowCrossed)/iCheckpointFrequencyRows) * iCheckpointFrequencyRows;
|
|
|
|
for( int r = iFirstCheckpointInRange; r <= iLastCheckpointInRange; r += iCheckpointFrequencyRows )
|
|
{
|
|
//LOG->Trace( "%d...", r );
|
|
vector<int> viColsWithHold;
|
|
int iNumHoldsHeldThisRow = 0;
|
|
int iNumHoldsMissedThisRow = 0;
|
|
|
|
// start at r-1 so that we consider holds whose end rows are equal to the checkpoint row
|
|
NoteData::all_tracks_iterator nIter = m_NoteData.GetTapNoteRangeAllTracks( r-1, r, true );
|
|
for( ; !nIter.IsAtEnd(); ++nIter )
|
|
{
|
|
TapNote &tn = *nIter;
|
|
if( tn.type != TapNote::hold_head )
|
|
continue;
|
|
|
|
int iStartRow = nIter.Row();
|
|
int iEndRow = iStartRow + tn.iDuration;
|
|
int iTrack = nIter.Track();
|
|
|
|
// "the first row after the hold head that lands on a beat"
|
|
int iFirstCheckpointOfHold = ((iStartRow+iCheckpointFrequencyRows)/iCheckpointFrequencyRows) * iCheckpointFrequencyRows;
|
|
|
|
// "the end row or the first earlier row that lands on a beat"
|
|
int iLastCheckpointOfHold = ((iEndRow)/iCheckpointFrequencyRows) * iCheckpointFrequencyRows;
|
|
|
|
// count the end of the hold as a checkpoint
|
|
bool bHoldOverlapsRow = iFirstCheckpointOfHold <= r && r <= iLastCheckpointOfHold;
|
|
if( !bHoldOverlapsRow )
|
|
continue;
|
|
|
|
viColsWithHold.push_back( iTrack );
|
|
if( tn.HoldResult.fLife > 0 )
|
|
{
|
|
++iNumHoldsHeldThisRow;
|
|
++tn.HoldResult.iCheckpointsHit;
|
|
}
|
|
else
|
|
{
|
|
++iNumHoldsMissedThisRow;
|
|
++tn.HoldResult.iCheckpointsMissed;
|
|
}
|
|
}
|
|
|
|
// TODO: Find a better way of handling hold checkpoints with other taps.
|
|
if( !viColsWithHold.empty() && ( CHECKPOINTS_TAPS_SEPARATE_JUDGMENT || m_NoteData.GetNumTapNotesInRow( iLastRowCrossed ) == 0 ) )
|
|
{
|
|
HandleHoldCheckpoint( r, iNumHoldsHeldThisRow, iNumHoldsMissedThisRow, viColsWithHold );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
m_iFirstUncrossedRow = iLastRowCrossed+1;
|
|
}
|
|
|
|
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.GetCurrent().m_fScrollSpeed );
|
|
|
|
int iNumOfTracks = m_NoteData.GetNumTracks();
|
|
for( int t=0; t+1 < iNumOfTracks; t++ )
|
|
{
|
|
const int iSwapWith = RandomInt( iNumOfTracks );
|
|
|
|
/* Only swap a tap and an empty. */
|
|
NoteData::iterator iter = m_NoteData.FindTapNote( t, iNewNoteRow );
|
|
if( iter == m_NoteData.end(t) || iter->second.type != TapNote::tap )
|
|
continue;
|
|
|
|
// Make sure this is empty.
|
|
if( m_NoteData.FindTapNote(iSwapWith, iNewNoteRow) != m_NoteData.end(iSwapWith) )
|
|
continue;
|
|
|
|
/* Make sure the destination row isn't in the middle of a hold. */
|
|
if( m_NoteData.IsHoldNoteAtRow(iSwapWith, iNoteRow) )
|
|
continue;
|
|
|
|
m_NoteData.SetTapNote( iSwapWith, iNewNoteRow, iter->second );
|
|
m_NoteData.RemoveTapNote( t, iter );
|
|
}
|
|
}
|
|
|
|
void Player::HandleTapRowScore( unsigned row )
|
|
{
|
|
bool bNoCheating = true;
|
|
#ifdef DEBUG
|
|
bNoCheating = false;
|
|
#endif
|
|
|
|
// Warp hackery. -aj
|
|
if( GAMESTATE->m_pCurSong->m_Timing.IsWarpAtRow( row ) )
|
|
return;
|
|
|
|
if( GAMESTATE->m_bDemonstrationOrJukebox )
|
|
bNoCheating = false;
|
|
// don't accumulate points if AutoPlay is on.
|
|
if( bNoCheating && m_pPlayerState->m_PlayerController == PC_AUTOPLAY )
|
|
return;
|
|
|
|
TapNoteScore scoreOfLastTap = NoteDataWithScoring::LastTapNoteWithResult(m_NoteData, row).result.tns;
|
|
const int iOldCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurCombo : 0;
|
|
const int iOldMissCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurMissCombo : 0;
|
|
|
|
if( scoreOfLastTap == TNS_Miss )
|
|
m_LastTapNoteScore = TNS_Miss;
|
|
|
|
for( int track = 0; track < m_NoteData.GetNumTracks(); ++track )
|
|
{
|
|
const TapNote &tn = m_NoteData.GetTapNote( track, row );
|
|
// Mines cannot be handled here.
|
|
if( tn.type == TapNote::empty || tn.type == TapNote::fake || tn.type == TapNote::mine )
|
|
continue;
|
|
if( m_pPrimaryScoreKeeper )
|
|
m_pPrimaryScoreKeeper->HandleTapScore( tn );
|
|
if( m_pSecondaryScoreKeeper )
|
|
m_pSecondaryScoreKeeper->HandleTapScore( tn );
|
|
}
|
|
|
|
if( m_pPrimaryScoreKeeper != NULL )
|
|
m_pPrimaryScoreKeeper->HandleTapRowScore( m_NoteData, row );
|
|
if( m_pSecondaryScoreKeeper != NULL )
|
|
m_pSecondaryScoreKeeper->HandleTapRowScore( m_NoteData, row );
|
|
|
|
const int iCurCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurCombo : 0;
|
|
const int iCurMissCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurMissCombo : 0;
|
|
|
|
SendComboMessages( iOldCombo, iOldMissCombo );
|
|
|
|
if( m_pPlayerStageStats )
|
|
{
|
|
SetCombo( iCurCombo, iCurMissCombo );
|
|
}
|
|
|
|
#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->m_iMaxCombo = max(m_pPlayerStageStats->m_iMaxCombo, iCurCombo);
|
|
|
|
/* Use the real current beat, not the beat we've been passed. That's because
|
|
* we 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 fStepsSeconds instead. */
|
|
if( m_pPlayerStageStats )
|
|
m_pPlayerStageStats->UpdateComboList( STATSMAN->m_CurStageStats.m_fStepsSeconds, false );
|
|
|
|
if( m_pScoreDisplay )
|
|
{
|
|
if( m_pPlayerStageStats )
|
|
m_pScoreDisplay->SetScore( m_pPlayerStageStats->m_iScore );
|
|
m_pScoreDisplay->OnJudgment( scoreOfLastTap );
|
|
}
|
|
if( m_pSecondaryScoreDisplay )
|
|
{
|
|
if( m_pPlayerStageStats )
|
|
m_pSecondaryScoreDisplay->SetScore( m_pPlayerStageStats->m_iScore );
|
|
m_pSecondaryScoreDisplay->OnJudgment( scoreOfLastTap );
|
|
}
|
|
|
|
ChangeLife( scoreOfLastTap );
|
|
}
|
|
|
|
void Player::HandleHoldCheckpoint( int iRow, int iNumHoldsHeldThisRow, int iNumHoldsMissedThisRow, const vector<int> &viColsWithHold )
|
|
{
|
|
bool bNoCheating = true;
|
|
#ifdef DEBUG
|
|
bNoCheating = false;
|
|
#endif
|
|
|
|
// More warp hackery. -aj
|
|
if( GAMESTATE->m_pCurSong->m_Timing.IsWarpAtRow( iRow ) )
|
|
return;
|
|
|
|
// don't accumulate combo if AutoPlay is on.
|
|
if( bNoCheating && m_pPlayerState->m_PlayerController == PC_AUTOPLAY )
|
|
return;
|
|
|
|
const int iOldCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurCombo : 0;
|
|
const int iOldMissCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurMissCombo : 0;
|
|
|
|
if( m_pPrimaryScoreKeeper )
|
|
m_pPrimaryScoreKeeper->HandleHoldCheckpointScore( m_NoteData, iRow, iNumHoldsHeldThisRow, iNumHoldsMissedThisRow );
|
|
if( m_pSecondaryScoreKeeper )
|
|
m_pSecondaryScoreKeeper->HandleHoldCheckpointScore( m_NoteData, iRow, iNumHoldsHeldThisRow, iNumHoldsMissedThisRow );
|
|
|
|
if( iNumHoldsMissedThisRow == 0 )
|
|
{
|
|
// added for http://ssc.ajworld.net/sm-ssc/bugtracker/view.php?id=16 -aj
|
|
if( CHECKPOINTS_FLASH_ON_HOLD )
|
|
{
|
|
FOREACH_CONST( int, viColsWithHold, i )
|
|
{
|
|
bool bBright = m_pPlayerStageStats && m_pPlayerStageStats->m_iCurCombo>(int)BRIGHT_GHOST_COMBO_THRESHOLD;
|
|
if( m_pNoteField )
|
|
m_pNoteField->DidHoldNote( *i, HNS_Held, bBright );
|
|
}
|
|
}
|
|
}
|
|
|
|
SendComboMessages( iOldCombo, iOldMissCombo );
|
|
|
|
if( m_pPlayerStageStats )
|
|
{
|
|
SetCombo( m_pPlayerStageStats->m_iCurCombo, m_pPlayerStageStats->m_iCurMissCombo );
|
|
m_pPlayerStageStats->UpdateComboList( STATSMAN->m_CurStageStats.m_fStepsSeconds, false );
|
|
}
|
|
|
|
ChangeLife( iNumHoldsMissedThisRow == 0? TNS_CheckpointHit:TNS_CheckpointMiss );
|
|
|
|
SetJudgment( iNumHoldsMissedThisRow == 0? TNS_CheckpointHit:TNS_CheckpointMiss, viColsWithHold[0], 0 );
|
|
}
|
|
|
|
void Player::HandleHoldScore( const TapNote &tn )
|
|
{
|
|
HoldNoteScore holdScore = tn.HoldResult.hns;
|
|
TapNoteScore tapScore = tn.result.tns;
|
|
bool bNoCheating = true;
|
|
#ifdef DEBUG
|
|
bNoCheating = false;
|
|
#endif
|
|
|
|
if( GAMESTATE->m_bDemonstrationOrJukebox )
|
|
bNoCheating = false;
|
|
// don't accumulate points if AutoPlay is on.
|
|
if( bNoCheating && m_pPlayerState->m_PlayerController == PC_AUTOPLAY )
|
|
return;
|
|
|
|
if( m_pPrimaryScoreKeeper )
|
|
m_pPrimaryScoreKeeper->HandleHoldScore( tn );
|
|
if( m_pSecondaryScoreKeeper )
|
|
m_pSecondaryScoreKeeper->HandleHoldScore( tn );
|
|
|
|
if( m_pScoreDisplay )
|
|
{
|
|
if( m_pPlayerStageStats )
|
|
m_pScoreDisplay->SetScore( m_pPlayerStageStats->m_iScore );
|
|
m_pScoreDisplay->OnJudgment( holdScore, tapScore );
|
|
}
|
|
if( m_pSecondaryScoreDisplay )
|
|
{
|
|
if( m_pPlayerStageStats )
|
|
m_pSecondaryScoreDisplay->SetScore( m_pPlayerStageStats->m_iScore );
|
|
m_pSecondaryScoreDisplay->OnJudgment( holdScore, tapScore );
|
|
}
|
|
|
|
ChangeLife( holdScore, tapScore );
|
|
}
|
|
|
|
float Player::GetMaxStepDistanceSeconds()
|
|
{
|
|
float fMax = 0;
|
|
fMax = max( fMax, GetWindowSeconds(TW_W5) );
|
|
fMax = max( fMax, GetWindowSeconds(TW_W4) );
|
|
fMax = max( fMax, GetWindowSeconds(TW_W3) );
|
|
fMax = max( fMax, GetWindowSeconds(TW_W2) );
|
|
fMax = max( fMax, GetWindowSeconds(TW_W1) );
|
|
float f = GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate * fMax;
|
|
return f + m_fMaxInputLatencySeconds;
|
|
}
|
|
|
|
void Player::FadeToFail()
|
|
{
|
|
if( m_pNoteField )
|
|
m_pNoteField->FadeToFail();
|
|
|
|
// clear miss combo
|
|
SetCombo( 0, 0 );
|
|
}
|
|
|
|
void Player::CacheAllUsedNoteSkins()
|
|
{
|
|
if( m_pNoteField )
|
|
m_pNoteField->CacheAllUsedNoteSkins();
|
|
}
|
|
|
|
void Player::SetJudgment( TapNoteScore tns, int iTrack, float fTapNoteOffset )
|
|
{
|
|
if( m_bSendJudgmentAndComboMessages )
|
|
{
|
|
Message msg("Judgment");
|
|
msg.SetParam( "Player", m_pPlayerState->m_PlayerNumber );
|
|
msg.SetParam( "MultiPlayer", m_pPlayerState->m_mp );
|
|
msg.SetParam( "FirstTrack", iTrack );
|
|
msg.SetParam( "TapNoteScore", tns );
|
|
msg.SetParam( "Early", fTapNoteOffset < 0.0f );
|
|
msg.SetParam( "TapNoteOffset", fTapNoteOffset );
|
|
MESSAGEMAN->Broadcast( msg );
|
|
}
|
|
}
|
|
|
|
void Player::SetHoldJudgment( TapNoteScore tns, HoldNoteScore hns, int iTrack )
|
|
{
|
|
ASSERT( iTrack < (int)m_vpHoldJudgment.size() );
|
|
if( m_vpHoldJudgment[iTrack] )
|
|
m_vpHoldJudgment[iTrack]->SetHoldJudgment( hns );
|
|
|
|
if( m_bSendJudgmentAndComboMessages )
|
|
{
|
|
Message msg("Judgment");
|
|
msg.SetParam( "Player", m_pPlayerState->m_PlayerNumber );
|
|
msg.SetParam( "MultiPlayer", m_pPlayerState->m_mp );
|
|
msg.SetParam( "FirstTrack", iTrack );
|
|
msg.SetParam( "TapNoteScore", tns );
|
|
msg.SetParam( "HoldNoteScore", hns );
|
|
MESSAGEMAN->Broadcast( msg );
|
|
}
|
|
}
|
|
|
|
void Player::SetCombo( int iCombo, int iMisses )
|
|
{
|
|
if( m_iLastSeenCombo == -1 ) // first update, don't set bIsMilestone=true
|
|
m_iLastSeenCombo = iCombo;
|
|
|
|
bool b25Milestone = false;
|
|
bool b50Milestone = false;
|
|
bool b100Milestone = false;
|
|
bool b250Milestone = false;
|
|
bool b1000Milestone = false;
|
|
for( int i=m_iLastSeenCombo+1; i<=iCombo; i++ )
|
|
{
|
|
if( i < 600 )
|
|
{
|
|
b25Milestone |= ((i % 25) == 0);
|
|
b50Milestone |= ((i % 50) == 0);
|
|
b100Milestone |= ((i % 100) == 0);
|
|
b250Milestone |= ((i % 250) == 0);
|
|
}
|
|
else
|
|
{
|
|
b1000Milestone |= ((i % 200) == 0);
|
|
}
|
|
}
|
|
m_iLastSeenCombo = iCombo;
|
|
|
|
if( b25Milestone )
|
|
this->PlayCommand( "TwentyFiveMilestone");
|
|
if( b50Milestone )
|
|
this->PlayCommand( "FiftyMilestone");
|
|
if( b100Milestone )
|
|
this->PlayCommand( "HundredMilestone" );
|
|
if( b250Milestone )
|
|
this->PlayCommand( "TwoHundredFiftyMilestone");
|
|
if( b1000Milestone )
|
|
this->PlayCommand( "ThousandMilestone" );
|
|
|
|
/* Colored combo logic differs between Songs and Courses.
|
|
* Songs:
|
|
* The theme decides how far into the song the combo color should appear.
|
|
* (PERCENT_UNTIL_COLOR_COMBO)
|
|
*
|
|
* Courses:
|
|
* PERCENT_UNTIL_COLOR_COMBO refers to how long through the course the
|
|
* combo color should appear (scaling to the number of songs). This may
|
|
* not be desired behavior, however. -aj
|
|
*
|
|
* TODO: Add a metric that determines Course combo colors logic?
|
|
* Or possibly move the logic to a Lua function? -aj */
|
|
bool bPastBeginning = false;
|
|
if( GAMESTATE->IsCourseMode() )
|
|
{
|
|
int iSongIndexStartColoring = GAMESTATE->m_pCurCourse->GetEstimatedNumStages();
|
|
iSongIndexStartColoring = static_cast<int>(floor(iSongIndexStartColoring*PERCENT_UNTIL_COLOR_COMBO));
|
|
bPastBeginning = GAMESTATE->GetCourseSongIndex() >= iSongIndexStartColoring;
|
|
}
|
|
else
|
|
{
|
|
bPastBeginning = GAMESTATE->m_fMusicSeconds > GAMESTATE->m_pCurSong->m_fMusicLengthSeconds * PERCENT_UNTIL_COLOR_COMBO;
|
|
}
|
|
|
|
if( m_bSendJudgmentAndComboMessages )
|
|
{
|
|
Message msg("Combo");
|
|
if( iCombo )
|
|
msg.SetParam( "Combo", iCombo );
|
|
if( iMisses )
|
|
msg.SetParam( "Misses", iMisses );
|
|
if( bPastBeginning && m_pPlayerStageStats->FullComboOfScore(TNS_W1) )
|
|
msg.SetParam( "FullComboW1", true );
|
|
if( bPastBeginning && m_pPlayerStageStats->FullComboOfScore(TNS_W2) )
|
|
msg.SetParam( "FullComboW2", true );
|
|
if( bPastBeginning && m_pPlayerStageStats->FullComboOfScore(TNS_W3) )
|
|
msg.SetParam( "FullComboW3", true );
|
|
if( bPastBeginning && m_pPlayerStageStats->FullComboOfScore(TNS_W4) )
|
|
msg.SetParam( "FullComboW4", true );
|
|
this->HandleMessage( msg );
|
|
}
|
|
}
|
|
|
|
RString Player::ApplyRandomAttack()
|
|
{
|
|
if( GAMESTATE->m_RandomAttacks.size() < 1 )
|
|
return "";
|
|
|
|
//int iAttackToUse = rand() % GAMESTATE->m_RandomAttacks.size();
|
|
DateTime now = DateTime::GetNowDate();
|
|
int iSeed = now.tm_hour * now.tm_min * now.tm_sec * now.tm_mday;
|
|
RandomGen rnd( GAMESTATE->m_iStageSeed * iSeed );
|
|
int iAttackToUse = rnd() % GAMESTATE->m_RandomAttacks.size();
|
|
return GAMESTATE->m_RandomAttacks[iAttackToUse];
|
|
}
|
|
|
|
// lua start
|
|
#include "LuaBinding.h"
|
|
|
|
/** @brief Allow Lua to have access to the Player. */
|
|
class LunaPlayer: public Luna<Player>
|
|
{
|
|
public:
|
|
static int SetActorWithJudgmentPosition( T* p, lua_State *L ) { Actor *pActor = Luna<Actor>::check(L, 1); p->SetActorWithJudgmentPosition(pActor); return 0; }
|
|
static int SetActorWithComboPosition( T* p, lua_State *L ) { Actor *pActor = Luna<Actor>::check(L, 1); p->SetActorWithComboPosition(pActor); return 0; }
|
|
|
|
LunaPlayer()
|
|
{
|
|
ADD_METHOD( SetActorWithJudgmentPosition );
|
|
ADD_METHOD( SetActorWithComboPosition );
|
|
}
|
|
};
|
|
|
|
LUA_REGISTER_DERIVED_CLASS( Player, ActorFrame )
|
|
// lua end
|
|
|
|
/*
|
|
* (c) 2001-2006 Chris Danford, Steve Checkoway
|
|
* 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.
|
|
*/
|