#include "global.h" #include "WheelBase.h" #include "RageUtil.h" #include "GameManager.h" #include "PrefsManager.h" #include "ScreenManager.h" // for sending SM_PlayMusicSample #include "RageLog.h" #include "GameConstantsAndTypes.h" #include "GameState.h" #include "RageMath.h" #include "ThemeManager.h" #include "RageTextureManager.h" #include "GameCommand.h" #include "ActorUtil.h" #include "Foreach.h" #include "Style.h" #include "ThemeMetric.h" #include "ScreenDimensions.h" const int MAX_WHEEL_SOUND_SPEED = 15; WheelBase::WheelBase() { } WheelBase::~WheelBase() { FOREACH( WheelItemBase*, m_WheelBaseItems, i ) SAFE_DELETE( *i ); m_WheelBaseItems.clear(); FOREACH( WheelItemBaseData*, m_WheelBaseItemsData, i ) SAFE_DELETE( *i ); m_WheelBaseItems.clear(); m_WheelBaseItemsData.clear(); m_LastSelection = NULL; } void WheelBase::Load( CString sType ) { LOG->Trace( "WheelBase::Load('%s')", sType.c_str() ); LoadFromMetrics( sType ); LoadVariables(); FOREACH( WheelItemBase*, m_WheelBaseItems, i ) SAFE_DELETE( *i ); m_WheelBaseItems.clear(); for( int i=0; iGetPathS(sType,"change"), true ); m_soundLocked.Load( THEME->GetPathS(sType,"locked"), true ); m_sprHighlight.Load( THEME->GetPathG(sType,"highlight") ); m_sprHighlight->SetName( "Highlight" ); this->AddChild( m_sprHighlight ); ActorUtil::OnCommand( m_sprHighlight, sType ); m_ScrollBar.SetX( SCROLL_BAR_X ); m_ScrollBar.SetBarHeight( SCROLL_BAR_HEIGHT ); this->AddChild( &m_ScrollBar ); } void WheelBase::LoadVariables() { m_isEmpty = true; m_LastSelection = NULL; m_iSelection = 0; m_fTimeLeftInState = 0; m_fPositionOffsetFromSelection = 0; m_iSwitchesLeftInSpinDown = 0; m_Moving = 0; } void WheelBase::GetItemPosition( float fPosOffsetsFromMiddle, float& fX_out, float& fY_out, float& fZ_out, float& fRotationX_out ) { if( USE_3D ) { const float curve = CIRCLE_PERCENT*2*PI; fRotationX_out = SCALE(fPosOffsetsFromMiddle,-NUM_WHEEL_ITEMS/2.0f,+NUM_WHEEL_ITEMS/2.0f,-curve/2.f,+curve/2.f); fX_out = (1-cosf(fPosOffsetsFromMiddle/PI))*ITEM_CURVE_X; fY_out = WHEEL_3D_RADIUS*sinf(fRotationX_out); fZ_out = -100+WHEEL_3D_RADIUS*cosf(fRotationX_out); fRotationX_out *= 180.f/PI; // to degrees // printf( "fRotationX_out = %f\n", fRotationX_out ); } else if(!USE_LINEAR_WHEEL.GetValue()) { fX_out = (1-cosf(fPosOffsetsFromMiddle/PI))*ITEM_CURVE_X; fY_out = fPosOffsetsFromMiddle*ITEM_SPACING_Y; fZ_out = 0; fRotationX_out = 0; fX_out = roundf( fX_out ); fY_out = roundf( fY_out ); fZ_out = roundf( fZ_out ); } else { fX_out = fPosOffsetsFromMiddle*ITEM_CURVE_X; fY_out = fPosOffsetsFromMiddle*ITEM_SPACING_Y; fZ_out = 0; fRotationX_out = 0; fX_out = roundf( fX_out ); fY_out = roundf( fY_out ); fZ_out = roundf( fZ_out ); } } void WheelBase::SetItemPosition( Actor &item, float fPosOffsetsFromMiddle ) { float fX, fY, fZ, fRotationX; GetItemPosition( fPosOffsetsFromMiddle, fX, fY, fZ, fRotationX ); item.SetXY( fX, fY ); item.SetZ( fZ ); item.SetRotationX( fRotationX ); } void WheelBase::DrawPrimitives() { if( USE_3D ) { // DISPLAY->PushMatrix(); // DISPLAY->EnterPerspective(45, false); //Not needed for 3D // construct view and project matrix // RageVector3 Up( 0.0f, 1.0f, 0.0f ); // RageVector3 Eye( SCREEN_CENTER_X, SCREEN_CENTER_Y, 550 ); // RageVector3 At( SCREEN_CENTER_X, SCREEN_CENTER_Y, 0 ); // DISPLAY->LoadLookAt(60, Eye, At, Up); //Changed from DISPLAY->LookAt(Eye, At, Up) } // draw outside->inside for( int i=0; i=NUM_WHEEL_ITEMS/2; i-- ) DrawItem( i ); ActorFrame::DrawPrimitives(); if( USE_3D ) { // DISPLAY->ExitPerspective(); //Not needed? // DISPLAY->PopMatrix(); } } void WheelBase::DrawItem( int i, WheelItemBase *display, const float fThisBannerPositionOffsetFromSelection) { if( fabsf(fThisBannerPositionOffsetFromSelection) > NUM_WHEEL_ITEMS_TO_DRAW/2 ) return; switch( m_WheelState ) { case STATE_SELECTING_GENERIC: case STATE_LOCKED: { SetItemPosition( *display, fThisBannerPositionOffsetFromSelection ); } break; } if( m_WheelState == STATE_LOCKED && i != NUM_WHEEL_ITEMS/2 ) display->m_fPercentGray = 0.5f; else display->m_fPercentGray = 0; display->Draw(); } void WheelBase::UpdateScrollbar(unsigned int size) { int total_num_items = size; float item_at=m_iSelection - m_fPositionOffsetFromSelection; if(NUM_WHEEL_ITEMS >= total_num_items) { m_ScrollBar.SetPercentage( 0, 1 ); } else { float size = float(NUM_WHEEL_ITEMS) / total_num_items; float center = item_at / total_num_items; size *= 0.5f; m_ScrollBar.SetPercentage( center - size, center + size ); } } bool WheelBase::IsSettled() const { if( m_Moving ) return false; if( m_WheelState != STATE_SELECTING_GENERIC && m_WheelState != STATE_LOCKED ) return false; if( m_fPositionOffsetFromSelection != 0 ) return false; return true; } void WheelBase::Update( float fDeltaTime ) { ActorFrame::Update( fDeltaTime ); UpdateItems(fDeltaTime); //Moved to CommonUpdateProcedure, seems to work fine //Revert if it happens to break something UpdateScrollbar(); if( m_Moving ) { m_TimeBeforeMovingBegins -= fDeltaTime; m_TimeBeforeMovingBegins = max(m_TimeBeforeMovingBegins, 0); } // update wheel state m_fTimeLeftInState -= fDeltaTime; if( m_fTimeLeftInState <= 0 ) // time to go to a new state { UpdateSwitch(); } if( m_WheelState == STATE_LOCKED ) { /* Do this in at most .1 sec chunks, so we don't get weird if we * stop for some reason (and so it behaves the same when being * single stepped). */ float tm = fDeltaTime; while(tm > 0) { float t = min(tm, 0.1f); tm -= t; m_fPositionOffsetFromSelection = clamp( m_fPositionOffsetFromSelection, -0.3f, +0.3f ); float fSpringForce = - m_fPositionOffsetFromSelection * LOCKED_INITIAL_VELOCITY; m_fLockedWheelVelocity += fSpringForce; float fDrag = -m_fLockedWheelVelocity * t*4; m_fLockedWheelVelocity += fDrag; m_fPositionOffsetFromSelection += m_fLockedWheelVelocity*t; if( fabsf(m_fPositionOffsetFromSelection) < 0.01f && fabsf(m_fLockedWheelVelocity) < 0.01f ) { m_fPositionOffsetFromSelection = 0; m_fLockedWheelVelocity = 0; } } } if( IsMoving() ) { /* We're automatically moving. Move linearly, and don't clamp * to the selection. */ float fSpinSpeed = m_SpinSpeed*m_Moving; m_fPositionOffsetFromSelection -= fSpinSpeed*fDeltaTime; /* Make sure that we don't go further than 1 away, in case the * speed is very high or we miss a lot of frames. */ m_fPositionOffsetFromSelection = clamp(m_fPositionOffsetFromSelection, -1.0f, 1.0f); /* If it passed the selection, move again. */ if((m_Moving == -1 && m_fPositionOffsetFromSelection >= 0) || (m_Moving == 1 && m_fPositionOffsetFromSelection <= 0)) { ChangeMusic(m_Moving); if(PREFSMAN->m_iMusicWheelSwitchSpeed < MAX_WHEEL_SOUND_SPEED) m_soundChangeMusic.Play(); } if(PREFSMAN->m_iMusicWheelSwitchSpeed >= MAX_WHEEL_SOUND_SPEED && m_MovingSoundTimer.PeekDeltaTime() >= 1.0f / MAX_WHEEL_SOUND_SPEED) { m_MovingSoundTimer.GetDeltaTime(); m_soundChangeMusic.Play(); } } else { // "rotate" wheel toward selected song float fSpinSpeed = 0.2f + fabsf(m_fPositionOffsetFromSelection)/SWITCH_SECONDS; if( m_fPositionOffsetFromSelection > 0 ) { m_fPositionOffsetFromSelection -= fSpinSpeed*fDeltaTime; if( m_fPositionOffsetFromSelection < 0 ) m_fPositionOffsetFromSelection = 0; } else if( m_fPositionOffsetFromSelection < 0 ) { m_fPositionOffsetFromSelection += fSpinSpeed*fDeltaTime; if( m_fPositionOffsetFromSelection > 0 ) m_fPositionOffsetFromSelection = 0; } } } void WheelBase::UpdateItems(float fDeltaTime) { for( unsigned i = 0; i < unsigned(NUM_WHEEL_ITEMS); i++) { m_WheelBaseItems[i]->Update( fDeltaTime ); } } void WheelBase::UpdateSwitch() { switch( m_WheelState ) { case STATE_TWEENING_ON_SCREEN: m_fTimeLeftInState = 0; if( (GAMESTATE->IsExtraStage() && !PREFSMAN->m_bPickExtraStage) || GAMESTATE->IsExtraStage2() ) { m_WheelState = STATE_LOCKED; SCREENMAN->PlayStartSound(); m_fLockedWheelVelocity = 0; } else { m_WheelState = STATE_SELECTING_GENERIC; } break; case STATE_TWEENING_OFF_SCREEN: m_WheelState = STATE_WAITING_OFF_SCREEN; m_fTimeLeftInState = 0; break; case STATE_SELECTING_GENERIC: m_fTimeLeftInState = 0; break; case STATE_WAITING_OFF_SCREEN: break; case STATE_LOCKED: break; default: ASSERT(0); // all state changes should be handled explicitly break; } } bool WheelBase::Select() // return true if this selection ends the screen { LOG->Trace( "WheelBase::Select()" ); m_Moving = 0; if (!m_isEmpty) { switch( m_WheelBaseItemsData[m_iSelection]->m_Type ) { case TYPE_GENERIC: m_LastSelection = m_WheelBaseItemsData[m_iSelection]; return false; case TYPE_SECTION: { CString sThisItemSectionName = m_WheelBaseItemsData[m_iSelection]->m_sText; if( m_sExpandedSectionName == sThisItemSectionName ) // already expanded m_sExpandedSectionName = ""; // collapse it else // already collapsed m_sExpandedSectionName = sThisItemSectionName; // expand it m_soundExpand.Play(); } return false; default: ASSERT(0); return false; } } return false; } WheelItemBaseData* WheelBase::GetItem(unsigned int index) // return true if this selection ends the screen { if (!m_isEmpty) if (index < m_WheelBaseItemsData.size()) return m_WheelBaseItemsData[index]; return NULL; } int WheelBase::IsMoving() const { return m_Moving && m_TimeBeforeMovingBegins == 0; } void WheelBase::TweenOnScreen(bool changing_sort) { m_WheelState = STATE_TWEENING_ON_SCREEN; SetItemPosition( *m_sprHighlight, 0 ); COMMAND( m_sprHighlight, "StartOn"); if( changing_sort ) { const float delay = fabsf(NUM_WHEEL_ITEMS/2-WHEEL_ITEM_ON_DELAY_CENTER) * WHEEL_ITEM_ON_DELAY_OFFSET; m_sprHighlight->BeginTweening( delay ); // sleep COMMAND( m_sprHighlight, "FinishOnSort"); } else { COMMAND( m_sprHighlight, "FinishOn"); } m_ScrollBar.SetX( SCROLL_BAR_X ); m_ScrollBar.AddX( 30 ); if(changing_sort) m_ScrollBar.BeginTweening( 0.2f ); // sleep else m_ScrollBar.BeginTweening( 0.7f ); // sleep m_ScrollBar.BeginTweening( 0.2f , Actor::TWEEN_ACCELERATE ); m_ScrollBar.AddX( -30 ); TweenOnScreenUpdateItems(changing_sort); if( changing_sort ) HurryTweening( 0.25f ); m_fTimeLeftInState = GetTweenTimeLeft() + 0.100f; } void WheelBase::TweenOnScreenUpdateItems(bool changing_sort) { for( int i=0; iBeginTweening( delay ); // sleep COMMAND( display, "FinishOn"); if( changing_sort ) display->HurryTweening( 0.25f ); } } void WheelBase::TweenOffScreen(bool changing_sort) { m_WheelState = STATE_TWEENING_OFF_SCREEN; SetItemPosition( *m_sprHighlight, 0 ); COMMAND( m_sprHighlight, "StartOff"); if( changing_sort ) { /* When changing sort, tween the overlay with the item in the center; * having it separate looks messy when we're moving fast. */ const float delay = fabsf(NUM_WHEEL_ITEMS/2-WHEEL_ITEM_ON_DELAY_CENTER) * WHEEL_ITEM_ON_DELAY_OFFSET; m_sprHighlight->BeginTweening( delay ); // sleep COMMAND( m_sprHighlight, "FinishOffSort"); } else { COMMAND( m_sprHighlight, "FinishOff"); } m_ScrollBar.BeginTweening( 0 ); m_ScrollBar.BeginTweening( 0.2f, Actor::TWEEN_ACCELERATE ); m_ScrollBar.SetX( SCROLL_BAR_X+30 ); TweenOffScreenUpdateItems(changing_sort); if( changing_sort ) HurryTweening( 0.25f ); m_fTimeLeftInState = GetTweenTimeLeft() + 0.100f; } void WheelBase::TweenOffScreenUpdateItems(bool changing_sort) { for( int i=0; iBeginTweening( delay ); // sleep COMMAND( display, "FinishOff"); if( changing_sort ) display->HurryTweening( 0.25f ); } } void WheelBase::Move(int n) { if(n == m_Moving) return; if( m_WheelState == STATE_LOCKED ) { if(n) { int iSign = n/abs(n); m_fLockedWheelVelocity = iSign*LOCKED_INITIAL_VELOCITY; m_soundLocked.Play(); } return; } if (!MoveSpecific(n)) return; m_TimeBeforeMovingBegins = 1/4.0f; m_SpinSpeed = float(PREFSMAN->m_iMusicWheelSwitchSpeed); m_Moving = n; if(m_Moving) ChangeMusic(m_Moving); } bool WheelBase::MoveSpecific(int n) { /* If we're not selecting, discard this. We won't ignore it; we'll * get called again every time the key is repeated. */ /* Still process Move(0) so we sometimes continue moving immediate * after the sort change finished and before the repeat event causes a * Move(0). -Chris */ switch( m_WheelState ) { case STATE_SELECTING_GENERIC: break; case STATE_FLYING_OFF_BEFORE_NEXT_SORT: case STATE_FLYING_ON_AFTER_NEXT_SORT: if( n!= 0 ) return false; break; default: return false; // don't continue } if(m_Moving != 0 && n == 0 && m_TimeBeforeMovingBegins == 0) { /* We were moving, and now we're stopping. If we're really close to * the selection, move to the next one, so we have a chance to spin down * smoothly. */ if(fabsf(m_fPositionOffsetFromSelection) < 0.25f ) ChangeMusic(m_Moving); /* Make sure the user always gets an SM_SongChanged when * Moving() is 0, so the final banner, etc. always gets set. */ // SCREENMAN->PostMessageToTopScreen( SM_SongChanged, 0 ); } return true; } void WheelBase::AddItem(WheelItemBaseData* itemdata) { int visible, index; m_WheelBaseItemsData.push_back(itemdata); visible = FirstVisibleIndex(); index = m_WheelBaseItemsData.size(); if (m_isEmpty) { m_isEmpty = false; //Remove the - Empty - field when we add an object from an empty state. RemoveItem(0); } //If the item was shown in the wheel, rebuild the wheel if ((0 <= (index - visible)) && ((index - visible) < NUM_WHEEL_ITEMS)) { RebuildWheelItems(); } } void WheelBase::ChangeMusic(int dist) { m_iSelection += dist; wrap( m_iSelection, m_WheelBaseItemsData.size() ); RebuildWheelItems( dist ); m_fPositionOffsetFromSelection += dist; // SCREENMAN->PostMessageToTopScreen( SM_SongChanged, 0 ); /* If we're moving automatically, don't play this; it'll be called in Update. */ if(!IsMoving()) m_soundChangeMusic.Play(); } void WheelBase::BuildWheelItemsData( vector &arrayWheelItemDatas ) { if( arrayWheelItemDatas.empty() ) { arrayWheelItemDatas.push_back( new WheelItemBaseData(TYPE_GENERIC, "- EMPTY -", RageColor(1,0,0,1)) ); } } void WheelBase::RebuildWheelItems( int dist ) { // rewind to first index that will be displayed; int iFirstVisibleIndex = m_iSelection; if( m_iSelection > int(m_WheelBaseItemsData.size()-1) ) m_iSelection = 0; // find the first wheel item shown iFirstVisibleIndex -= NUM_WHEEL_ITEMS/2; ASSERT(m_WheelBaseItemsData.size()); wrap( iFirstVisibleIndex, m_WheelBaseItemsData.size() ); // iIndex is now the index of the lowest WheelItem to draw if( dist == -999999 ) { // Refresh all for( int i=0; iLoadFromWheelItemBaseData( data ); } } else { // Shift items and refresh only those that have changed. CircularShift( m_WheelBaseItems, dist ); if( dist > 0 ) { for( int i=NUM_WHEEL_ITEMS-dist; iLoadFromWheelItemBaseData( data ); } } else if( dist < 0 ) { for( int i=0; i<-dist; i++ ) { int iIndex = iFirstVisibleIndex + i; wrap( iIndex, m_WheelBaseItemsData.size() ); WheelItemBaseData *data = m_WheelBaseItemsData[iIndex]; WheelItemBase *display = m_WheelBaseItems[i]; display->LoadFromWheelItemBaseData( data ); } } } } WheelItemBaseData* WheelBase::LastSelected( ) { if (m_isEmpty) return NULL; else return m_LastSelection; } void WheelBase::RemoveItem( int index ) { if (!m_isEmpty) { if (index < (int)m_WheelBaseItemsData.size()) { vector::iterator i = m_WheelBaseItemsData.begin(); i += index; //If this item's data happened to be last selected, make it NULL. if (m_LastSelection == *i) m_LastSelection = NULL; SAFE_DELETE( *i ); m_WheelBaseItemsData.erase(i); if (m_WheelBaseItemsData.size() < 1) { m_isEmpty = true; BuildWheelItemsData(m_WheelBaseItemsData); } RebuildWheelItems(); } } } int WheelBase::FirstVisibleIndex() { // rewind to first index that will be displayed; int iFirstVisibleIndex = m_iSelection; if( m_iSelection > int(m_WheelBaseItemsData.size()-1) ) m_iSelection = 0; // find the first wheel item shown iFirstVisibleIndex -= NUM_WHEEL_ITEMS/2; wrap( iFirstVisibleIndex, m_WheelBaseItemsData.size() ); return iFirstVisibleIndex; } /* * (c) 2001-2004 Chris Danford, Chris Gomez, Glenn Maynard, Josh Allen * 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. */