From ec3e76277cc24962c561cae377f50f2f2f111e2e Mon Sep 17 00:00:00 2001 From: Kyzentun Date: Mon, 5 May 2014 14:16:08 -0600 Subject: [PATCH] Added interface for controlling the behavior of the hasts system. Added support for negative haste. Moves haste rate calculations to UpdateHasteRate. GetHasteRate now just returns the calculated value. Added FLOAT_NO_SPEED_INTERFACE and FLOAT_TABLE_INTERFACE macros to OptionsBinding.h --- Docs/Luadoc/Lua.xml | 5 + Docs/Luadoc/LuaDocumentation.xml | 22 +++++ Docs/Themerdocs/haste.txt | 79 ++++++++++++++++ src/OptionsBinding.h | 46 ++++++++++ src/ScreenGameplay.cpp | 151 +++++++++++++++++++++++++++---- src/ScreenGameplay.h | 9 ++ src/SongOptions.cpp | 2 +- 7 files changed, 297 insertions(+), 17 deletions(-) create mode 100644 Docs/Themerdocs/haste.txt diff --git a/Docs/Luadoc/Lua.xml b/Docs/Luadoc/Lua.xml index 681e827a2c..c73a9f21d6 100644 --- a/Docs/Luadoc/Lua.xml +++ b/Docs/Luadoc/Lua.xml @@ -1284,6 +1284,11 @@ + + + + + diff --git a/Docs/Luadoc/LuaDocumentation.xml b/Docs/Luadoc/LuaDocumentation.xml index 3fd0305536..f2b82ba9b7 100644 --- a/Docs/Luadoc/LuaDocumentation.xml +++ b/Docs/Luadoc/LuaDocumentation.xml @@ -3598,6 +3598,27 @@ save yourself some time, copy this for undocumented things: Returns the PlayerInfo for player pn. + + Returns the current true beats per second for the specified player.
+ This takes into account the current music rate and the current haste effect.
+ If you are displaying the BPM on ScreenGameplay, this is what you should use to have correct behavior when Haste and/or a music rate mod are in effect. +
+ + This is part of the system for controlling how haste behaves.
+ Read Docs/Themerdocs/haste.txt. +
+ + This is part of the system for controlling how haste behaves.
+ Read Docs/Themerdocs/haste.txt. +
+ + This is part of the system for controlling how haste behaves.
+ Read Docs/Themerdocs/haste.txt. +
+ + This is part of the system for controlling how haste behaves.
+ Read Docs/Themerdocs/haste.txt. +
Sets the next Screen to be loaded. @@ -4092,6 +4113,7 @@ save yourself some time, copy this for undocumented things: Limited to the range 0 < rate <= 3 because speeds greater than 3 are likely to crash.
+ Not actually usable as a float. Any value other than 0 is considered "on". diff --git a/Docs/Themerdocs/haste.txt b/Docs/Themerdocs/haste.txt new file mode 100644 index 0000000000..ecb3da1208 --- /dev/null +++ b/Docs/Themerdocs/haste.txt @@ -0,0 +1,79 @@ +Traditionally, the Haste effect is an effect where the song speeds up when the player is doing well, and slows down when the player is doing poorly. +This document lists the lua functions that change the behavior of Haste and some suggested practices. + +All of these functions are combined Get/Set. This means that they always return the value as it was before the function was called, and the parameters are optional. "prev_value= SongOptions:Haste(new_value)" (somewhere else) "SongOptions:Haste(prev_value)" is an example of using this to set a new value, store the previous, then later restore the previous value. + +SongOptions:Haste(n) + Gets/Sets the haste multiplier. The haste multiplier is applied to the Haste effect as a scale factor. Negative haste works as the inverse of normal haste. + SongOptions:Haste(0) disables Haste. + Limited to the range of -1 to 1, but is a floating point value. + +ScreenGameplay:HasteTurningPoints({a, b ...}) + Gets/Sets the turning points at which the haste effect changes. + There must be at least two turning points. + Each turning point must be greater than the previous. + Each turning point must be between -1 and 1. + +ScreenGameplay:HasteAddAmounts({a, b ...}) + Gets/Sets the amounts that are added to the song speed at each turning point. + There must be at least two add amounts. + Each add amount must be greater than the previous. + Each add amount must be between -1 and 1. + If you allow negative haste in your theme, do not allow it to be combined with an add amount of 1. It won't crash, but it will bring the song to a standstill. The same goes for positive haste and add amount -1. + +ScreenGameplay:HasteTimeBetweenUpdates(n) + Gets/Sets the amount of time between Haste updates. Every n seconds, if any player has hit all the steps since the last update, the value used to determine where the player is on the haste scale increases by .1. + +ScreenGameplay:HasteLifeSwitchPoint(n) + Gets/Sets the point below which the haste effect depends on the life bar. + +ScreenGameplay:GetTrueBPS(player_number) + Returns the current true beats per second for the specified player. + This takes into account the current music rate and the current haste effect. + If you are displaying the BPM on ScreenGameplay, this is what you should use to have correct behavior when Haste and/or a music rate mod are in effect. + + +Full explanation: +Variables: (not the names used in the source code) +haste_score: + Controls where the player is on the scale from minimum haste to maximum haste. + Ranges from -1 to 1. + Mode 1: Player's life is above haste_life_switch_point. + haste_score starts at 0 and increases by .1 every haste_update_time seconds if the player has hit all steps. + Mode 2: Player's life is at or below haste_life_switch_point. + haste_score is between -1 and 0, scaled by the player's life. At 0 life, haste_score is -1. At haste_life_switch_point, haste_score is at 0. +haste_life_switch_point: + The point at which haste_score switches from Mode 1 to Mode 2. +turning_points: +add_amounts: + There must be the same number of turning_points as add_amounts. + There must be at least two turning_points/add_amounts. + If either of these conditions is false, haste is disabled. + Values must be between -1 and 1. Attempting to set an invalid value will throw an error. + haste_score is a single linear input. turning_points and add_amounts are used to transform this single linear input into a set of connected lines. + The turning_points adjacent to haste_score's value are used as the "from" range for scaling haste_score to speed_add. This shall be referred to as from_low and from_high. + The add_amounts that match up to the turning_points are used as the "to" range for scaling haste_score to speed_add. This shall be referred to as to_low and to_high. + So when haste_score is equal to from_low, speed_add comes out as to_low. Wheen haste_score is equal to from_high, speed_add comes out as to_high. +speed_add: + The amount that is added to 1 to get the final haste effect. + After being calculated from turning_points and add_amounts, speed_add is multiplied by song_haste from the song options. This means that if song_haste is negative, speed_add will work in reverse, slowing the song down. +accumulated_seconds: + While the song is sped up, the player accumulates seconds. While the song is slowed down, the player loses seconds. When the player has 0 seconds, the song is no longer slowed down. This prevents a player from keeping the song perpetually slow by keeping their life bar low. + If song_haste is negative, this works in reverse. + +Example: +-- This is what the default haste settings would be if they were set through the above functions. + ScreenGameplay:HasteTurningPoints({-1, 0, .3, 1}) + ScreenGameplay:HasteAddAmounts({-.5, 0, .2, .5}) + ScreenGameplay:HasteTimeBetweenUpdates(4) + ScreenGameplay:HasteLifeSwitchPoint(.5) +-- haste_score goes up by .1 every 4 seconds if the player hits all steps. +-- When haste_score is between -1 and 0 (player's life is between 0 and .5), speed_add will be between -.5 and 0 before being multiplied by song_haste. +-- When haste_score is between 0 and .3 (player life above .5), speed_add will be between 0 and .2 before being multiplied by song_haste. +-- When haste_score is between .3 and 1 (player life above .5), speed_add will be between .2 and .5 before being multiplied by song_haste. + + +Source code references to be used by people looking to improve this explanation: +ScreenGameplay::UpdateHasteRate +ScreenGameplay.cpp:1743-1757 (where UpdateHasteRate is called and AccumulatedHasteSeconds is updated) +ScreenGameplay::Init (lines 355-369) diff --git a/src/OptionsBinding.h b/src/OptionsBinding.h index 67628b541b..be979ab7bb 100644 --- a/src/OptionsBinding.h +++ b/src/OptionsBinding.h @@ -33,6 +33,22 @@ } \ return 2; \ } +#define FLOAT_NO_SPEED_INTERFACE(func_name, member, valid) \ + static int func_name(T* p, lua_State* L) \ + { \ + DefaultNilArgs(L, 1); \ + lua_pushnumber(L, p->m_f ## member); \ + if(lua_isnumber(L, 1)) \ + { \ + float v= FArg(1); \ + if(!valid) \ + { \ + luaL_error(L, "Invalid value %f", v); \ + } \ + p->m_f ## member = v; \ + } \ + return 1; \ + } #define INT_INTERFACE(func_name, member) \ static int func_name(T* p, lua_State* L) \ { \ @@ -66,6 +82,36 @@ } \ return 1; \ } +// Walk the table to make sure all entries are valid before setting. +#define FLOAT_TABLE_INTERFACE(func_name, member, valid) \ + static int func_name(T* p, lua_State* L) \ + { \ + DefaultNilArgs(L, 1); \ + lua_createtable(L, p->m_ ## member.size(), 0); \ + for(size_t n= 0; n < p->m_ ## member.size(); ++n) \ + { \ + lua_pushnumber(L, p->m_ ## member[n]); \ + lua_rawseti(L, -2, n+1); \ + } \ + if(lua_istable(L, 1)) \ + { \ + size_t size= lua_objlen(L, 1); \ + if(valid(L, 1)) \ + { \ + p->m_ ## member.clear(); \ + p->m_ ## member.reserve(size); \ + for(size_t n= 1; n <= size; ++n) \ + { \ + lua_pushnumber(L, n); \ + lua_gettable(L, 1); \ + float v= FArg(-1); \ + p->m_ ## member.push_back(v); \ + lua_pop(L, 1); \ + } \ + } \ + } \ + return 1; \ + } #endif diff --git a/src/ScreenGameplay.cpp b/src/ScreenGameplay.cpp index ed67500d1b..49adc799bf 100644 --- a/src/ScreenGameplay.cpp +++ b/src/ScreenGameplay.cpp @@ -352,6 +352,21 @@ void ScreenGameplay::Init() UNPAUSE_WITH_START.Load( m_sName, "UnpauseWithStart"); SURVIVAL_MOD_OVERRIDE.Load(m_sName, "SurvivalModOverride"); + // Default values. The theme can set its own through the Lua interface. + m_HasteTurningPoints.clear(); + m_HasteTurningPoints.push_back(-1); + m_HasteTurningPoints.push_back(0); + m_HasteTurningPoints.push_back(.3); + m_HasteTurningPoints.push_back(1); + m_HasteAddAmounts.clear(); + m_HasteAddAmounts.push_back(-.5); + m_HasteAddAmounts.push_back(0); + m_HasteAddAmounts.push_back(.2); + m_HasteAddAmounts.push_back(.5); + m_fHasteTimeBetweenUpdates= 4; + m_fHasteLifeSwitchPoint= .5; + m_fCurrHasteRate= 1; // Should this be in BeginSong? Not sure whether it should carry over between songs. + if( UseSongBackgroundAndForeground() ) { m_pSongBackground = new Background; @@ -1616,7 +1631,7 @@ void ScreenGameplay::Update( float fDeltaTime ) fSpeed *= GetHasteRate(); RageSoundParams p = m_pSoundMusic->GetParams(); - if( fabsf(p.m_fSpeed - fSpeed) > 0.01f ) + if( fabsf(p.m_fSpeed - fSpeed) > 0.01f && fSpeed >= 0.0f) { p.m_fSpeed = fSpeed; m_pSoundMusic->SetParams( p ); @@ -1725,10 +1740,21 @@ void ScreenGameplay::Update( float fDeltaTime ) { STATSMAN->m_CurStageStats.m_fStepsSeconds += fUnscaledDeltaTime; + UpdateHasteRate(); + if( GAMESTATE->m_SongOptions.GetCurrent().m_fHaste != 0.0f ) { float fHasteRate = GetHasteRate(); - GAMESTATE->m_fAccumulatedHasteSeconds += (fUnscaledDeltaTime * fHasteRate) - fUnscaledDeltaTime; + // For negative haste, accumulate seconds while the song is slowed down. + if(GAMESTATE->m_SongOptions.GetCurrent().m_fHaste < 0) + { + GAMESTATE->m_fAccumulatedHasteSeconds -= (fUnscaledDeltaTime * fHasteRate) - fUnscaledDeltaTime; + } + // For positive haste, accumulate seconds while the song is sped up. + else + { + GAMESTATE->m_fAccumulatedHasteSeconds += (fUnscaledDeltaTime * fHasteRate) - fUnscaledDeltaTime; + } } } @@ -1894,9 +1920,14 @@ void ScreenGameplay::Update( float fDeltaTime ) } float ScreenGameplay::GetHasteRate() +{ + return m_fCurrHasteRate; +} + +void ScreenGameplay::UpdateHasteRate() { if( GAMESTATE->m_Position.m_fMusicSeconds < GAMESTATE->m_fLastHasteUpdateMusicSeconds || // new song - GAMESTATE->m_Position.m_fMusicSeconds > GAMESTATE->m_fLastHasteUpdateMusicSeconds + 4 ) + GAMESTATE->m_Position.m_fMusicSeconds > GAMESTATE->m_fLastHasteUpdateMusicSeconds + m_fHasteTimeBetweenUpdates ) { bool bAnyPlayerHitAllNotes = false; FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) @@ -1929,27 +1960,70 @@ float ScreenGameplay::GetHasteRate() continue; fMaxLife = max( fMaxLife, pi->m_pLifeMeter->GetLife() ); } - if( fMaxLife < 0.5f ) - GAMESTATE->m_fHasteRate = SCALE( fMaxLife, 0.0f, 0.5f, -1.0f, 0.0f ); + if( fMaxLife <= m_fHasteLifeSwitchPoint ) + GAMESTATE->m_fHasteRate = SCALE( fMaxLife, 0.0f, m_fHasteLifeSwitchPoint, -1.0f, 0.0f ); + CLAMP( GAMESTATE->m_fHasteRate, -1.0f, +1.0f ); float fSpeed = 1.0f; - if( GAMESTATE->m_fHasteRate < 0 ) - fSpeed = SCALE( GAMESTATE->m_fHasteRate, -1.0f, 0.0f, 0.5f, 1.0f ); - else if( GAMESTATE->m_fHasteRate < 0.3f ) - fSpeed = SCALE( GAMESTATE->m_fHasteRate, 0.0f, 0.3f, 1.0f, 1.2f ); - else - fSpeed = SCALE( GAMESTATE->m_fHasteRate, 0.3f, 1.0f, 1.2f, 1.5f ); - fSpeed *= GAMESTATE->m_SongOptions.GetCurrent().m_fHaste; + // If there are no turning points or no add amounts, the bad themer probably thinks that's a way to disable haste. + // Since we're outside a lua function, crashing (asserting) won't point back to the source of the problem. + if(m_HasteTurningPoints.size() < 2 || m_HasteAddAmounts.size() < 2 || + m_HasteTurningPoints.size() != m_HasteAddAmounts.size()) + { + m_fCurrHasteRate= fSpeed; + return; + } + float options_haste= GAMESTATE->m_SongOptions.GetCurrent().m_fHaste; + float scale_from_low= -1; + float scale_from_high= 1; + float scale_to_low= 0; + float scale_to_high=0; + for(size_t turning_point= 0; turning_point < m_HasteTurningPoints.size(); + ++turning_point) + { + float curr_turning_point= m_HasteTurningPoints[turning_point]; + scale_from_high= curr_turning_point; + scale_to_high= m_HasteAddAmounts[turning_point]; + if(GAMESTATE->m_fHasteRate < curr_turning_point) + { + break; + } + scale_from_low= curr_turning_point; + scale_to_low= m_HasteAddAmounts[turning_point]; + } + // If negative haste is being used, the game instead slows down when the player does well. + float speed_add= SCALE(GAMESTATE->m_fHasteRate, scale_from_low, scale_from_high, scale_to_low, scale_to_high) * options_haste; + if(scale_from_low == scale_from_high) + { + speed_add= scale_to_high * options_haste; + } + CLAMP(speed_add, -1.0f, 1.0f); - if( GAMESTATE->m_fAccumulatedHasteSeconds <= 1 ) + // Only adjust speed_add by AccumulatedHasteSeconds when the player is losing seconds. Otherwise, gaining the first second is interfered with. + bool losing_seconds= false; + if(options_haste > 0) + { + losing_seconds= speed_add < 0; + } + else + { + losing_seconds= speed_add > 0; + } + if( losing_seconds && GAMESTATE->m_fAccumulatedHasteSeconds <= 1 ) { /* Only allow slowing down the song while the players have accumulated * haste. This prevents dragging on the song by keeping the life meter * nearly empty. */ - float fClamped = max( 1.0f, fSpeed ); - fSpeed = lerp( GAMESTATE->m_fAccumulatedHasteSeconds, fClamped, fSpeed ); + /* In positive haste mode, the player accumulates seconds while the song + * is sped up, and loses them while the song is slowed down. "<= 1" + * means that the player is only eligible to slow the song down when + * they are down to their last accumulated second. -Kyz */ + // 1 second left is full speed_add, 0 seconds left is no speed_add. + float clamp_secs= max(0, GAMESTATE->m_fAccumulatedHasteSeconds); + speed_add = speed_add * clamp_secs; } - return fSpeed; + fSpeed += speed_add; + m_fCurrHasteRate= fSpeed; } void ScreenGameplay::UpdateLights() @@ -2823,6 +2897,7 @@ bool ScreenGameplay::LoadReplay() // lua start #include "LuaBinding.h" +#include "OptionsBinding.h" /** @brief Allow Lua to have access to the ScreenGameplay. */ class LunaScreenGameplay: public Luna @@ -2867,6 +2942,45 @@ public: static int PauseGame( T* p, lua_State *L ) { p->Pause( BArg(1)); return 0; } static int IsPaused( T* p, lua_State *L ) { lua_pushboolean( L, p->IsPaused() ); return 1; } static int GetHasteRate( T* p, lua_State *L ) { lua_pushnumber( L, p->GetHasteRate() ); return 1; } + static bool TurningPointsValid(lua_State* L, int index) + { + size_t size= lua_objlen(L, index); + if(size < 2) + { + luaL_error(L, "Invalid number of entries %zu", size); + } + float prev_turning= -1; + for(size_t n= 1; n < size; ++n) + { + lua_pushnumber(L, n); + lua_gettable(L, index); + float v= FArg(-1); + if(v < prev_turning || v > 1) + { + luaL_error(L, "Invalid value %f", v); + } + lua_pop(L, 1); + } + return true; + } + static bool AddAmountsValid(lua_State* L, int index) + { + return TurningPointsValid(L, index); + } + FLOAT_TABLE_INTERFACE(HasteTurningPoints, HasteTurningPoints, TurningPointsValid); + FLOAT_TABLE_INTERFACE(HasteAddAmounts, HasteAddAmounts, AddAmountsValid); + FLOAT_NO_SPEED_INTERFACE(HasteTimeBetweenUpdates, HasteTimeBetweenUpdates, (v > 0)); + FLOAT_NO_SPEED_INTERFACE(HasteLifeSwitchPoint, HasteLifeSwitchPoint, (v >= 0 && v <= 1)); + static int GetTrueBPS(T* p, lua_State* L) + { + PlayerNumber pn= Enum::Check(L, 1); + float haste= p->GetHasteRate(); + float rate= GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate; + float bps= GAMESTATE->m_pPlayerState[pn]->m_Position.m_fCurBPS; + float true_bps= haste * rate * bps; + lua_pushnumber(L, true_bps); + return 1; + } LunaScreenGameplay() { @@ -2879,6 +2993,11 @@ public: ADD_METHOD( PauseGame ); ADD_METHOD( IsPaused ); ADD_METHOD( GetHasteRate ); + ADD_METHOD( HasteTurningPoints ); + ADD_METHOD( HasteAddAmounts ); + ADD_METHOD( HasteTimeBetweenUpdates ); + ADD_METHOD( HasteLifeSwitchPoint ); + ADD_METHOD( GetTrueBPS ); } }; diff --git a/src/ScreenGameplay.h b/src/ScreenGameplay.h index 462c197e59..bd385cb9cb 100644 --- a/src/ScreenGameplay.h +++ b/src/ScreenGameplay.h @@ -172,6 +172,11 @@ public: bool IsPaused() const { return m_bPaused; } float GetHasteRate(); + vector m_HasteTurningPoints; // Values at which the meaning of GAMESTATE->m_fHasteRate changes. + vector m_HasteAddAmounts; // Amounts that are added to speed depending on what turning point has been passed. + float m_fHasteTimeBetweenUpdates; // Seconds between haste updates. + float m_fHasteLifeSwitchPoint; // Life amount below which GAMESTATE->m_fHasteRate is based on the life amount. + protected: virtual void UpdateStageStats( MultiPlayer /* mp */ ) {}; // overridden for multiplayer @@ -227,6 +232,10 @@ protected: virtual void InitSongQueues(); + void UpdateHasteRate(); + float m_fCurrHasteRate; + // These exist so that the haste rate isn't recalculated every time GetHasteRate is called, which is at least once per frame. -Kyz + /** @brief The different game states of ScreenGameplay. */ enum DancingState { STATE_INTRO = 0, /**< The starting state, pressing Back isn't allowed here. */ diff --git a/src/SongOptions.cpp b/src/SongOptions.cpp index 360b96505e..8e67f5e545 100644 --- a/src/SongOptions.cpp +++ b/src/SongOptions.cpp @@ -296,7 +296,7 @@ public: BOOL_INTERFACE(SaveScore, SaveScore); BOOL_INTERFACE(SaveReplay, SaveReplay); FLOAT_INTERFACE(MusicRate, MusicRate, (v > 0.0f && v <= 3.0f)); // Greater than 3 seems to crash frequently, haven't investigated why. -Kyz - FLOAT_INTERFACE(Haste, Haste, true); + FLOAT_INTERFACE(Haste, Haste, (v >= -1.0f && v <= 1.0f)); LunaSongOptions() {