KKI LABS // PUBLIC RELEASE
In-Song Modifiers
by AJ 187 of KKI Labs
Draft 1
Official Submission Date: April 12, 2008
[plaintext edition]
In-Song Modifiers
by AJ 187 of KKI Labs
Draft 1
Official Submission Date: April 12, 2008
[plaintext edition]
Abstract
Background
For long periods of time, fans of StepMania and Pump It Up have had to make compromises in terms of accuracy. For example, different difficulties could not have different BPMs in songs due to how the steps were laid out. In addition, an inquiry was posed about launching modifiers at players during gameplay. A solution for both is proposed with experimental research.Conclusion
It is possible for a BGAnimation to launch modifiers at players. This can be used in many forms, perhaps for conditional song BPM changes or throwing effects in to throw players off.In-Song Modifiers: Implementation and Analyzation
Problem: Launch modifiers at players in gameplay.Hypothesis: By using the PlayerState's accessors for PlayerOptions, it is possible to get and set the PlayerOptions in gameplay via BGAnimation Lua scripts.
Implementation: Provided for the benefit of the student, here is a breakdown of the examples provided.
Implementation 1: MAX 300 (arbitrary modifiers) [vimeo]
The first implementation was the baseline test to throw modifiers at a player at given time periods.
The original time periods (presented here as seconds although later research disproved this, as they were actually beats) and modifiers were as follows:
- 5 seconds - 1.5x,hallway
- 10 seconds - 3x,Overhead
- 20 seconds - Overhead,Dizzy
- 30 seconds - Overhead,no Dizzy,Distant
These examples included canceling out, which will be explained later.
The experiment proved that setting the mods will cancel out the player's mods. This was a breakthrough that required us to change our thinking.
[5sec/default.lua]
This is the implementation at 5 "seconds". Since the other files only differ in one line, they will not be reprinted here.
Explanations are in here only.
local t = Def.ActorFrame {
-- A BitmapText was originally used to display the modifiers as they
-- were thrown on. However, difficulties in positioning the text meant
-- that they were not really used. You can swap this out for a Def.Actor
-- if you wish, just remove the settext and InitCommand stuff.
LoadFont("", "_shared2") ..{
-- A good piece of advice from the KKI Labs style guide, ripped off
-- of the "we know what we're doing because we wrote the code" style
-- guide: Name your objects. I thank GRIM for introducing me to it
-- as well. By naming your objects, you can use
-- [ActorFrame]:GetChild("whatevername") to access them, allowing
-- for advanced functionality.
Name="PlayerMods";
InitCommand=cmd(shadowlength,0);
OnCommand=function(self)
-- this code assumes player 1.
-- The PlayerState doesn't have much, but it does have
-- GetPlayerOptions and SetPlayerOptions, which are crucial for
-- this.
local P1State = GAMESTATE:GetPlayerState(PLAYER_1);
-- There are a few ModsLevels. You may want to check Lua.xml
-- (KKI Labs copy: http://kki.ajworld.net/lua/ ) for a list
-- of possible values. ModsLevel_Song was used because it
-- only lasts for one song.
local options = P1State:GetPlayerOptions('ModsLevel_Song');
-- SetPlayerOptions is the other important command. There's a
-- ModsLevel like before, and the second argument is the modifiers
-- you want to replace the player's options with. Yes, replace.
-- All your percentage and #* tricks should work here as well,
-- leading the way for course-like modifiers. However, we are
-- now working on a different implementation of course modifiers
-- that are simpler to modify (only one file needed).
P1State:SetPlayerOptions('ModsLevel_Song',"1.5x,hallway");
-- And these last two statements are just debugging.
options = P1State:GetPlayerOptions('ModsLevel_Song');
self:settext( options );
end;
};
};
return t;
Implementation 2: Mexi Mexi (duplicating expected behavior) [vimeo]
Given the newfound power provided by a successful first test, I then decided to apply this logic to imitate Mexi Mexi's BPM speedup on Crazy.
The code only had to be run once, but it also had to account for any other modifiers the player was using as well, especially if one of them was a speed mod.
[Crazy/default.lua]
Similar layout to last time, better commented as well.
local function MakeModifierForPlayer(pn)
-- A BitmapText was used again for debugging.
return LoadFont("Common","normal")..{
-- Naming your objects according to player can be a real help if
-- you need to do something that involves more than one player.
Name=pn.."ModCheck";
--Text="Test";
OnCommand=function(self)
-- check if the player is actually even here!
if not GAMESTATE:IsSideJoined(pn) then return; end;
-- first get the difficulty.
-- This may seem like it won't work with courses at first, but
-- I am under the belief it does. Currently untested.
local difficulty = GAMESTATE:GetCurrentSteps(pn):GetDifficulty();
-- check if this player is on Crazy.
if( difficulty == 'Difficulty_Hard' ) then
-- we are on CZ, let's do this.
-- get the PlayerState for this player.
local PlayerState = GAMESTATE:GetPlayerState(pn);
-- grab the song options from this PlayerState.
local options = PlayerState:GetPlayerOptions('ModsLevel_Song');
-- now using split, let's put them into a table for comparison
-- This is an important part, as it allows us to tokenize the
-- items so they can be run through as a table.
local listOfOptions = split(", ", options);
-- output a modifier list and set the BitmapText to the list.
-- this code will eventually not write to the BitmapText at
-- all, but will instead output most options.
for i=1,#listOfOptions do
local text = self:GetText();
self:settext(text..","..listOfOptions[i]);
end;
-- now look for any speed modifiers. we will need to
-- perform math on any #x mod. If a Cmod is in place,
-- ignore this, because the expected behavior with a
-- cmod is to FORCE the song to a certain BPM. If
-- that's happening, it's not good to double the Cmod.
local xModIndex = 0;
local originalXMod = 1;
for i=1,#listOfOptions do
-- cmod check.
if string.find(listOfOptions[i], "C%d") ~= nil then
-- oh no, we can't do this. return out
Trace("WARNING: CMOD FOUND. EXITING LOOP");
return;
end;
if string.find(listOfOptions[i], "%dx") ~= nil or string.find(listOfOptions[i], "%d%.%dx") ~= nil then
-- okay this has an x in it. Now, is it an actual
-- speed mod or is it something like Expand?
Trace("AJ DEBUG: Possible x mod = ".. listOfOptions[i]);
local parts = split("x", listOfOptions[i]);
originalXMod = parts[1];
xModIndex = i;
Trace("AJ DEBUG: Original x mod = ".. originalXMod);
end;
end;
-- now double the original x Mod
-- This is where the Mexi Mexi-specific behavior begins.
-- In other songs this could mean play the music at 1.1x rate
-- or give a monster slowdown at a certain point.
local newXMod = originalXMod * 2;
Trace("AJ DEBUG: New x mod = ".. newXMod);
local newOptions = "";
Trace("AJ DEBUG: mod index = ".. xModIndex);
for i=1,#listOfOptions do
-- This statement makes sure we don't add in the original
-- x mod, because one may take precedence over the other
-- or become a compound speed mod that's too fast.
if i ~= xModIndex then
newOptions = newOptions ..",".. listOfOptions[i];
end;
end;
-- Now the new speed mod is appended at the beginning, which
-- had a leading comma before.
newOptions = newXMod .. "x" .. newOptions;
Trace("AJ DEBUG: New Options = ".. newOptions);
-- sets the player options
PlayerState:SetPlayerOptions('ModsLevel_Song',newOptions);
end;
end;
}
end;
local t = Def.ActorFrame{
-- Finally, make the modifier for each player. This probably could've
-- been done with a loop and ivalues but I didn't wanna do that.
MakeModifierForPlayer(PLAYER_1);
MakeModifierForPlayer(PLAYER_2);
};
return t;
Implementation 3: MAX 300 (Canceling a slowdown)
Wolfman2000 challenged me to this after seeing the two videos on YouTube.
The goal was to nullify both the slowdown and freeze of MAX 300. The only stipulations were:
- I could not use a Cmod for the slowdown.
- I had to respect the player's speed mods, as in the Mexi Mexi experiment.
Reusing the code from Mexi Mexi helped us here, as we didn't have to check for anything out of the ordinary.
The next step was to figure out what mods would cancel out the drops. A table of beats, BPMs, and BPM ratios (300/newBPM) is below:
| Beat | BPM | BPM Ratio |
|---|---|---|
| 286 | 200 | 1.5 |
| 287 | 150 | 2.0 |
| 288 | 125 | 2.4 |
| 288.5 | 100 | 3.0 |
| 289 | 75 | 4.0 |
| 289.5 | 50 | 6.0 |
[beat286/default.lua]
The code for beat286 follows:
local function MakeModifierForPlayer(pn)
return LoadFont("Common","normal")..{
Name=pn.."ModCheck";
--Text="Test";
OnCommand=function(self)
-- check if the player is actually even here!
if not GAMESTATE:IsSideJoined(pn) then return; end;
-- first get the difficulty.
local difficulty = GAMESTATE:GetCurrentSteps(pn):GetDifficulty();
-- check if this player is on Heavy.
if( difficulty == 'Difficulty_Hard' ) then
-- get the PlayerState for this player.
local PlayerState = GAMESTATE:GetPlayerState(pn);
-- grab the song options from this PlayerState.
local options = PlayerState:GetPlayerOptions('ModsLevel_Song');
-- now using split, let's put them into a table for comparison
local listOfOptions = split(", ", options);
-- output a modifier list and set the BitmapText to the list.
-- this code will eventually not write to the BitmapText at
-- all, but will instead output most options.
for i=1,#listOfOptions do
local text = self:GetText();
self:settext(text..","..listOfOptions[i]);
end;
-- look for any speed modifiers. we will need to
-- perform math on any #x mod. If a Cmod is in place,
-- ignore this, because the expected behavior with a
-- cmod is to FORCE the song to a certain BPM. If
-- that's happening, it's not good to double the Cmod.
local xModIndex = 0;
local originalXMod = 1;
for i=1,#listOfOptions do
-- cmod check.
if string.find(listOfOptions[i], "C%d") ~= nil then
-- oh no, we can't do this. return out
--Trace("WARNING: CMOD FOUND. EXITING LOOP");
return;
end;
if string.find(listOfOptions[i], "%dx") ~= nil or string.find(listOfOptions[i], "%d%.%dx") ~= nil then
-- okay this has an x in it. Now, is it an actual
-- speed mod or is it something like Expand?
--Trace("AJ DEBUG: Possible x mod = ".. listOfOptions[i]);
local parts = split("x", listOfOptions[i]);
-- This is the first part that the code actually
-- changes from the other implementations.
-- The original xMod is kept in storage for playing
-- with later. On subsequent beats, the new xMod that
-- was set up in the previous beat would be here,
-- meaning that some division had to be done as well
-- to account for it.
originalXMod = parts[1];
xModIndex = i;
--Trace("AJ DEBUG: Original x mod = ".. originalXMod);
end;
end;
-- now change the original x Mod
-- This is where the BPM ratio comes in. 300 - (200*1.5) = 0
-- in difference. This is what we wanted as a result.
local newXMod = originalXMod * 1.5;
Trace("AJ DEBUG: New x mod = ".. newXMod);
local newOptions = "";
--Trace("AJ DEBUG: mod index = ".. xModIndex);
for i=1,#listOfOptions do
if i ~= xModIndex then
newOptions = newOptions ..",".. listOfOptions[i];
end;
end;
newOptions = newXMod .. "x" .. newOptions;
--Trace("AJ DEBUG: New Options = ".. newOptions);
-- sets the player options
-- More debugging junk.
Trace("NEW SPEED MOD: ".. newXMod);
self:settext("Beat ".. GAMESTATE:GetSongBeatVisible() .." / "..newOptions);
PlayerState:SetPlayerOptions('ModsLevel_Song',newOptions);
end;
end;
}
end;
local t = Def.ActorFrame{
MakeModifierForPlayer(PLAYER_1)..{
OnCommand=cmd(x,SCREEN_LEFT;y,SCREEN_CENTER_Y;diffuse,color("1,0,0,1");horizalign,left);
};
MakeModifierForPlayer(PLAYER_2);
};
return t;
For subsequent beats, you would have to divide the new mod (original * ratio) and subtract that by the old mod. If you did not, you'd get up to about 576x, which is pretty insane.
This played on until beat 290, when the freeze came in.
This time around, we had to use a Cmod to nullify the freeze.
[beat290/default.lua]
local function MakeModifierForPlayer(pn)
return LoadFont("Common","normal")..{
Name=pn.."ModCheck";
--Text="Test";
OnCommand=function(self)
-- check if the player is actually even here!
if not GAMESTATE:IsSideJoined(pn) then return; end;
-- first get the difficulty.
local difficulty = GAMESTATE:GetCurrentSteps(pn):GetDifficulty();
-- check if this player is on Heavy.
if( difficulty == 'Difficulty_Hard' ) then
-- get the PlayerState for this player.
local PlayerState = GAMESTATE:GetPlayerState(pn);
-- grab the song options from this PlayerState.
local options = PlayerState:GetPlayerOptions('ModsLevel_Song');
-- now using split, let's put them into a table for comparison
local listOfOptions = split(", ", options);
-- look for any speed modifiers. we will need to
-- perform math on any #x mod. If a Cmod is in place,
-- ignore this, because the expected behavior with a
-- cmod is to FORCE the song to a certain BPM. If
-- that's happening, it's not good to double the Cmod.
local xModIndex = 0;
local originalXMod = 1;
for i=1,#listOfOptions do
-- cmod check.
if string.find(listOfOptions[i], "C%d") ~= nil then
-- oh no, we can't do this. return out
--Trace("WARNING: CMOD FOUND. EXITING LOOP");
return;
end;
if string.find(listOfOptions[i], "%dx") ~= nil or string.find(listOfOptions[i], "%d%.%dx") ~= nil then
-- okay this has an x in it. Now, is it an actual
-- speed mod or is it something like Expand?
--Trace("AJ DEBUG: Possible x mod = ".. listOfOptions[i]);
local parts = split("x", listOfOptions[i]);
originalXMod = parts[1];
xModIndex = i;
Trace("AJ DEBUG: Original x mod = ".. originalXMod);
end;
end;
-- now change the original x Mod
local newXMod = originalXMod / 6;
Trace("AJ DEBUG: New x mod = ".. newXMod);
local newOptions = "";
--Trace("AJ DEBUG: mod index = ".. xModIndex);
for i=1,#listOfOptions do
if i ~= xModIndex then
newOptions = newOptions ..",".. listOfOptions[i];
end;
end;
-- generate a new CMOD by using the original X mod
-- plus MAX 300 BPM LOL!!
-- The first new code starts here. It takes the new xMod,
-- which is the original divided by 6x, the last forced
-- multiplication, and multiplies that by the song's base
-- bpm of 300.
local newCMod = 300 * newXMod;
-- This condition was thrown in there on a whim, as I wanted
-- to make sure the song was on a freeze, even though I knew it
-- would be. The else statement gives some insight as to why
-- I'd even do this.
if GAMESTATE:GetSongFreeze() then
-- Adds the cmod to the front of the line.
newOptions = "C" .. newCMod .. newOptions;
else
Trace("NEW SPEED MOD: ".. newXMod);
-- This would have reset the Cmod and set the xMod, however
-- it's unknown if it worked or not.
newOptions = newXMod .. "x" .. newOptions .. ",no C" .. newCMod;
end;
Trace("AJ DEBUG: New Options = ".. newOptions);
-- sets the player options
self:settext("Beat ".. GAMESTATE:GetSongBeatVisible() .." / "..newOptions);
PlayerState:SetPlayerOptions('ModsLevel_Song',newOptions);
end;
end;
}
end;
local t = Def.ActorFrame{
MakeModifierForPlayer(PLAYER_1)..{
OnCommand=cmd(x,SCREEN_LEFT;y,SCREEN_CENTER_Y;diffuse,color("1,0,0,1");horizalign,left);
};
MakeModifierForPlayer(PLAYER_2);
};
return t;
[beat291/default.lua]
This would dissect the Cmod, extract the xMod out of it, and reset the speed mod the player had before.
local function MakeModifierForPlayer(pn)
return LoadFont("Common","normal")..{
Name=pn.."ModCheck";
--Text="Test";
OnCommand=function(self)
-- check if the player is actually even here!
if not GAMESTATE:IsSideJoined(pn) then return; end;
-- first get the difficulty.
local difficulty = GAMESTATE:GetCurrentSteps(pn):GetDifficulty();
-- check if this player is on Heavy.
if( difficulty == 'Difficulty_Hard' ) then
-- get the PlayerState for this player.
local PlayerState = GAMESTATE:GetPlayerState(pn);
-- grab the song options from this PlayerState.
local options = PlayerState:GetPlayerOptions('ModsLevel_Song');
Trace("AJ Debug: beat 291 options = ".. options);
-- now using split, let's put them into a table for comparison
local listOfOptions = split(", ", options);
-- look for any speed modifiers. we will need to
-- perform math on any #x mod. If a Cmod is in place,
-- ignore this, because the expected behavior with a
-- cmod is to FORCE the song to a certain BPM. If
-- that's happening, it's not good to double the Cmod.
local xModIndex = 0;
local originalXMod = 1;
local cMod = 300;
for i=1,#listOfOptions do
-- cmod check.
if string.find(listOfOptions[i], "C%d") ~= nil then
-- this is when we need to do the cmod shit.
Trace("AJ DEBUG: we have a Cmod bitches: ".. listOfOptions[i]);
local parts = split("C", listOfOptions[i]);
Trace("AJ DEBUG: parts: ".. parts[1]);
cMod = parts[2];
Trace("AJ DEBUG: Cmod is ".. cMod);
end;
--[[
ignore this in beat 291, we're going to do some nutty balls
if string.find(listOfOptions[i], "%dx") ~= nil or string.find(listOfOptions[i], "%d%.%dx") ~= nil then
-- okay this has an x in it. Now, is it an actual
-- speed mod or is it something like Expand?
--Trace("AJ DEBUG: Possible x mod = ".. listOfOptions[i]);
local parts = split("x", listOfOptions[i]);
originalXMod = parts[1];
xModIndex = i;
Trace("AJ DEBUG: Original x mod = ".. originalXMod);
end;
]]
end;
-- dissect the cMod to get the x mod, and set it
local newXMod = cMod / 300;
Trace("AJ DEBUG: New x mod = ".. newXMod);
local newOptions = "";
--Trace("AJ DEBUG: mod index = ".. xModIndex);
for i=1,#listOfOptions do
if i ~= xModIndex then
newOptions = newOptions ..",".. listOfOptions[i];
end;
end;
Trace("NEW SPEED MOD: ".. newXMod);
-- string.gsub is used here as a replacement tool, in order to
-- forcibly remove the Cmod using the no modifier. It's unknown
-- if leaving it out entirely would do the same thing.
newOptions = string.gsub(newOptions, "C"..cMod, "no C"..cMod);
newOptions = newXMod .. "x" .. newOptions;
Trace("AJ DEBUG: New Options = ".. newOptions);
-- sets the player options
self:settext("Beat ".. GAMESTATE:GetSongBeatVisible() .." / "..newOptions);
PlayerState:SetPlayerOptions('ModsLevel_Song',newOptions);
end;
end;
}
end;
local t = Def.ActorFrame{
MakeModifierForPlayer(PLAYER_1)..{
OnCommand=cmd(x,SCREEN_LEFT;y,SCREEN_CENTER_Y+24;diffuse,color("1,0,1,1");horizalign,left);
};
MakeModifierForPlayer(PLAYER_2);
};
return t;
