4de9bd7c7f
The song timing was used for the calculations, but the steps timing should be used instead because of split timing.
3477 lines
110 KiB
C++
3477 lines
110 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 "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"
|
|
|
|
RString ATTACK_DISPLAY_X_NAME( size_t p, size_t both_sides );
|
|
void TimingWindowSecondsInit( size_t /*TimingWindow*/ i, RString &sNameOut, float &defaultValueOut );
|
|
|
|
/**
|
|
* @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( static_cast<TimingWindow>(i) );
|
|
switch( i )
|
|
{
|
|
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: // same as great
|
|
defaultValueOut = 0.090f;
|
|
break;
|
|
case TW_Hold: // allow enough time to take foot off and put back on
|
|
defaultValueOut = 0.250f;
|
|
break;
|
|
case TW_Roll:
|
|
defaultValueOut = 0.500f;
|
|
break;
|
|
case TW_Attack:
|
|
defaultValueOut = 0.135f;
|
|
break;
|
|
case TW_Checkpoint: // similar to TW_Hold, but a little more strict/accurate to Pump play.
|
|
defaultValueOut = 0.1664f;
|
|
break;
|
|
default:
|
|
FAIL_M(ssprintf("Invalid timing window: %i", static_cast<int>(i)));
|
|
}
|
|
}
|
|
|
|
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 );
|
|
|
|
/** @brief How much life is in a hold note when you start on it? */
|
|
ThemeMetric<float> INITIAL_HOLD_LIFE ( "Player", "InitialHoldLife" );
|
|
/**
|
|
* @brief How much hold life is possible to have when holding a hold note?
|
|
*
|
|
* This was an sm-ssc addition. */
|
|
ThemeMetric<float> MAX_HOLD_LIFE ( "Player", "MaxHoldLife" );
|
|
ThemeMetric<bool> PENALIZE_TAP_SCORE_NONE ( "Player", "PenalizeTapScoreNone" );
|
|
ThemeMetric<bool> JUDGE_HOLD_NOTES_ON_SAME_ROW_TOGETHER ( "Player", "JudgeHoldNotesOnSameRowTogether" );
|
|
ThemeMetric<bool> CHECKPOINTS_FLASH_ON_HOLD ( "Player", "CheckpointsFlashOnHold" ); // sm-ssc addition
|
|
ThemeMetric<bool> IMMEDIATE_HOLD_LET_GO ( "Player", "ImmediateHoldLetGo" );
|
|
ThemeMetric<bool> COMBO_BREAK_ON_IMMEDIATE_HOLD_LET_GO ( "Player", "ComboBreakOnImmediateHoldLetGo" );
|
|
/**
|
|
* @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" );
|
|
/**
|
|
* @brief Does not stepping on a mine increase the combo?
|
|
*
|
|
* If set to true, every mine missed will increment the combo.
|
|
* If set to false, missing a mine will not affect the combo. */
|
|
ThemeMetric<bool> AVOID_MINE_INCREMENTS_COMBO ( "Gameplay", "AvoidMineIncrementsCombo" );
|
|
/**
|
|
* @brief Does stepping on a mine increment the miss combo?
|
|
*
|
|
* If set to true, every mine stepped on will break the combo and increment the miss combo.
|
|
* If set to false, stepping on a mine will not affect the combo. */
|
|
ThemeMetric<bool> MINE_HIT_INCREMENTS_MISS_COMBO ( "Gameplay", "MineHitIncrementsMissCombo" );
|
|
/**
|
|
* @brief Are checkpoints and taps considered separate judgments?
|
|
*
|
|
* If set to true, they are considered separate.
|
|
* If set to false, they are considered the same. */
|
|
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" );
|
|
|
|
/**
|
|
* @brief What is our highest cap for mMods?
|
|
*
|
|
* If set to 0 or less, assume the song takes over. */
|
|
ThemeMetric<float> M_MOD_HIGH_CAP("Player", "MModHighCap");
|
|
|
|
/** @brief Will battle modes have their steps mirrored or kept the same? */
|
|
ThemeMetric<bool> BATTLE_RAVE_MIRROR ( "Player", "BattleRaveMirror" );
|
|
|
|
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_drawing_notefield_board= false;
|
|
m_bLoaded = false;
|
|
m_inside_lua_set_life= false;
|
|
m_oitg_zoom_mode= false;
|
|
|
|
m_pPlayerState = nullptr;
|
|
m_pPlayerStageStats = nullptr;
|
|
m_fNoteFieldHeight = 0;
|
|
|
|
m_pLifeMeter = nullptr;
|
|
m_pCombinedLifeMeter = nullptr;
|
|
m_pScoreDisplay = nullptr;
|
|
m_pSecondaryScoreDisplay = nullptr;
|
|
m_pPrimaryScoreKeeper = nullptr;
|
|
m_pSecondaryScoreKeeper = nullptr;
|
|
m_pInventory = nullptr;
|
|
m_pIterNeedsTapJudging = nullptr;
|
|
m_pIterNeedsHoldJudging = nullptr;
|
|
m_pIterUncrossedRows = nullptr;
|
|
m_pIterUnjudgedRows = nullptr;
|
|
m_pIterUnjudgedMineRows = nullptr;
|
|
|
|
m_bPaused = false;
|
|
m_bDelay = false;
|
|
|
|
m_pAttackDisplay = nullptr;
|
|
if( bVisibleParts )
|
|
{
|
|
m_pAttackDisplay = new AttackDisplay;
|
|
this->AddChild( m_pAttackDisplay );
|
|
}
|
|
|
|
PlayerAI::InitFromDisk();
|
|
|
|
m_pNoteField = nullptr;
|
|
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(pPlayerState->m_PlayerNumber)->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 = 0;
|
|
m_bSeenComboYet = false;
|
|
|
|
// 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;
|
|
}
|
|
|
|
// calculate M-mod speed here, so we can adjust properly on a per-song basis.
|
|
// XXX: can we find a better location for this?
|
|
// Always calculate the reading bpm, to allow switching to an mmod mid-song.
|
|
{
|
|
DisplayBpms bpms;
|
|
|
|
if( GAMESTATE->IsCourseMode() )
|
|
{
|
|
ASSERT( GAMESTATE->m_pCurTrail[pn] != nullptr );
|
|
GAMESTATE->m_pCurTrail[pn]->GetDisplayBpms( bpms );
|
|
}
|
|
else
|
|
{
|
|
ASSERT( GAMESTATE->m_pCurSong != nullptr );
|
|
GAMESTATE->m_pCurSong->GetDisplayBpms( bpms );
|
|
}
|
|
|
|
float fMaxBPM = 0;
|
|
|
|
/* TODO: Find a way to not go above a certain BPM range
|
|
* for getting the max BPM. Otherwise, you get songs
|
|
* like Tsuhsuixamush, M550, 0.18x speed. Even slow
|
|
* speed readers would not generally find this fun.
|
|
* -Wolfman2000
|
|
*/
|
|
|
|
// all BPMs are listed and available, so try them first.
|
|
// get the maximum listed value for the song or course.
|
|
// if the BPMs are < 0, reset and get the actual values.
|
|
if( !bpms.IsSecret() )
|
|
{
|
|
fMaxBPM = (M_MOD_HIGH_CAP > 0 ?
|
|
bpms.GetMaxWithin(M_MOD_HIGH_CAP) :
|
|
bpms.GetMax());
|
|
fMaxBPM = max( 0, fMaxBPM );
|
|
}
|
|
|
|
// we can't rely on the displayed BPMs, so manually calculate.
|
|
if( fMaxBPM == 0 )
|
|
{
|
|
float fThrowAway = 0;
|
|
|
|
if( GAMESTATE->IsCourseMode() )
|
|
{
|
|
for (TrailEntry const &e : GAMESTATE->m_pCurTrail[pn]->m_vEntries)
|
|
{
|
|
float fMaxForEntry;
|
|
if (M_MOD_HIGH_CAP > 0)
|
|
e.pSong->m_SongTiming.GetActualBPM( fThrowAway, fMaxForEntry, M_MOD_HIGH_CAP );
|
|
else
|
|
e.pSong->m_SongTiming.GetActualBPM( fThrowAway, fMaxForEntry );
|
|
fMaxBPM = max( fMaxForEntry, fMaxBPM );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (M_MOD_HIGH_CAP > 0)
|
|
GAMESTATE->m_pCurSong->m_SongTiming.GetActualBPM( fThrowAway, fMaxBPM, M_MOD_HIGH_CAP );
|
|
else
|
|
GAMESTATE->m_pCurSong->m_SongTiming.GetActualBPM( fThrowAway, fMaxBPM );
|
|
}
|
|
}
|
|
|
|
ASSERT( fMaxBPM > 0 );
|
|
m_pPlayerState->m_fReadBPM= fMaxBPM;
|
|
}
|
|
|
|
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 = nullptr;
|
|
m_pActorWithJudgmentPosition = nullptr;
|
|
}
|
|
|
|
// Load HoldJudgments
|
|
m_vpHoldJudgment.resize( GAMESTATE->GetCurrentStyle(GetPlayerState()->m_PlayerNumber)->m_iColsPerPlayer );
|
|
for( int i = 0; i < GAMESTATE->GetCurrentStyle(GetPlayerState()->m_PlayerNumber)->m_iColsPerPlayer; ++i )
|
|
m_vpHoldJudgment[i] = nullptr;
|
|
|
|
if( HasVisibleParts() )
|
|
{
|
|
for( int i = 0; i < GAMESTATE->GetCurrentStyle(GetPlayerState()->m_PlayerNumber)->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 );
|
|
this->AddChild( m_pNoteField );
|
|
}
|
|
|
|
m_vbFretIsDown.resize( GAMESTATE->GetCurrentStyle(GetPlayerState()->m_PlayerNumber)->m_iColsPerPlayer );
|
|
std::fill_n(m_vbFretIsDown.begin(), m_vbFretIsDown.size(), 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 TapNoteType_Tap:
|
|
case TapNoteType_HoldHead:
|
|
case TapNoteType_Mine:
|
|
case TapNoteType_Lift:
|
|
return tn.result.tns == TNS_None;
|
|
case TapNoteType_HoldTail:
|
|
case TapNoteType_Attack:
|
|
case TapNoteType_AutoKeysound:
|
|
case TapNoteType_Fake:
|
|
case TapNoteType_Empty:
|
|
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 TapNoteType_HoldHead:
|
|
return tn.HoldResult.hns == HNS_None;
|
|
case TapNoteType_Tap:
|
|
case TapNoteType_HoldTail:
|
|
case TapNoteType_Mine:
|
|
case TapNoteType_Lift:
|
|
case TapNoteType_Attack:
|
|
case TapNoteType_AutoKeysound:
|
|
case TapNoteType_Fake:
|
|
case TapNoteType_Empty:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static void GenerateCacheDataStructure(PlayerState *pPlayerState, const NoteData ¬es) {
|
|
|
|
pPlayerState->m_CacheDisplayedBeat.clear();
|
|
|
|
const vector<TimingSegment*> vScrolls = pPlayerState->GetDisplayedTiming().GetTimingSegments( SEGMENT_SCROLL );
|
|
|
|
float displayedBeat = 0.0f;
|
|
float lastRealBeat = 0.0f;
|
|
float lastRatio = 1.0f;
|
|
for ( unsigned i = 0; i < vScrolls.size(); i++ )
|
|
{
|
|
ScrollSegment *seg = ToScroll( vScrolls[i] );
|
|
displayedBeat += ( seg->GetBeat() - lastRealBeat ) * lastRatio;
|
|
lastRealBeat = seg->GetBeat();
|
|
lastRatio = seg->GetRatio();
|
|
CacheDisplayedBeat c = { seg->GetBeat(), displayedBeat, seg->GetRatio() };
|
|
pPlayerState->m_CacheDisplayedBeat.push_back( c );
|
|
}
|
|
|
|
pPlayerState->m_CacheNoteStat.clear();
|
|
|
|
NoteData::all_tracks_const_iterator it = notes.GetTapNoteRangeAllTracks( 0, MAX_NOTE_ROW, true );
|
|
int count = 0, lastCount = 0;
|
|
for( ; !it.IsAtEnd(); ++it )
|
|
{
|
|
for( int t = 0; t < notes.GetNumTracks(); t++ )
|
|
{
|
|
if( notes.GetTapNote( t, it.Row() ) != TAP_EMPTY ) count ++;
|
|
}
|
|
CacheNoteStat c = { NoteRowToBeat(it.Row()), lastCount, count };
|
|
lastCount = count;
|
|
pPlayerState->m_CacheNoteStat.push_back(c);
|
|
}
|
|
|
|
}
|
|
|
|
void Player::Load()
|
|
{
|
|
m_bLoaded = true;
|
|
|
|
// Figured this is probably a little expensive so let's cache it
|
|
m_bTickHolds = GAMESTATE->GetCurrentGame()->m_bTickHolds;
|
|
|
|
m_LastTapNoteScore = TNS_None;
|
|
// The editor can start playing in the middle of the song.
|
|
const int iNoteRow = BeatToNoteRowNotRounded( m_pPlayerState->m_Position.m_fSongBeat );
|
|
m_iFirstUncrossedRow = iNoteRow - 1;
|
|
m_pJudgedRows->Reset( iNoteRow );
|
|
|
|
// TODO: Remove use of PlayerNumber.
|
|
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
|
|
|
|
bool bOniDead = m_pPlayerState->m_PlayerOptions.GetStage().m_LifeType == LifeType_Battery &&
|
|
(m_pPlayerStageStats == nullptr || 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 );
|
|
|
|
m_Timing = GAMESTATE->m_pCurSteps[pn]->GetTimingData();
|
|
|
|
/* Apply transforms. */
|
|
NoteDataUtil::TransformNoteData(m_NoteData, *m_Timing, m_pPlayerState->m_PlayerOptions.GetStage(), GAMESTATE->GetCurrentStyle(GetPlayerState()->m_PlayerNumber)->m_StepsType);
|
|
|
|
const Song* pSong = GAMESTATE->m_pCurSong;
|
|
|
|
// Generate some cache data structure.
|
|
GenerateCacheDataStructure(m_pPlayerState, 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(GetPlayerState()->m_PlayerNumber)->m_StepsType;
|
|
NoteDataUtil::TransformNoteData(m_NoteData, *m_Timing, m_pPlayerState->m_PlayerOptions.GetStage(), st);
|
|
|
|
if (BATTLE_RAVE_MIRROR)
|
|
{
|
|
// 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:
|
|
FAIL_M(ssprintf("Count %i not in range 0-3", count));
|
|
}
|
|
count++;
|
|
count %= 4;
|
|
}
|
|
break;
|
|
}
|
|
default: 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(GetPlayerState()->m_PlayerNumber)->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( unsigned int iOldCombo, unsigned int iOldMissCombo )
|
|
{
|
|
const unsigned int iCurCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurCombo : 0;
|
|
if( iOldCombo > (unsigned int)COMBO_STOPPED_AT && iCurCombo < (unsigned int)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== nullptr || IsOniDead() )
|
|
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 = m_pPlayerState->m_Position.m_fSongBeat;
|
|
const int iSongRow = BeatToNoteRow( fSongBeat );
|
|
|
|
ArrowEffects::SetCurrentOptions(&m_pPlayerState->m_PlayerOptions.GetCurrent());
|
|
|
|
// 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(false);
|
|
if( m_pPlayerState->m_bAttackEndedThisUpdate )
|
|
m_soundAttackEnding.Play(false);
|
|
}
|
|
|
|
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(GetPlayerState()->m_PlayerNumber)->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);
|
|
fX *= ( 1 - fMiniPercent * 0.5f );
|
|
|
|
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 != nullptr )
|
|
{
|
|
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 != nullptr )
|
|
{
|
|
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 field_zoom = 1 - fMiniPercent*0.5f;
|
|
if(m_pNoteField)
|
|
{
|
|
if(m_oitg_zoom_mode)
|
|
{
|
|
m_pNoteField->SetZoomX(field_zoom);
|
|
m_pNoteField->SetZoomY(field_zoom);
|
|
}
|
|
else
|
|
{
|
|
m_pNoteField->SetZoom(field_zoom);
|
|
}
|
|
}
|
|
if( m_pActorWithJudgmentPosition != nullptr )
|
|
m_pActorWithJudgmentPosition->SetZoom( m_pActorWithJudgmentPosition->GetZoom() * fJudgmentZoom );
|
|
if( m_pActorWithComboPosition != nullptr )
|
|
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;
|
|
|
|
// update pressed flag
|
|
const int iNumCols = GAMESTATE->GetCurrentStyle(GetPlayerState()->m_PlayerNumber)->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 != nullptr );
|
|
|
|
// TODO: Remove use of PlayerNumber.
|
|
vector<GameInput> GameI;
|
|
GAMESTATE->GetCurrentStyle(GetPlayerState()->m_PlayerNumber)->StyleInputToGameInput( col, m_pPlayerState->m_PlayerNumber, GameI );
|
|
|
|
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 != TapNoteType_HoldHead || tn.subType != TapNoteSubType_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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Track held misses
|
|
//
|
|
// In order to track held misses we have to check whether a note was
|
|
// held any time during the judgment window before it is judged a miss.
|
|
// Note: at this point we don't actually know yet whether a note will
|
|
// be a miss or a hit, so we have to track for all notes whether they
|
|
// were held at some point before getting judged.
|
|
{
|
|
float largestWindow = 0.0f;
|
|
const auto &disabledWindows = m_pPlayerState->m_PlayerOptions.GetCurrent().m_twDisabledWindows;
|
|
if (!disabledWindows[TW_W1])
|
|
largestWindow = max(largestWindow, GetWindowSeconds(TW_W1));
|
|
if (!disabledWindows[TW_W2])
|
|
largestWindow = max(largestWindow, GetWindowSeconds(TW_W2));
|
|
if (!disabledWindows[TW_W3])
|
|
largestWindow = max(largestWindow, GetWindowSeconds(TW_W3));
|
|
if (!disabledWindows[TW_W4])
|
|
largestWindow = max(largestWindow, GetWindowSeconds(TW_W4));
|
|
if (!disabledWindows[TW_W5])
|
|
largestWindow = max(largestWindow, GetWindowSeconds(TW_W5));
|
|
|
|
// We have to check the unjudged notes that are within the
|
|
// timing window. Let's find the cutoff point! (lastCheckRow)
|
|
const float rate = GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate;
|
|
const SongPosition songPosition = m_pPlayerState->m_Position;
|
|
const float musicPosition = songPosition.m_fMusicSeconds + (songPosition.m_LastBeatUpdate.Ago() * rate);
|
|
// We have to add 1 here, because GetBeatFromElapsedTime() can round down.
|
|
const int lastCheckRow = BeatToNoteRow(m_Timing->GetBeatFromElapsedTime(musicPosition + (largestWindow * rate)) + 1);
|
|
|
|
// The button being held only counts for the first unjudged
|
|
// note on a track (== column/arrow direction), so we have to
|
|
// keep track for which tracks we have already seen an unjudged
|
|
// note.
|
|
vector<bool> seenTracks(m_NoteData.GetNumTracks(), false);
|
|
|
|
for(auto iter = *m_pIterNeedsTapJudging; !iter.IsAtEnd() && iter.Row() <= lastCheckRow; ++iter)
|
|
{
|
|
TapNote &tn = *iter;
|
|
const int row = iter.Row();
|
|
const int track = iter.Track();
|
|
|
|
// Skip over warp and fake segments
|
|
if (!m_Timing->IsJudgableAtRow(row))
|
|
continue;
|
|
|
|
// Held misses only apply to tap notes
|
|
if (tn.type != TapNoteType_Tap && tn.type != TapNoteType_HoldHead)
|
|
continue;
|
|
|
|
const float notePosition = m_Timing->GetElapsedTimeFromBeat(NoteRowToBeat(row));
|
|
const float offset = fabsf((notePosition - musicPosition) / rate);
|
|
|
|
// Skip if we are outside of the largest timing window
|
|
if (offset > largestWindow)
|
|
continue;
|
|
|
|
// Skip the note if there is an earlier note on the same track that still awaits judgement
|
|
if (seenTracks[track])
|
|
continue;
|
|
|
|
seenTracks[track] = true;
|
|
|
|
if (!tn.result.bHeld)
|
|
{
|
|
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
|
|
vector<GameInput> input;
|
|
GAMESTATE->GetCurrentStyle(pn)->StyleInputToGameInput(track, pn, input);
|
|
|
|
tn.result.bHeld = INPUTMAPPER->IsBeingPressed(input, m_pPlayerState->m_mp);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 != TapNoteType_HoldHead )
|
|
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 TapNoteSubType_Hold:
|
|
break;
|
|
case TapNoteSubType_Roll:
|
|
{
|
|
vector<TrackRowTapNote> v;
|
|
v.push_back( trtn );
|
|
UpdateHoldNotes( iSongRow, fDeltaTime, v );
|
|
}
|
|
continue; // don't process this below
|
|
}
|
|
/*
|
|
case TapNoteSubType_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 float fPositionSeconds = m_pPlayerState->m_Position.m_fMusicSeconds - PREFSMAN->m_fPadStickSeconds;
|
|
const float fSongBeat = m_pPlayerState->GetDisplayedTiming().GetBeatFromElapsedTime( fPositionSeconds );
|
|
const int iRowNow = BeatToNoteRowNotRounded( fSongBeat );
|
|
|
|
if( iRowNow >= 0 )
|
|
{
|
|
if( GAMESTATE->IsPlayerEnabled(m_pPlayerState) )
|
|
{
|
|
if(m_pPlayerState->m_Position.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
|
|
if (!GAMESTATE->m_bInStepEditor)
|
|
{
|
|
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;
|
|
|
|
TapNoteSubType subType = TapNoteSubType_Invalid;
|
|
for (TrackRowTapNote const &trtn : vTN)
|
|
{
|
|
int iTrack = trtn.iTrack;
|
|
ASSERT( iStartRow == trtn.iRow );
|
|
TapNote &tn = *trtn.pTN;
|
|
int iEndRow = iStartRow + tn.iDuration;
|
|
if( subType == TapNoteSubType_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)) );
|
|
|
|
for (TrackRowTapNote const &trtn : vTN)
|
|
{
|
|
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;
|
|
for (TrackRowTapNote const &trtn : vTN)
|
|
{
|
|
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;
|
|
for (TrackRowTapNote const &trtn : vTN)
|
|
{
|
|
/*if this hold is already done, pretend it's always being pressed.
|
|
fixes/masks the phantom hold issue. -FSX*/
|
|
// That interacts badly with !IMMEDIATE_HOLD_LET_GO,
|
|
// causing ALL holds to be judged HNS_Held whether they were or not.
|
|
if( !IMMEDIATE_HOLD_LET_GO || (iStartRow + trtn.pTN->iDuration) > iSongRow )
|
|
{
|
|
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 != nullptr )
|
|
m_pPlayerStageStats->m_bDisqualified = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
vector<GameInput> GameI;
|
|
GAMESTATE->GetCurrentStyle(GetPlayerState()->m_PlayerNumber)->StyleInputToGameInput( iTrack, pn, GameI );
|
|
|
|
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. */
|
|
for (TrackRowTapNote const &trtn : vTN)
|
|
{
|
|
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));
|
|
tn.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 TapNoteSubType_Hold:
|
|
for (TrackRowTapNote const &trtn : vTN)
|
|
{
|
|
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");
|
|
*/
|
|
|
|
TimingWindow window = m_bTickHolds ? TW_Checkpoint : TW_Hold;
|
|
//LOG->Trace("fLife before minus: %f",fLife);
|
|
fLife -= fDeltaTime / GetWindowSeconds(window);
|
|
//LOG->Trace("fLife before clamp: %f",fLife);
|
|
fLife = max(0, fLife);
|
|
//LOG->Trace("fLife after: %f",fLife);
|
|
}
|
|
break;
|
|
case TapNoteSubType_Roll:
|
|
for (TrackRowTapNote const &trtn : vTN)
|
|
{
|
|
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 TapNoteSubType_Mine:
|
|
break;
|
|
*/
|
|
default:
|
|
FAIL_M(ssprintf("Invalid tap note subtype: %i", subType));
|
|
}
|
|
}
|
|
|
|
// 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
|
|
* m_bTickHolds 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 TapNoteSubType_Hold:
|
|
bAllowHoldCheckpoints = true;
|
|
break;
|
|
case TapNoteSubType_Roll:
|
|
bAllowHoldCheckpoints = false;
|
|
break;
|
|
/*
|
|
case TapNoteSubType_Mine:
|
|
bAllowHoldCheckpoints = true;
|
|
break;
|
|
*/
|
|
}
|
|
|
|
if( m_bTickHolds && bAllowHoldCheckpoints )
|
|
{
|
|
//LOG->Trace("(hold checkpoints are allowed and enabled.)");
|
|
int iCheckpointsHit = 0;
|
|
int iCheckpointsMissed = 0;
|
|
for (TrackRowTapNote const &v : vTN)
|
|
{
|
|
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>(unsigned int)BRIGHT_GHOST_COMBO_THRESHOLD;
|
|
if( m_pNoteField )
|
|
{
|
|
for (TrackRowTapNote const &trtn : vTN)
|
|
{
|
|
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_Missed;
|
|
}
|
|
}
|
|
|
|
float fLifeFraction = fLife / MAX_HOLD_LIFE;
|
|
|
|
for (TrackRowTapNote const &trtn : vTN)
|
|
{
|
|
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
|
|
// This can still crash. I think it expects a full game and quit before the preference works:
|
|
// otherwise, it causes problems on holds. At least, that hapened on my Mac. -wolfman2000
|
|
|
|
Preference<float> *pVolume = Preference<float>::GetPreferenceByName("SoundVolume");
|
|
if (pVolume != nullptr)
|
|
{
|
|
float fVol = pVolume->Get();
|
|
|
|
if( tn.iKeysoundIndex >= 0 && tn.iKeysoundIndex < (int) m_vKeysounds.size() )
|
|
{
|
|
float factor = (tn.subType == TapNoteSubType_Roll ? 2.0f * fLifeFraction : 10.0f * fLifeFraction - 8.5f);
|
|
m_vKeysounds[tn.iKeysoundIndex].SetProperty ("Volume", max(0.0f, min(1.0f, factor)) * fVol);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( (hns == HNS_LetGo) && COMBO_BREAK_ON_IMMEDIATE_HOLD_LET_GO )
|
|
IncrementMissCombo();
|
|
|
|
if( hns != HNS_None )
|
|
{
|
|
//LOG->Trace("tap note scoring time.");
|
|
TapNote &tn = *vTN[0].pTN;
|
|
SetHoldJudgment( tn, iFirstTrackWithMaxEndRow );
|
|
HandleHoldScore( tn );
|
|
//LOG->Trace("hold result = %s",StringConversion::ToString(tn.HoldResult.hns).c_str());
|
|
}
|
|
//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, *m_Timing, po, GAMESTATE->GetCurrentStyle(GetPlayerState()->m_PlayerNumber)->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(GetPlayerState()->m_PlayerNumber)->m_StyleType == StyleType_OnePlayerTwoSides &&
|
|
pn != GAMESTATE->GetMasterPlayerNumber() )
|
|
return;
|
|
|
|
bool draw_notefield= m_pNoteField && !IsOniDead();
|
|
|
|
const PlayerOptions& curr_options= m_pPlayerState->m_PlayerOptions.GetCurrent();
|
|
float tilt= curr_options.m_fPerspectiveTilt;
|
|
float skew= curr_options.m_fSkew;
|
|
float mini= curr_options.m_fEffects[PlayerOptions::EFFECT_MINI];
|
|
float center_y= GetY() + (GRAY_ARROWS_Y_STANDARD + GRAY_ARROWS_Y_REVERSE) / 2;
|
|
bool reverse= curr_options.GetReversePercentForColumn(0) > .5;
|
|
|
|
if(m_drawing_notefield_board)
|
|
{
|
|
// Ask the Notefield to draw its board primitive before everything else
|
|
// so that things drawn under the field aren't behind the opaque board.
|
|
// -Kyz
|
|
if(draw_notefield)
|
|
{
|
|
PlayerNoteFieldPositioner poser(this, GetX(), tilt, skew, mini, center_y, reverse);
|
|
m_pNoteField->DrawBoardPrimitive();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Draw these below everything else.
|
|
if( COMBO_UNDER_FIELD && curr_options.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();
|
|
|
|
if(draw_notefield)
|
|
{
|
|
PlayerNoteFieldPositioner poser(this, GetX(), tilt, skew, mini, center_y, reverse);
|
|
m_pNoteField->Draw();
|
|
}
|
|
|
|
// m_pNoteField->m_sprBoard->GetVisible()
|
|
if( !COMBO_UNDER_FIELD && curr_options.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::PushPlayerMatrix(float x, float skew, float center_y)
|
|
{
|
|
DISPLAY->CameraPushMatrix();
|
|
DISPLAY->PushMatrix();
|
|
DISPLAY->LoadMenuPerspective(45, SCREEN_WIDTH, SCREEN_HEIGHT,
|
|
SCALE(skew, 0.1f, 1.0f, x, SCREEN_CENTER_X), center_y);
|
|
}
|
|
|
|
void Player::PopPlayerMatrix()
|
|
{
|
|
DISPLAY->CameraPopMatrix();
|
|
DISPLAY->PopMatrix();
|
|
}
|
|
|
|
void Player::DrawNoteFieldBoard()
|
|
{
|
|
m_drawing_notefield_board= true;
|
|
Draw();
|
|
m_drawing_notefield_board= false;
|
|
}
|
|
|
|
Player::PlayerNoteFieldPositioner::PlayerNoteFieldPositioner(
|
|
Player* p, float x, float tilt, float skew, float mini, float center_y, bool reverse)
|
|
:player(p)
|
|
{
|
|
player->PushPlayerMatrix(x, skew, center_y);
|
|
float reverse_mult= (reverse ? -1 : 1);
|
|
original_y= player->m_pNoteField->GetY();
|
|
float tilt_degrees= SCALE(tilt, -1.f, +1.f, +30, -30) * reverse_mult;
|
|
float zoom= SCALE(mini, 0.f, 1.f, 1.f, .5f);
|
|
// Something strange going on here. Notice that the range for tilt's
|
|
// effect on y_offset goes to -45 when positive, but -20 when negative.
|
|
// I don't know why it's done this why, simply preserving old behavior.
|
|
// -Kyz
|
|
if(tilt > 0)
|
|
{
|
|
zoom*= SCALE(tilt, 0.f, 1.f, 1.f, 0.9f);
|
|
y_offset= SCALE(tilt, 0.f, 1.f, 0.f, -45.f) * reverse_mult;
|
|
}
|
|
else
|
|
{
|
|
zoom*= SCALE(tilt, 0.f, -1.f, 1.f, 0.9f);
|
|
y_offset= SCALE(tilt, 0.f, -1.f, 0.f, -20.f) * reverse_mult;
|
|
}
|
|
player->m_pNoteField->SetY(original_y + y_offset);
|
|
if(player->m_oitg_zoom_mode)
|
|
{
|
|
player->m_pNoteField->SetZoomX(zoom);
|
|
player->m_pNoteField->SetZoomY(zoom);
|
|
}
|
|
else
|
|
{
|
|
player->m_pNoteField->SetZoom(zoom);
|
|
}
|
|
player->m_pNoteField->SetRotationX(tilt_degrees);
|
|
}
|
|
|
|
Player::PlayerNoteFieldPositioner::~PlayerNoteFieldPositioner()
|
|
{
|
|
player->m_pNoteField->SetY(original_y);
|
|
player->PopPlayerMatrix();
|
|
}
|
|
|
|
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::ChangeLife(float delta)
|
|
{
|
|
// If ChangeLifeRecord is not called before the change, then the life graph
|
|
// will show a gradual change from the time of the previous step (or
|
|
// change) to the time of this change, instead of the sharp change that
|
|
// actually occurred. -Kyz
|
|
ChangeLifeRecord();
|
|
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
|
|
if(m_pLifeMeter)
|
|
{
|
|
m_pLifeMeter->ChangeLife(delta);
|
|
}
|
|
if(m_pCombinedLifeMeter)
|
|
{
|
|
m_pCombinedLifeMeter->ChangeLife(pn, delta);
|
|
}
|
|
ChangeLifeRecord();
|
|
}
|
|
|
|
void Player::SetLife(float value)
|
|
{
|
|
// If ChangeLifeRecord is not called before the change, then the life graph
|
|
// will show a gradual change from the time of the previous step (or
|
|
// change) to the time of this change, instead of the sharp change that
|
|
// actually occurred. -Kyz
|
|
ChangeLifeRecord();
|
|
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
|
|
if(m_pLifeMeter)
|
|
{
|
|
m_pLifeMeter->SetLife(value);
|
|
}
|
|
if(m_pCombinedLifeMeter)
|
|
{
|
|
m_pCombinedLifeMeter->SetLife(pn, value);
|
|
}
|
|
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 (!m_Timing->IsJudgableAtRow( begin->first ))
|
|
break;
|
|
// unsure if autoKeysounds should be excluded. -Wolfman2000
|
|
if( tn.type == TapNoteType_Empty || tn.type == TapNoteType_AutoKeysound )
|
|
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 (!m_Timing->IsJudgableAtRow(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;
|
|
|
|
// Get the current time, previous time, and next time.
|
|
float fNoteTime = m_pPlayerState->m_Position.m_fMusicSeconds;
|
|
float fNextTime = m_Timing->GetElapsedTimeFromBeat(NoteRowToBeat(iNextRow));
|
|
float fPrevTime = m_Timing->GetElapsedTimeFromBeat(NoteRowToBeat(iPrevRow));
|
|
|
|
/* Figure out which row is closer. */
|
|
if( fabsf(fNoteTime-fNextTime) > fabsf(fNoteTime-fPrevTime) )
|
|
return iPrevRow;
|
|
else
|
|
return iNextRow;
|
|
}
|
|
|
|
bool Player::IsOniDead() const
|
|
{
|
|
// If we're playing on oni and we've died, do nothing.
|
|
return m_pPlayerState->m_PlayerOptions.GetStage().m_LifeType == LifeType_Battery && m_pPlayerStageStats && m_pPlayerStageStats->m_bFailed;
|
|
}
|
|
|
|
void Player::DoTapScoreNone()
|
|
{
|
|
Message msg( "ScoreNone" );
|
|
MESSAGEMAN->Broadcast( msg );
|
|
|
|
const unsigned int iOldCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurCombo : 0;
|
|
const unsigned 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( BeatToNoteRow( m_pPlayerState->m_Position.m_fSongBeat ), -1, TAP_EMPTY, TNS_Miss, 0 );
|
|
// the ScoreKeeper will subtract points later.
|
|
}
|
|
}
|
|
|
|
void Player::ScoreAllActiveHoldsLetGo()
|
|
{
|
|
if( PENALIZE_TAP_SCORE_NONE )
|
|
{
|
|
const float fSongBeat = m_pPlayerState->m_Position.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;
|
|
|
|
SetHoldJudgment( tn, iTrack );
|
|
HandleHoldScore( tn );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Player::PlayKeysound( const TapNote &tn, TapNoteScore score )
|
|
{
|
|
// tap note must have keysound
|
|
if( tn.iKeysoundIndex >= 0 && tn.iKeysoundIndex < (int) m_vKeysounds.size() )
|
|
{
|
|
// handle a case for hold notes
|
|
if( tn.type == TapNoteType_HoldHead )
|
|
{
|
|
// if the hold is not already held
|
|
if( tn.HoldResult.hns == HNS_None )
|
|
{
|
|
// if the hold is already activated
|
|
TapNoteScore tns = tn.result.tns;
|
|
if( tns != TNS_None && tns != TNS_Miss && score == TNS_None )
|
|
{
|
|
// the sound must also be already playing
|
|
if( m_vKeysounds[tn.iKeysoundIndex].IsPlaying() )
|
|
{
|
|
// if all of these conditions are met, don't play the sound.
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
m_vKeysounds[tn.iKeysoundIndex].Play(false);
|
|
Preference<float> *pVolume = Preference<float>::GetPreferenceByName("SoundVolume");
|
|
float fVol = pVolume->Get();
|
|
m_vKeysounds[tn.iKeysoundIndex].SetProperty ("Volume", fVol);
|
|
}
|
|
}
|
|
|
|
void Player::Step( int col, int row, const RageTimer &tm, bool bHeld, bool bRelease )
|
|
{
|
|
if( IsOniDead() )
|
|
return;
|
|
|
|
// Do everything that depends on a RageTimer here;
|
|
// set your breakpoints somewhere after this block.
|
|
const float fLastBeatUpdate = m_pPlayerState->m_Position.m_LastBeatUpdate.Ago();
|
|
const float fPositionSeconds = m_pPlayerState->m_Position.m_fMusicSeconds - tm.Ago();
|
|
const float fTimeSinceStep = tm.Ago();
|
|
|
|
float fSongBeat = m_pPlayerState->m_Position.m_fSongBeat;
|
|
|
|
if( GAMESTATE->m_pCurSong )
|
|
{
|
|
fSongBeat = GAMESTATE->m_pCurSong->m_SongTiming.GetBeatFromElapsedTime( fPositionSeconds );
|
|
|
|
if( GAMESTATE->m_pCurSteps[m_pPlayerState->m_PlayerNumber] )
|
|
fSongBeat = m_Timing->GetBeatFromElapsedTime( fPositionSeconds );
|
|
}
|
|
|
|
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 != TapNoteType_HoldHead )
|
|
continue;
|
|
|
|
switch( tn.subType )
|
|
{
|
|
DEFAULT_FAIL( tn.subType );
|
|
case TapNoteSubType_Hold:
|
|
continue;
|
|
case TapNoteSubType_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 && m_pPlayerState->m_PlayerController != PC_AUTOPLAY )
|
|
{
|
|
IncrementCombo();
|
|
|
|
bool bBright = m_pPlayerStageStats && m_pPlayerStageStats->m_iCurCombo>(unsigned 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;
|
|
Profile *pProfile = PROFILEMAN->GetProfile( pn );
|
|
|
|
int iNumTracksHeld = 0;
|
|
for( int t=0; t<m_NoteData.GetNumTracks(); t++ )
|
|
{
|
|
vector<GameInput> GameI;
|
|
GAMESTATE->GetCurrentStyle(GetPlayerState()->m_PlayerNumber)->StyleInputToGameInput( t, pn, GameI );
|
|
float secs_held= 0.0f;
|
|
for(size_t i= 0; i < GameI.size(); ++i)
|
|
{
|
|
secs_held= max(secs_held, INPUTMAPPER->GetSecsHeld( GameI[i] ));
|
|
}
|
|
if( secs_held > 0 && secs_held < 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( m_Timing->GetBeatFromElapsedTime( m_pPlayerState->m_Position.m_fMusicSeconds + StepSearchDistance ) ) - iSongRow,
|
|
iSongRow - BeatToNoteRow( m_Timing->GetBeatFromElapsedTime( m_pPlayerState->m_Position.m_fMusicSeconds - StepSearchDistance ) )
|
|
) + ROWS_PER_BEAT;
|
|
int iRowOfOverlappingNoteOrRow = row;
|
|
if( row == -1 )
|
|
iRowOfOverlappingNoteOrRow = GetClosestNote( col, iSongRow, iStepSearchRows, iStepSearchRows, false );
|
|
|
|
// 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 = m_Timing->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 = m_pPlayerState->m_Position.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 = nullptr;
|
|
NoteData::iterator iter = m_NoteData.FindTapNote( col, iRowOfOverlappingNoteOrRow );
|
|
DEBUG_ASSERT( iter!= m_NoteData.end(col) );
|
|
pTN = &iter->second;
|
|
|
|
switch( m_pPlayerState->m_PlayerController )
|
|
{
|
|
case PC_HUMAN:
|
|
switch( pTN->type )
|
|
{
|
|
case TapNoteType_Mine:
|
|
// Stepped too close to mine?
|
|
if(!bRelease &&
|
|
(!REQUIRE_STEP_ON_MINES || REQUIRE_STEP_ON_MINES == !bHeld ) &&
|
|
fSecondsFromExact <= GetWindowSeconds(TW_Mine))
|
|
score = TNS_HitMine;
|
|
break;
|
|
case TapNoteType_Attack:
|
|
if( !bRelease && fSecondsFromExact <= GetWindowSeconds(TW_Attack) && !pTN->result.bHidden )
|
|
score = AllowW1() ? TNS_W1 : TNS_W2; // sentinel
|
|
break;
|
|
case TapNoteType_HoldHead:
|
|
// 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 ) )
|
|
{
|
|
// Set it to the first non-disabled window.
|
|
const auto &disabledWindows = m_pPlayerState->m_PlayerOptions.GetCurrent().m_twDisabledWindows;
|
|
if (!disabledWindows[TW_W1])
|
|
score = TNS_W1;
|
|
else if (!disabledWindows[TW_W2])
|
|
score = TNS_W2;
|
|
else if (!disabledWindows[TW_W3])
|
|
score = TNS_W3;
|
|
else if (!disabledWindows[TW_W4])
|
|
score = TNS_W4;
|
|
else if (!disabledWindows[TW_W5])
|
|
score = TNS_W5;
|
|
|
|
break;
|
|
}
|
|
// Fall through to default.
|
|
default:
|
|
if( (pTN->type == TapNoteType_Lift) == bRelease )
|
|
{
|
|
const auto &disabledWindows = m_pPlayerState->m_PlayerOptions.GetCurrent().m_twDisabledWindows;
|
|
if( fSecondsFromExact <= GetWindowSeconds(TW_W1) && !disabledWindows[TW_W1] ) score = TNS_W1;
|
|
else if( fSecondsFromExact <= GetWindowSeconds(TW_W2) && !disabledWindows[TW_W2] ) score = TNS_W2;
|
|
else if( fSecondsFromExact <= GetWindowSeconds(TW_W3) && !disabledWindows[TW_W3] ) score = TNS_W3;
|
|
else if( fSecondsFromExact <= GetWindowSeconds(TW_W4) && !disabledWindows[TW_W4] ) score = TNS_W4;
|
|
else if( fSecondsFromExact <= GetWindowSeconds(TW_W5) && !disabledWindows[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 == TapNoteType_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 == TapNoteType_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 == TapNoteType_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:
|
|
FAIL_M(ssprintf("Invalid player controller type: %i", m_pPlayerState->m_PlayerController));
|
|
}
|
|
|
|
// handle attack notes
|
|
if( pTN->type == TapNoteType_Attack && score == TNS_W2 )
|
|
{
|
|
score = TNS_None; // don't score this as anything
|
|
|
|
m_soundAttackLaunch.Play(false);
|
|
|
|
// 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 == TapNoteType_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 )
|
|
{
|
|
pTN->result.tns = score;
|
|
pTN->result.fTapNoteOffset = -fNoteOffset;
|
|
}
|
|
|
|
m_LastTapNoteScore = score;
|
|
if( GAMESTATE->GetCurrentGame()->m_bCountNotesSeparately )
|
|
{
|
|
if( pTN->type != TapNoteType_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 > (unsigned int)BRIGHT_GHOST_COMBO_THRESHOLD ) || bBlind;
|
|
if( m_pNoteField )
|
|
m_pNoteField->DidTapNote( col, bBlind? TNS_W1:score, bBright );
|
|
if( score >= m_pPlayerState->m_PlayerOptions.GetCurrent().m_MinTNSToHideNotes || bBlind )
|
|
HideNote( col, iRowOfOverlappingNoteOrRow );
|
|
}
|
|
}
|
|
else if( NoteDataWithScoring::IsRowCompletelyJudged(m_NoteData, iRowOfOverlappingNoteOrRow) )
|
|
{
|
|
FlashGhostRow( iRowOfOverlappingNoteOrRow );
|
|
}
|
|
}
|
|
|
|
if( score == TNS_None )
|
|
DoTapScoreNone();
|
|
|
|
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 )
|
|
{
|
|
const TapNote &tn = m_NoteData.GetTapNote( col, iRowOfOverlappingNoteOrRow );
|
|
PlayKeysound( tn, score );
|
|
}
|
|
}
|
|
// XXX:
|
|
if( !bRelease )
|
|
{
|
|
if( m_pNoteField )
|
|
{
|
|
m_pNoteField->Step( col, score );
|
|
}
|
|
Message msg( "Step" );
|
|
msg.SetParam( "PlayerNumber", m_pPlayerState->m_PlayerNumber );
|
|
msg.SetParam( "MultiPlayer", m_pPlayerState->m_mp );
|
|
msg.SetParam( "Column", col );
|
|
MESSAGEMAN->Broadcast( msg );
|
|
// Backwards compatibility
|
|
Message msg2( ssprintf("StepP%d", m_pPlayerState->m_PlayerNumber + 1) );
|
|
MESSAGEMAN->Broadcast( msg2 );
|
|
}
|
|
}
|
|
|
|
void Player::UpdateTapNotesMissedOlderThan( float fMissIfOlderThanSeconds )
|
|
{
|
|
//LOG->Trace( "Steps::UpdateTapNotesMissedOlderThan(%f)", fMissIfOlderThanThisBeat );
|
|
int iMissIfOlderThanThisRow;
|
|
const float fEarliestTime = m_pPlayerState->m_Position.m_fMusicSeconds - fMissIfOlderThanSeconds;
|
|
{
|
|
TimingData::GetBeatArgs beat_info;
|
|
beat_info.elapsed_time= fEarliestTime;
|
|
m_Timing->GetBeatAndBPSFromElapsedTime(beat_info);
|
|
|
|
iMissIfOlderThanThisRow = BeatToNoteRow(beat_info.beat);
|
|
if(beat_info.freeze_out || beat_info.delay_out )
|
|
{
|
|
/* 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(!beat_info.delay_out)
|
|
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 in WarpSegments or FakeSegments.
|
|
if (!m_Timing->IsJudgableAtRow(iter.Row()))
|
|
continue;
|
|
|
|
if( tn.type == TapNoteType_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()
|
|
{
|
|
// Look ahead far enough to catch any rows judged early.
|
|
const int iEndRow = BeatToNoteRow( m_Timing->GetBeatFromElapsedTime( m_pPlayerState->m_Position.m_fMusicSeconds + GetMaxStepDistanceSeconds() ) );
|
|
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();
|
|
|
|
// Do not judge arrows in WarpSegments or FakeSegments
|
|
if (!m_Timing->IsJudgableAtRow(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 == TapNoteType_Empty ||
|
|
tn.type == TapNoteType_Mine ||
|
|
tn.type == TapNoteType_AutoKeysound) continue;
|
|
SetJudgment( iRow, iTrack, tn );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SetJudgment( iRow, m_NoteData.GetFirstTrackWithTapOrHoldHead(iRow), NoteDataWithScoring::LastTapNoteWithResult( m_NoteData, iRow ) );
|
|
}
|
|
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();
|
|
|
|
// Do not worry about mines in WarpSegments or FakeSegments
|
|
if (!m_Timing->IsJudgableAtRow(iRow))
|
|
continue;
|
|
|
|
TapNote &tn = *iter;
|
|
|
|
if( iRow != iLastSeenRow )
|
|
{
|
|
iLastSeenRow = iRow;
|
|
if( bAllJudged )
|
|
*m_pIterUnjudgedMineRows = iter;
|
|
}
|
|
|
|
bool bMineNotHidden = tn.type == TapNoteType_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:
|
|
SetMineJudgment( tn.result.tns , iter.Track() );
|
|
tn.result.bHidden= true;
|
|
continue;
|
|
case TNS_HitMine:
|
|
SetMineJudgment( tn.result.tns , iter.Track() );
|
|
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 = ATTACK_STARTS_NOW;
|
|
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;
|
|
}
|
|
// If we hit the end of the loop, m_pIterUnjudgedMineRows needs to be
|
|
// updated. -Kyz
|
|
if((iter.IsAtEnd() || iLastSeenRow == iEndRow) && bAllJudged)
|
|
{
|
|
*m_pIterUnjudgedMineRows= iter;
|
|
}
|
|
|
|
for (RageSound *sound : setSounds)
|
|
{
|
|
// Only play one copy of each mine sound at a time per player.
|
|
sound->Stop();
|
|
sound->Play(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 > (unsigned 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 == TapNoteType_Empty || tn.type == TapNoteType_Mine ||
|
|
tn.type == TapNoteType_Fake || tn.result.bHidden)
|
|
{
|
|
continue;
|
|
}
|
|
if( m_pNoteField )
|
|
{
|
|
m_pNoteField->DidTapNote( iTrack, lastTNS, bBright );
|
|
}
|
|
if( lastTNS >= m_pPlayerState->m_PlayerOptions.GetCurrent().m_MinTNSToHideNotes || 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 TapNoteType_HoldHead:
|
|
{
|
|
tn.HoldResult.fLife = INITIAL_HOLD_LIFE;
|
|
if( !REQUIRE_STEP_ON_HOLD_HEADS )
|
|
{
|
|
PlayerNumber pn = m_pPlayerState->m_PlayerNumber;
|
|
vector<GameInput> GameI;
|
|
GAMESTATE->GetCurrentStyle(GetPlayerState()->m_PlayerNumber)->StyleInputToGameInput( iTrack, pn, GameI );
|
|
if( PREFSMAN->m_fPadStickSeconds > 0.f )
|
|
{
|
|
for(size_t i= 0; i < GameI.size(); ++i)
|
|
{
|
|
float fSecsHeld = INPUTMAPPER->GetSecsHeld(GameI[i], 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 TapNoteType_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;
|
|
vector<GameInput> GameI;
|
|
GAMESTATE->GetCurrentStyle(GetPlayerState()->m_PlayerNumber)->StyleInputToGameInput( iTrack, pn, GameI );
|
|
if( PREFSMAN->m_fPadStickSeconds > 0.0f )
|
|
{
|
|
for(size_t i= 0; i < GameI.size(); ++i)
|
|
{
|
|
float fSecsHeld = INPUTMAPPER->GetSecsHeld(GameI[i], 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;
|
|
}
|
|
default: break;
|
|
}
|
|
|
|
// check to see if there's a note at the crossed row
|
|
if( m_pPlayerState->m_PlayerController != PC_HUMAN )
|
|
{
|
|
if (tn.type != TapNoteType_Empty &&
|
|
tn.type != TapNoteType_Fake &&
|
|
tn.type != TapNoteType_AutoKeysound &&
|
|
tn.result.tns == TNS_None &&
|
|
this->m_Timing->IsJudgableAtRow(iRow) )
|
|
{
|
|
Step( iTrack, iRow, now, false, false );
|
|
if( m_pPlayerState->m_PlayerController == PC_AUTOPLAY )
|
|
{
|
|
if( m_pPlayerStageStats )
|
|
m_pPlayerStageStats->m_bDisqualified = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Can we remove the iLastSeenRow logic and the
|
|
// autokeysound for loop, since the iterator in this loop will
|
|
// already be iterating over all of the tracks?
|
|
if( iRow != iLastSeenRow )
|
|
{
|
|
// crossed a new not-empty row
|
|
iLastSeenRow = iRow;
|
|
|
|
// handle autokeysounds here (if not in the editor).
|
|
if (!GAMESTATE->m_bInStepEditor)
|
|
{
|
|
for (int t = 0; t < m_NoteData.GetNumTracks(); ++t)
|
|
{
|
|
const TapNote &tap = m_NoteData.GetTapNote(t, iRow);
|
|
if (tap.type == TapNoteType_AutoKeysound)
|
|
{
|
|
PlayKeysound(tap, TNS_None);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* Update hold checkpoints
|
|
*
|
|
* TODO: Move this to a separate function. */
|
|
if( m_bTickHolds && m_pPlayerState->m_PlayerController != PC_AUTOPLAY )
|
|
{
|
|
// Few rows typically cross per update. Easier to check all crossed rows
|
|
// than to calculate from timing segments.
|
|
for( int r = m_iFirstUncrossedRow; r <= iLastRowCrossed; ++r )
|
|
{
|
|
int tickCurrent = m_Timing->GetTickcountAtRow( r );
|
|
|
|
// There is a tick count at this row
|
|
if( tickCurrent > 0 && r % ( ROWS_PER_BEAT / tickCurrent ) == 0 )
|
|
{
|
|
|
|
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 != TapNoteType_HoldHead )
|
|
continue;
|
|
|
|
int iTrack = nIter.Track();
|
|
viColsWithHold.push_back( iTrack );
|
|
|
|
if( tn.HoldResult.fLife > 0 )
|
|
{
|
|
++iNumHoldsHeldThisRow;
|
|
++tn.HoldResult.iCheckpointsHit;
|
|
}
|
|
else
|
|
{
|
|
++iNumHoldsMissedThisRow;
|
|
++tn.HoldResult.iCheckpointsMissed;
|
|
}
|
|
}
|
|
GAMESTATE->SetProcessedTimingData(this->m_Timing);
|
|
|
|
// TODO: Find a better way of handling hold checkpoints with other taps.
|
|
if( !viColsWithHold.empty() && ( CHECKPOINTS_TAPS_SEPARATE_JUDGMENT || m_NoteData.GetNumTapNotesInRow( r ) == 0 ) )
|
|
{
|
|
HandleHoldCheckpoint(r,
|
|
iNumHoldsHeldThisRow,
|
|
iNumHoldsMissedThisRow,
|
|
viColsWithHold );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
m_iFirstUncrossedRow = iLastRowCrossed+1;
|
|
}
|
|
|
|
void Player::HandleTapRowScore( unsigned row )
|
|
{
|
|
bool bNoCheating = true;
|
|
#ifdef DEBUG
|
|
bNoCheating = false;
|
|
#endif
|
|
|
|
// Do not score rows in WarpSegments or FakeSegments
|
|
if (!m_Timing->IsJudgableAtRow(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 unsigned int iOldCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurCombo : 0;
|
|
const unsigned 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 == TapNoteType_Empty ||
|
|
tn.type == TapNoteType_Fake ||
|
|
tn.type == TapNoteType_Mine ||
|
|
tn.type == TapNoteType_AutoKeysound)
|
|
continue;
|
|
if( m_pPrimaryScoreKeeper )
|
|
m_pPrimaryScoreKeeper->HandleTapScore( tn );
|
|
if( m_pSecondaryScoreKeeper )
|
|
m_pSecondaryScoreKeeper->HandleTapScore( tn );
|
|
}
|
|
|
|
if( m_pPrimaryScoreKeeper != nullptr )
|
|
m_pPrimaryScoreKeeper->HandleTapRowScore( m_NoteData, row );
|
|
if( m_pSecondaryScoreKeeper != nullptr )
|
|
m_pSecondaryScoreKeeper->HandleTapRowScore( m_NoteData, row );
|
|
|
|
const unsigned int iCurCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurCombo : 0;
|
|
const unsigned 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
|
|
|
|
// WarpSegments and FakeSegments aren't judged in any way.
|
|
if (!m_Timing->IsJudgableAtRow(iRow))
|
|
return;
|
|
|
|
// don't accumulate combo if AutoPlay is on.
|
|
if( bNoCheating && m_pPlayerState->m_PlayerController == PC_AUTOPLAY )
|
|
return;
|
|
|
|
const unsigned int iOldCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurCombo : 0;
|
|
const unsigned 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 && m_pNoteField != nullptr)
|
|
{
|
|
for (int const &i : viColsWithHold)
|
|
{
|
|
bool bBright = m_pPlayerStageStats
|
|
&& m_pPlayerStageStats->m_iCurCombo>(unsigned 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( iRow, viColsWithHold[0], TAP_EMPTY, iNumHoldsMissedThisRow == 0? TNS_CheckpointHit:TNS_CheckpointMiss, 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) );
|
|
fMax = max( fMax, GetWindowSeconds(TW_Mine) );
|
|
fMax = max( fMax, GetWindowSeconds(TW_Hold) );
|
|
fMax = max( fMax, GetWindowSeconds(TW_Roll) );
|
|
fMax = max( fMax, GetWindowSeconds(TW_Attack) );
|
|
fMax = max( fMax, GetWindowSeconds(TW_Checkpoint) );
|
|
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::SetMineJudgment( TapNoteScore tns , int iTrack )
|
|
{
|
|
if( m_bSendJudgmentAndComboMessages )
|
|
{
|
|
Message msg("Judgment");
|
|
msg.SetParam( "Player", m_pPlayerState->m_PlayerNumber );
|
|
msg.SetParam( "TapNoteScore", tns );
|
|
msg.SetParam( "FirstTrack", iTrack );
|
|
MESSAGEMAN->Broadcast( msg );
|
|
if( m_pPlayerStageStats &&
|
|
( ( tns == TNS_AvoidMine && AVOID_MINE_INCREMENTS_COMBO ) ||
|
|
( tns == TNS_HitMine && MINE_HIT_INCREMENTS_MISS_COMBO ))
|
|
)
|
|
{
|
|
SetCombo( m_pPlayerStageStats->m_iCurCombo, m_pPlayerStageStats->m_iCurMissCombo );
|
|
}
|
|
}
|
|
}
|
|
|
|
void Player::SetJudgment( int iRow, int iTrack, const TapNote &tn, TapNoteScore tns, 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", tn.result.fTapNoteOffset );
|
|
|
|
if ( tns == TNS_Miss )
|
|
msg.SetParam( "HeldMiss", tn.result.bHeld );
|
|
|
|
Lua* L= LUA->Get();
|
|
lua_createtable( L, 0, m_NoteData.GetNumTracks() ); // TapNotes this row
|
|
lua_createtable( L, 0, m_NoteData.GetNumTracks() ); // HoldHeads of tracks held at this row.
|
|
|
|
for( int iTrack = 0; iTrack < m_NoteData.GetNumTracks(); ++iTrack )
|
|
{
|
|
NoteData::iterator tn = m_NoteData.FindTapNote(iTrack, iRow);
|
|
if( tn != m_NoteData.end(iTrack) )
|
|
{
|
|
tn->second.PushSelf(L);
|
|
lua_rawseti(L, -3, iTrack + 1);
|
|
}
|
|
else
|
|
{
|
|
int iHeadRow;
|
|
if( m_NoteData.IsHoldNoteAtRow( iTrack, iRow, &iHeadRow ) )
|
|
{
|
|
NoteData::iterator hold = m_NoteData.FindTapNote(iTrack, iHeadRow);
|
|
hold->second.PushSelf(L);
|
|
lua_rawseti(L, -2, iTrack + 1);
|
|
}
|
|
}
|
|
}
|
|
msg.SetParamFromStack( L, "Holds" );
|
|
msg.SetParamFromStack( L, "Notes" );
|
|
|
|
LUA->Release( L );
|
|
MESSAGEMAN->Broadcast( msg );
|
|
}
|
|
}
|
|
|
|
void Player::SetHoldJudgment( TapNote &tn, int iTrack )
|
|
{
|
|
ASSERT( iTrack < (int)m_vpHoldJudgment.size() );
|
|
if( m_vpHoldJudgment[iTrack] )
|
|
m_vpHoldJudgment[iTrack]->SetHoldJudgment( tn.HoldResult.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( "NumTracks", (int)m_vpHoldJudgment.size() );
|
|
msg.SetParam( "TapNoteScore", tn.result.tns );
|
|
msg.SetParam( "HoldNoteScore", tn.HoldResult.hns );
|
|
|
|
Lua* L = LUA->Get();
|
|
tn.PushSelf(L);
|
|
msg.SetParamFromStack( L, "TapNote" );
|
|
LUA->Release( L );
|
|
|
|
MESSAGEMAN->Broadcast( msg );
|
|
}
|
|
}
|
|
|
|
void Player::SetCombo( unsigned int iCombo, unsigned int iMisses )
|
|
{
|
|
if( !m_bSeenComboYet ) // first update, don't set bIsMilestone=true
|
|
{
|
|
m_bSeenComboYet = true;
|
|
m_iLastSeenCombo = iCombo;
|
|
}
|
|
|
|
bool b25Milestone = false;
|
|
bool b50Milestone = false;
|
|
bool b100Milestone = false;
|
|
bool b250Milestone = false;
|
|
bool b1000Milestone = false;
|
|
|
|
#define MILESTONE_CHECK(amount) ((iCombo / amount) > (m_iLastSeenCombo / amount))
|
|
if(m_iLastSeenCombo < 600)
|
|
{
|
|
b25Milestone= MILESTONE_CHECK(25);
|
|
b50Milestone= MILESTONE_CHECK(50);
|
|
b100Milestone= MILESTONE_CHECK(100);
|
|
b250Milestone= MILESTONE_CHECK(250);
|
|
b1000Milestone= MILESTONE_CHECK(1000);
|
|
}
|
|
else
|
|
{
|
|
b1000Milestone= MILESTONE_CHECK(1000);
|
|
}
|
|
#undef MILESTONE_CHECK
|
|
|
|
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 = m_pPlayerState->m_Position.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 );
|
|
}
|
|
}
|
|
|
|
void Player::IncrementComboOrMissCombo(bool bComboOrMissCombo)
|
|
{
|
|
const unsigned int iOldCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurCombo : 0;
|
|
const unsigned int iOldMissCombo = m_pPlayerStageStats ? m_pPlayerStageStats->m_iCurMissCombo : 0;
|
|
|
|
if( m_pPlayerStageStats )
|
|
{
|
|
if( bComboOrMissCombo )
|
|
{
|
|
m_pPlayerStageStats->m_iCurCombo++;
|
|
m_pPlayerStageStats->m_iCurMissCombo = 0;
|
|
}
|
|
else
|
|
{
|
|
m_pPlayerStageStats->m_iCurCombo = 0;
|
|
m_pPlayerStageStats->m_iCurMissCombo++;
|
|
}
|
|
SetCombo( m_pPlayerStageStats->m_iCurCombo, m_pPlayerStageStats->m_iCurMissCombo );
|
|
}
|
|
|
|
SendComboMessages( iOldCombo, iOldMissCombo );
|
|
}
|
|
|
|
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 SetLife(T* p, lua_State* L)
|
|
{
|
|
if(p->m_inside_lua_set_life)
|
|
{
|
|
luaL_error(L, "Do not call SetLife from inside LifeChangedMessageCommand because SetLife causes a LifeChangedMessageCommand.");
|
|
}
|
|
p->m_inside_lua_set_life= true;
|
|
p->SetLife(FArg(1));
|
|
p->m_inside_lua_set_life= false;
|
|
COMMON_RETURN_SELF;
|
|
}
|
|
static int ChangeLife(T* p, lua_State* L)
|
|
{
|
|
if(p->m_inside_lua_set_life)
|
|
{
|
|
luaL_error(L, "Do not call ChangeLife from inside LifeChangedMessageCommand because ChangeLife causes a LifeChangedMessageCommand.");
|
|
}
|
|
p->m_inside_lua_set_life= true;
|
|
p->ChangeLife(FArg(1));
|
|
p->m_inside_lua_set_life= false;
|
|
COMMON_RETURN_SELF;
|
|
}
|
|
static int SetActorWithJudgmentPosition( T* p, lua_State *L )
|
|
{
|
|
Actor *pActor = Luna<Actor>::check(L, 1);
|
|
p->SetActorWithJudgmentPosition(pActor);
|
|
COMMON_RETURN_SELF;
|
|
}
|
|
static int SetActorWithComboPosition( T* p, lua_State *L )
|
|
{
|
|
Actor *pActor = Luna<Actor>::check(L, 1);
|
|
p->SetActorWithComboPosition(pActor);
|
|
COMMON_RETURN_SELF;
|
|
}
|
|
static int GetPlayerTimingData( T* p, lua_State *L )
|
|
{
|
|
p->GetPlayerTimingData().PushSelf(L);
|
|
return 1;
|
|
}
|
|
GET_SET_BOOL_METHOD(oitg_zoom_mode, m_oitg_zoom_mode);
|
|
|
|
LunaPlayer()
|
|
{
|
|
ADD_METHOD(SetLife);
|
|
ADD_METHOD(ChangeLife);
|
|
ADD_METHOD( SetActorWithJudgmentPosition );
|
|
ADD_METHOD( SetActorWithComboPosition );
|
|
ADD_METHOD( GetPlayerTimingData );
|
|
ADD_GET_SET_METHODS(oitg_zoom_mode);
|
|
}
|
|
};
|
|
|
|
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.
|
|
*/
|