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

This commit is contained in:
Kyzentun
2014-05-05 14:16:08 -06:00
parent b10c04c7f3
commit ec3e76277c
7 changed files with 297 additions and 17 deletions
+5
View File
@@ -1284,6 +1284,11 @@
<Function name='GetLifeMeter'/>
<Function name='GetNextCourseSong'/>
<Function name='GetPlayerInfo'/>
<Function name='GetTrueBPS'/>
<Function name='HasteAddAmounts'/>
<Function name='HasteLifeSwitchPoint'/>
<Function name='HasteTimeBetweenUpdates'/>
<Function name='HasteTurningPoints'/>
<Function name='IsPaused'/>
<Function name='PauseGame'/>
</Class>
+22
View File
@@ -3598,6 +3598,27 @@ save yourself some time, copy this for undocumented things:
<Function name='GetPlayerInfo' return='PlayerInfo' arguments='PlayerNumber pn'>
Returns the PlayerInfo for player <code>pn</code>.
</Function>
<Function name='GetTrueBPS' return='float' arguments='PlayerNumber pn'>
Returns the current true beats per second for the specified player.<br />
This takes into account the current music rate and the current haste effect.<br />
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.
</Function>
<Function name='HasteAddAmounts' return='{float}' arguments='{float}'>
This is part of the system for controlling how haste behaves. <br />
Read Docs/Themerdocs/haste.txt.
</Function>
<Function name='HasteLifeSwitchPoint' return='float' arguments='float'>
This is part of the system for controlling how haste behaves. <br />
Read Docs/Themerdocs/haste.txt.
</Function>
<Function name='HasteTimeBetweenUpdates' return='float' arguments='float'>
This is part of the system for controlling how haste behaves. <br />
Read Docs/Themerdocs/haste.txt.
</Function>
<Function name='HasteTurningPoints' return='{float}' arguments='{float}'>
This is part of the system for controlling how haste behaves. <br />
Read Docs/Themerdocs/haste.txt.
</Function>
<Function name='SetNewScreen' return='void' arguments='string s'>
Sets the next Screen to be loaded.
</Function>
@@ -4092,6 +4113,7 @@ save yourself some time, copy this for undocumented things:
Limited to the range 0 &lt; rate &lt;= 3 because speeds greater than 3 are likely to crash.
</Function>
<Function name='Haste' return='float' arguments='float h'>
Not actually usable as a float. Any value other than 0 is considered "on".
</Function>
</Class>
<Class name='SongPosition'>
+79
View File
@@ -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)
+46
View File
@@ -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
+135 -16
View File
@@ -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<ScreenGameplay>
@@ -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<PlayerNumber>(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 );
}
};
+9
View File
@@ -172,6 +172,11 @@ public:
bool IsPaused() const { return m_bPaused; }
float GetHasteRate();
vector<float> m_HasteTurningPoints; // Values at which the meaning of GAMESTATE->m_fHasteRate changes.
vector<float> 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. */
+1 -1
View File
@@ -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()
{