mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2024-11-23 16:17:53 -05:00
Merge pull request #114 from FunkinCrew/bugfix/conductor-monster
Fixes to Conductor for Monster
This commit is contained in:
commit
9e40ed9006
13 changed files with 262 additions and 164 deletions
|
@ -1,23 +1,18 @@
|
||||||
package funkin;
|
package funkin;
|
||||||
|
|
||||||
import funkin.play.song.SongData.SongTimeChange;
|
import funkin.util.Constants;
|
||||||
import flixel.util.FlxSignal;
|
import flixel.util.FlxSignal;
|
||||||
|
import flixel.math.FlxMath;
|
||||||
|
import funkin.SongLoad.SwagSong;
|
||||||
import funkin.play.song.Song.SongDifficulty;
|
import funkin.play.song.Song.SongDifficulty;
|
||||||
|
import funkin.play.song.SongData.SongTimeChange;
|
||||||
typedef BPMChangeEvent =
|
|
||||||
{
|
|
||||||
var stepTime:Int;
|
|
||||||
var songTime:Float;
|
|
||||||
var bpm:Float;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A global source of truth for timing information.
|
* A core class which handles musical timing throughout the game,
|
||||||
|
* both in gameplay and in menus.
|
||||||
*/
|
*/
|
||||||
class Conductor
|
class Conductor
|
||||||
{
|
{
|
||||||
static final STEPS_PER_BEAT:Int = 4;
|
|
||||||
|
|
||||||
// onBeatHit is called every quarter note
|
// onBeatHit is called every quarter note
|
||||||
// onStepHit is called every sixteenth note
|
// onStepHit is called every sixteenth note
|
||||||
// 4/4 = 4 beats per measure = 16 steps per measure
|
// 4/4 = 4 beats per measure = 16 steps per measure
|
||||||
|
@ -33,11 +28,22 @@ class Conductor
|
||||||
// 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second
|
// 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second
|
||||||
// 7/8 = 3.5 beats per measure = 14 steps per measure
|
// 7/8 = 3.5 beats per measure = 14 steps per measure
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of time changes in the song.
|
||||||
|
* There should be at least one time change (at the beginning of the song) to define the BPM.
|
||||||
|
*/
|
||||||
|
static var timeChanges:Array<SongTimeChange> = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current time change.
|
||||||
|
*/
|
||||||
|
static var currentTimeChange:SongTimeChange;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current position in the song in milliseconds.
|
* The current position in the song in milliseconds.
|
||||||
* Updated every frame based on the audio position.
|
* Updated every frame based on the audio position.
|
||||||
*/
|
*/
|
||||||
public static var songPosition:Float;
|
public static var songPosition:Float = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Beats per minute of the current song at the current time.
|
* Beats per minute of the current song at the current time.
|
||||||
|
@ -48,33 +54,17 @@ class Conductor
|
||||||
{
|
{
|
||||||
if (bpmOverride != null) return bpmOverride;
|
if (bpmOverride != null) return bpmOverride;
|
||||||
|
|
||||||
if (currentTimeChange == null) return 100;
|
if (currentTimeChange == null) return Constants.DEFAULT_BPM;
|
||||||
|
|
||||||
return currentTimeChange.bpm;
|
return currentTimeChange.bpm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current value set by `forceBPM`.
|
||||||
|
* If false, BPM is determined by time changes.
|
||||||
|
*/
|
||||||
static var bpmOverride:Null<Float> = null;
|
static var bpmOverride:Null<Float> = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Current position in the song, in whole measures.
|
|
||||||
*/
|
|
||||||
public static var currentMeasure(default, null):Int;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current position in the song, in whole beats.
|
|
||||||
**/
|
|
||||||
public static var currentBeat(default, null):Int;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current position in the song, in whole steps.
|
|
||||||
*/
|
|
||||||
public static var currentStep(default, null):Int;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current position in the song, in steps and fractions of a step.
|
|
||||||
*/
|
|
||||||
public static var currentStepTime(default, null):Float;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Duration of a measure in milliseconds. Calculated based on bpm.
|
* Duration of a measure in milliseconds. Calculated based on bpm.
|
||||||
*/
|
*/
|
||||||
|
@ -86,119 +76,99 @@ class Conductor
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Duration of a beat (quarter note) in milliseconds. Calculated based on bpm.
|
* Duration of a beat in milliseconds. Calculated based on bpm.
|
||||||
*/
|
*/
|
||||||
public static var beatLengthMs(get, null):Float;
|
public static var beatLengthMs(get, null):Float;
|
||||||
|
|
||||||
static function get_beatLengthMs():Float
|
static function get_beatLengthMs():Float
|
||||||
{
|
{
|
||||||
// Tied directly to BPM.
|
return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC);
|
||||||
return ((60 / bpm) * 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Duration of a step (sixteenth) in milliseconds. Calculated based on bpm.
|
* Duration of a step (quarter) in milliseconds. Calculated based on bpm.
|
||||||
*/
|
*/
|
||||||
public static var stepLengthMs(get, null):Float;
|
public static var stepLengthMs(get, null):Float;
|
||||||
|
|
||||||
static function get_stepLengthMs():Float
|
static function get_stepLengthMs():Float
|
||||||
{
|
{
|
||||||
return beatLengthMs / STEPS_PER_BEAT;
|
return beatLengthMs / timeSignatureNumerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The numerator of the current time signature (number of notes in a measure)
|
|
||||||
*/
|
|
||||||
public static var timeSignatureNumerator(get, null):Int;
|
public static var timeSignatureNumerator(get, null):Int;
|
||||||
|
|
||||||
static function get_timeSignatureNumerator():Int
|
static function get_timeSignatureNumerator():Int
|
||||||
{
|
{
|
||||||
if (currentTimeChange == null) return 4;
|
if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_NUM;
|
||||||
|
|
||||||
return currentTimeChange.timeSignatureNum;
|
return currentTimeChange.timeSignatureNum;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The numerator of the current time signature (length of notes in a measure)
|
|
||||||
*/
|
|
||||||
public static var timeSignatureDenominator(get, null):Int;
|
public static var timeSignatureDenominator(get, null):Int;
|
||||||
|
|
||||||
static function get_timeSignatureDenominator():Int
|
static function get_timeSignatureDenominator():Int
|
||||||
{
|
{
|
||||||
if (currentTimeChange == null) return 4;
|
if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_DEN;
|
||||||
|
|
||||||
return currentTimeChange.timeSignatureDen;
|
return currentTimeChange.timeSignatureDen;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var offset:Float = 0;
|
/**
|
||||||
|
* Current position in the song, in measures.
|
||||||
// TODO: What's the difference between visualOffset and audioOffset?
|
*/
|
||||||
public static var visualOffset:Float = 0;
|
public static var currentMeasure(default, null):Int;
|
||||||
public static var audioOffset:Float = 0;
|
|
||||||
|
|
||||||
//
|
|
||||||
// Signals
|
|
||||||
//
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signal that is dispatched every measure.
|
* Current position in the song, in beats.
|
||||||
* At 120 BPM 4/4, this is dispatched every 2 seconds.
|
|
||||||
* At 120 BPM 3/4, this is dispatched every 1.5 seconds.
|
|
||||||
*/
|
*/
|
||||||
public static var measureHit(default, null):FlxSignal = new FlxSignal();
|
public static var currentBeat(default, null):Int;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signal that is dispatched every beat.
|
* Current position in the song, in steps.
|
||||||
* At 120 BPM 4/4, this is dispatched every 0.5 seconds.
|
|
||||||
* At 120 BPM 3/4, this is dispatched every 0.5 seconds.
|
|
||||||
*/
|
*/
|
||||||
|
public static var currentStep(default, null):Int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current position in the song, in measures and fractions of a measure.
|
||||||
|
*/
|
||||||
|
public static var currentMeasureTime(default, null):Float;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current position in the song, in beats and fractions of a measure.
|
||||||
|
*/
|
||||||
|
public static var currentBeatTime(default, null):Float;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current position in the song, in steps and fractions of a step.
|
||||||
|
*/
|
||||||
|
public static var currentStepTime(default, null):Float;
|
||||||
|
|
||||||
public static var beatHit(default, null):FlxSignal = new FlxSignal();
|
public static var beatHit(default, null):FlxSignal = new FlxSignal();
|
||||||
|
|
||||||
/**
|
|
||||||
* Signal that is dispatched when a step is hit.
|
|
||||||
* At 120 BPM 4/4, this is dispatched every 0.125 seconds.
|
|
||||||
* At 120 BPM 3/4, this is dispatched every 0.125 seconds.
|
|
||||||
*/
|
|
||||||
public static var stepHit(default, null):FlxSignal = new FlxSignal();
|
public static var stepHit(default, null):FlxSignal = new FlxSignal();
|
||||||
|
|
||||||
//
|
|
||||||
// Internal Variables
|
|
||||||
//
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of time changes in the song.
|
|
||||||
* There should be at least one time change (at the beginning of the song) to define the BPM.
|
|
||||||
*/
|
|
||||||
static var timeChanges:Array<SongTimeChange> = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current time change.
|
|
||||||
*/
|
|
||||||
static var currentTimeChange:SongTimeChange;
|
|
||||||
|
|
||||||
public static var lastSongPos:Float;
|
public static var lastSongPos:Float;
|
||||||
|
public static var visualOffset:Float = 0;
|
||||||
|
public static var audioOffset:Float = 0;
|
||||||
|
public static var offset:Float = 0;
|
||||||
|
|
||||||
/**
|
public static var beatsPerMeasure(get, null):Float;
|
||||||
* The number of beats (whole notes) in a measure.
|
|
||||||
*/
|
|
||||||
public static var beatsPerMeasure(get, null):Int;
|
|
||||||
|
|
||||||
static function get_beatsPerMeasure():Int
|
static function get_beatsPerMeasure():Float
|
||||||
{
|
{
|
||||||
return timeSignatureNumerator;
|
// NOTE: Not always an integer, for example 7/8 is 3.5 beats per measure
|
||||||
|
return stepsPerMeasure / Constants.STEPS_PER_BEAT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of steps (quarter-notes) in a measure.
|
|
||||||
*/
|
|
||||||
public static var stepsPerMeasure(get, null):Int;
|
public static var stepsPerMeasure(get, null):Int;
|
||||||
|
|
||||||
static function get_stepsPerMeasure():Int
|
static function get_stepsPerMeasure():Int
|
||||||
{
|
{
|
||||||
// This is always 4, b
|
// TODO: Is this always an integer?
|
||||||
return timeSignatureNumerator * 4;
|
return Std.int(timeSignatureNumerator / timeSignatureDenominator * Constants.STEPS_PER_BEAT * Constants.STEPS_PER_BEAT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function new() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forcibly defines the current BPM of the song.
|
* Forcibly defines the current BPM of the song.
|
||||||
* Useful for things like the chart editor that need to manipulate BPM in real time.
|
* Useful for things like the chart editor that need to manipulate BPM in real time.
|
||||||
|
@ -208,16 +178,11 @@ class Conductor
|
||||||
* WARNING: Avoid this for things like setting the BPM of the title screen music,
|
* WARNING: Avoid this for things like setting the BPM of the title screen music,
|
||||||
* you should have a metadata file for it instead.
|
* you should have a metadata file for it instead.
|
||||||
*/
|
*/
|
||||||
public static function forceBPM(?bpm:Float = null):Void
|
public static function forceBPM(?bpm:Float = null)
|
||||||
{
|
{
|
||||||
if (bpm != null)
|
if (bpm != null) trace('[CONDUCTOR] Forcing BPM to ' + bpm);
|
||||||
{
|
|
||||||
trace('[CONDUCTOR] Forcing BPM to ' + bpm);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
trace('[CONDUCTOR] Resetting BPM to default');
|
trace('[CONDUCTOR] Resetting BPM to default');
|
||||||
}
|
|
||||||
Conductor.bpmOverride = bpm;
|
Conductor.bpmOverride = bpm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,13 +193,12 @@ class Conductor
|
||||||
* @param songPosition The current position in the song in milliseconds.
|
* @param songPosition The current position in the song in milliseconds.
|
||||||
* Leave blank to use the FlxG.sound.music position.
|
* Leave blank to use the FlxG.sound.music position.
|
||||||
*/
|
*/
|
||||||
public static function update(songPosition:Float = null):Void
|
public static function update(songPosition:Float = null)
|
||||||
{
|
{
|
||||||
if (songPosition == null) songPosition = (FlxG.sound.music != null) ? FlxG.sound.music.time + Conductor.offset : 0.0;
|
if (songPosition == null) songPosition = (FlxG.sound.music != null) ? FlxG.sound.music.time + Conductor.offset : 0.0;
|
||||||
|
|
||||||
var oldMeasure:Int = currentMeasure;
|
var oldBeat = currentBeat;
|
||||||
var oldBeat:Int = currentBeat;
|
var oldStep = currentStep;
|
||||||
var oldStep:Int = currentStep;
|
|
||||||
|
|
||||||
Conductor.songPosition = songPosition;
|
Conductor.songPosition = songPosition;
|
||||||
|
|
||||||
|
@ -252,16 +216,23 @@ class Conductor
|
||||||
}
|
}
|
||||||
else if (currentTimeChange != null)
|
else if (currentTimeChange != null)
|
||||||
{
|
{
|
||||||
currentStepTime = (currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepLengthMs;
|
// roundDecimal prevents representing 8 as 7.9999999
|
||||||
|
currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
|
||||||
|
currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
|
||||||
|
currentMeasureTime = currentStepTime / stepsPerMeasure;
|
||||||
currentStep = Math.floor(currentStepTime);
|
currentStep = Math.floor(currentStepTime);
|
||||||
currentBeat = Math.floor(currentStep / 4);
|
currentBeat = Math.floor(currentBeatTime);
|
||||||
|
currentMeasure = Math.floor(currentMeasureTime);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Assume a constant BPM equal to the forced value.
|
// Assume a constant BPM equal to the forced value.
|
||||||
currentStepTime = (songPosition / stepLengthMs);
|
currentStepTime = FlxMath.roundDecimal((songPosition / stepLengthMs), 4);
|
||||||
|
currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
|
||||||
|
currentMeasureTime = currentStepTime / stepsPerMeasure;
|
||||||
currentStep = Math.floor(currentStepTime);
|
currentStep = Math.floor(currentStepTime);
|
||||||
currentBeat = Math.floor(currentStep / 4);
|
currentBeat = Math.floor(currentBeatTime);
|
||||||
|
currentMeasure = Math.floor(currentMeasureTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FlxSignals are really cool.
|
// FlxSignals are really cool.
|
||||||
|
@ -274,31 +245,52 @@ class Conductor
|
||||||
{
|
{
|
||||||
beatHit.dispatch();
|
beatHit.dispatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentMeasure != oldMeasure)
|
|
||||||
{
|
|
||||||
measureHit.dispatch();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function mapTimeChanges(songTimeChanges:Array<SongTimeChange>):Void
|
public static function mapTimeChanges(songTimeChanges:Array<SongTimeChange>)
|
||||||
{
|
{
|
||||||
timeChanges = [];
|
timeChanges = [];
|
||||||
|
|
||||||
for (currentTimeChange in songTimeChanges)
|
for (currentTimeChange in songTimeChanges)
|
||||||
{
|
{
|
||||||
|
// TODO: Maybe handle this different?
|
||||||
|
// Do we care about BPM at negative timestamps?
|
||||||
|
// Without any custom handling, `currentStepTime` becomes non-zero at `songPosition = 0`.
|
||||||
|
if (currentTimeChange.timeStamp < 0.0) currentTimeChange.timeStamp = 0.0;
|
||||||
|
|
||||||
|
if (currentTimeChange.beatTime == null)
|
||||||
|
{
|
||||||
|
if (currentTimeChange.timeStamp <= 0.0)
|
||||||
|
{
|
||||||
|
currentTimeChange.beatTime = 0.0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Calculate the beat time of this timestamp.
|
||||||
|
currentTimeChange.beatTime = 0.0;
|
||||||
|
|
||||||
|
if (currentTimeChange.timeStamp > 0.0 && timeChanges.length > 0)
|
||||||
|
{
|
||||||
|
var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1];
|
||||||
|
currentTimeChange.beatTime = prevTimeChange.beatTime
|
||||||
|
+ ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
timeChanges.push(currentTimeChange);
|
timeChanges.push(currentTimeChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
trace('Done mapping time changes: ' + timeChanges);
|
trace('Done mapping time changes: ' + timeChanges);
|
||||||
|
|
||||||
// Done.
|
// Update currentStepTime
|
||||||
|
Conductor.update(Conductor.songPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a time in milliseconds, return a time in steps.
|
* Given a time in milliseconds, return a time in steps.
|
||||||
*/
|
*/
|
||||||
public static function getTimeInSteps(ms:Float):Int
|
public static function getTimeInSteps(ms:Float):Float
|
||||||
{
|
{
|
||||||
if (timeChanges.length == 0)
|
if (timeChanges.length == 0)
|
||||||
{
|
{
|
||||||
|
@ -307,7 +299,7 @@ class Conductor
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var resultStep:Int = 0;
|
var resultStep:Float = 0;
|
||||||
|
|
||||||
var lastTimeChange:SongTimeChange = timeChanges[0];
|
var lastTimeChange:SongTimeChange = timeChanges[0];
|
||||||
for (timeChange in timeChanges)
|
for (timeChange in timeChanges)
|
||||||
|
@ -329,4 +321,14 @@ class Conductor
|
||||||
return resultStep;
|
return resultStep;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function reset():Void
|
||||||
|
{
|
||||||
|
beatHit.removeAll();
|
||||||
|
stepHit.removeAll();
|
||||||
|
|
||||||
|
mapTimeChanges([]);
|
||||||
|
forceBPM(null);
|
||||||
|
update(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,9 +67,11 @@ class MusicBeatState extends FlxUIState implements IEventHandler
|
||||||
if (FlxG.keys.justPressed.F5) debug_refreshModules();
|
if (FlxG.keys.justPressed.F5) debug_refreshModules();
|
||||||
|
|
||||||
// Display Conductor info in the watch window.
|
// Display Conductor info in the watch window.
|
||||||
FlxG.watch.addQuick("songPos", Conductor.songPosition);
|
FlxG.watch.addQuick("songPosition", Conductor.songPosition);
|
||||||
FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime);
|
|
||||||
FlxG.watch.addQuick("bpm", Conductor.bpm);
|
FlxG.watch.addQuick("bpm", Conductor.bpm);
|
||||||
|
FlxG.watch.addQuick("currentMeasureTime", Conductor.currentBeatTime);
|
||||||
|
FlxG.watch.addQuick("currentBeatTime", Conductor.currentBeatTime);
|
||||||
|
FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime);
|
||||||
|
|
||||||
dispatchEvent(new UpdateScriptEvent(elapsed));
|
dispatchEvent(new UpdateScriptEvent(elapsed));
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package funkin;
|
||||||
import flixel.FlxSubState;
|
import flixel.FlxSubState;
|
||||||
import funkin.modding.IScriptedClass.IEventHandler;
|
import funkin.modding.IScriptedClass.IEventHandler;
|
||||||
import flixel.util.FlxColor;
|
import flixel.util.FlxColor;
|
||||||
import funkin.Conductor.BPMChangeEvent;
|
|
||||||
import funkin.modding.events.ScriptEvent;
|
import funkin.modding.events.ScriptEvent;
|
||||||
import funkin.modding.module.ModuleHandler;
|
import funkin.modding.module.ModuleHandler;
|
||||||
import flixel.text.FlxText;
|
import flixel.text.FlxText;
|
||||||
|
|
|
@ -6,7 +6,8 @@ import openfl.utils.Assets as OpenFlAssets;
|
||||||
|
|
||||||
class Paths
|
class Paths
|
||||||
{
|
{
|
||||||
inline public static var SOUND_EXT = #if web "mp3" #else "ogg" #end;
|
public static var SOUND_EXT = #if web "mp3" #else "ogg" #end;
|
||||||
|
public static var VIDEO_EXT = "mp4";
|
||||||
|
|
||||||
static var currentLevel:String;
|
static var currentLevel:String;
|
||||||
|
|
||||||
|
@ -96,14 +97,19 @@ class Paths
|
||||||
return getPath('music/$key.$SOUND_EXT', MUSIC, library);
|
return getPath('music/$key.$SOUND_EXT', MUSIC, library);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline static public function voices(song:String, ?suffix:String)
|
inline static public function videos(key:String, ?library:String)
|
||||||
|
{
|
||||||
|
return getPath('videos/$key.$VIDEO_EXT', BINARY, library);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static public function voices(song:String, ?suffix:String = '')
|
||||||
{
|
{
|
||||||
if (suffix == null) suffix = ""; // no suffix, for a sorta backwards compatibility with older-ish voice files
|
if (suffix == null) suffix = ""; // no suffix, for a sorta backwards compatibility with older-ish voice files
|
||||||
|
|
||||||
return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.$SOUND_EXT';
|
return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.$SOUND_EXT';
|
||||||
}
|
}
|
||||||
|
|
||||||
inline static public function inst(song:String, ?suffix:String)
|
inline static public function inst(song:String, ?suffix:String = '')
|
||||||
{
|
{
|
||||||
return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.$SOUND_EXT';
|
return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.$SOUND_EXT';
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import funkin.play.PlayStatePlaylist;
|
||||||
import flixel.FlxSprite;
|
import flixel.FlxSprite;
|
||||||
import flixel.addons.transition.FlxTransitionableState;
|
import flixel.addons.transition.FlxTransitionableState;
|
||||||
import flixel.group.FlxGroup.FlxTypedGroup;
|
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||||
import flixel.system.FlxSound;
|
import flixel.sound.FlxSound;
|
||||||
import flixel.text.FlxText;
|
import flixel.text.FlxText;
|
||||||
import flixel.tweens.FlxEase;
|
import flixel.tweens.FlxEase;
|
||||||
import flixel.tweens.FlxTween;
|
import flixel.tweens.FlxTween;
|
||||||
|
|
|
@ -12,6 +12,8 @@ import flixel.util.FlxTimer;
|
||||||
import funkin.audiovis.SpectogramSprite;
|
import funkin.audiovis.SpectogramSprite;
|
||||||
import funkin.shaderslmfao.ColorSwap;
|
import funkin.shaderslmfao.ColorSwap;
|
||||||
import funkin.shaderslmfao.LeftMaskShader;
|
import funkin.shaderslmfao.LeftMaskShader;
|
||||||
|
import funkin.play.song.SongData.SongDataParser;
|
||||||
|
import funkin.play.song.SongData.SongMetadata;
|
||||||
import funkin.shaderslmfao.TitleOutline;
|
import funkin.shaderslmfao.TitleOutline;
|
||||||
import funkin.ui.AtlasText;
|
import funkin.ui.AtlasText;
|
||||||
import funkin.util.Constants;
|
import funkin.util.Constants;
|
||||||
|
@ -135,12 +137,7 @@ class TitleState extends MusicBeatState
|
||||||
|
|
||||||
function startIntro()
|
function startIntro()
|
||||||
{
|
{
|
||||||
if (FlxG.sound.music == null || !FlxG.sound.music.playing)
|
playMenuMusic();
|
||||||
{
|
|
||||||
FlxG.sound.playMusic(Paths.music('freakyMenu'), 0);
|
|
||||||
FlxG.sound.music.fadeIn(4, 0, 0.7);
|
|
||||||
Conductor.forceBPM(Constants.FREAKY_MENU_BPM);
|
|
||||||
}
|
|
||||||
|
|
||||||
persistentUpdate = true;
|
persistentUpdate = true;
|
||||||
|
|
||||||
|
@ -234,6 +231,18 @@ class TitleState extends MusicBeatState
|
||||||
if (FlxG.sound.music != null) FlxG.sound.music.onComplete = function() FlxG.switchState(new VideoState());
|
if (FlxG.sound.music != null) FlxG.sound.music.onComplete = function() FlxG.switchState(new VideoState());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function playMenuMusic():Void
|
||||||
|
{
|
||||||
|
if (FlxG.sound.music == null || !FlxG.sound.music.playing)
|
||||||
|
{
|
||||||
|
var freakyMenuMetadata:SongMetadata = SongDataParser.parseMusicMetadata('freakyMenu');
|
||||||
|
Conductor.mapTimeChanges(freakyMenuMetadata.timeChanges);
|
||||||
|
|
||||||
|
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0);
|
||||||
|
FlxG.sound.music.fadeIn(4, 0, 0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getIntroTextShit():Array<Array<String>>
|
function getIntroTextShit():Array<Array<String>>
|
||||||
{
|
{
|
||||||
var fullText:String = Assets.getText(Paths.txt('introText'));
|
var fullText:String = Assets.getText(Paths.txt('introText'));
|
||||||
|
|
|
@ -3,7 +3,7 @@ package funkin.play;
|
||||||
import flixel.FlxG;
|
import flixel.FlxG;
|
||||||
import flixel.FlxObject;
|
import flixel.FlxObject;
|
||||||
import flixel.FlxSprite;
|
import flixel.FlxSprite;
|
||||||
import flixel.system.FlxSound;
|
import flixel.sound.FlxSound;
|
||||||
import funkin.ui.story.StoryMenuState;
|
import funkin.ui.story.StoryMenuState;
|
||||||
import flixel.util.FlxColor;
|
import flixel.util.FlxColor;
|
||||||
import flixel.util.FlxTimer;
|
import flixel.util.FlxTimer;
|
||||||
|
|
|
@ -894,7 +894,7 @@ class PlayState extends MusicBeatState
|
||||||
trace('Song difficulty could not be loaded.');
|
trace('Song difficulty could not be loaded.');
|
||||||
}
|
}
|
||||||
|
|
||||||
Conductor.forceBPM(currentChart.getStartingBPM());
|
// Conductor.forceBPM(currentChart.getStartingBPM());
|
||||||
|
|
||||||
vocals = currentChart.buildVocals(currentPlayerId);
|
vocals = currentChart.buildVocals(currentPlayerId);
|
||||||
if (vocals.members.length == 0)
|
if (vocals.members.length == 0)
|
||||||
|
@ -1208,13 +1208,10 @@ class PlayState extends MusicBeatState
|
||||||
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95);
|
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
FlxG.watch.addQuick('beatShit', Conductor.currentBeat);
|
|
||||||
FlxG.watch.addQuick('stepShit', Conductor.currentStep);
|
|
||||||
if (currentStage != null)
|
if (currentStage != null)
|
||||||
{
|
{
|
||||||
FlxG.watch.addQuick('bfAnim', currentStage.getBoyfriend().getCurrentAnimation());
|
FlxG.watch.addQuick('bfAnim', currentStage.getBoyfriend().getCurrentAnimation());
|
||||||
}
|
}
|
||||||
FlxG.watch.addQuick('songPos', Conductor.songPosition);
|
|
||||||
|
|
||||||
// Handle GF dance speed.
|
// Handle GF dance speed.
|
||||||
// TODO: Add a song event for this.
|
// TODO: Add a song event for this.
|
||||||
|
|
|
@ -30,7 +30,8 @@ class VideoCutscene
|
||||||
|
|
||||||
if (!openfl.Assets.exists(filePath))
|
if (!openfl.Assets.exists(filePath))
|
||||||
{
|
{
|
||||||
trace('ERROR: Video file does not exist: ${filePath}');
|
// Display a popup.
|
||||||
|
lime.app.Application.current.window.alert('Video file does not exist: ${filePath}', 'Error playing video');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ package funkin.play.song;
|
||||||
import funkin.modding.events.ScriptEvent;
|
import funkin.modding.events.ScriptEvent;
|
||||||
import funkin.modding.events.ScriptEventDispatcher;
|
import funkin.modding.events.ScriptEventDispatcher;
|
||||||
import flixel.util.typeLimit.OneOfTwo;
|
import flixel.util.typeLimit.OneOfTwo;
|
||||||
|
import funkin.modding.events.ScriptEvent;
|
||||||
|
import funkin.modding.events.ScriptEventDispatcher;
|
||||||
import funkin.play.song.ScriptedSong;
|
import funkin.play.song.ScriptedSong;
|
||||||
import funkin.util.assets.DataAssets;
|
import funkin.util.assets.DataAssets;
|
||||||
import haxe.DynamicAccess;
|
import haxe.DynamicAccess;
|
||||||
|
@ -22,6 +24,7 @@ class SongDataParser
|
||||||
|
|
||||||
static final DEFAULT_SONG_ID:String = 'UNKNOWN';
|
static final DEFAULT_SONG_ID:String = 'UNKNOWN';
|
||||||
static final SONG_DATA_PATH:String = 'songs/';
|
static final SONG_DATA_PATH:String = 'songs/';
|
||||||
|
static final MUSIC_DATA_PATH:String = 'music/';
|
||||||
static final SONG_DATA_SUFFIX:String = '-metadata.json';
|
static final SONG_DATA_SUFFIX:String = '-metadata.json';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -181,6 +184,36 @@ class SongDataParser
|
||||||
return rawJson;
|
return rawJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function parseMusicMetadata(musicId:String):SongMetadata
|
||||||
|
{
|
||||||
|
var rawJson:String = loadMusicMetadataFile(musicId);
|
||||||
|
var jsonData:Dynamic = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
jsonData = Json.parse(rawJson);
|
||||||
|
}
|
||||||
|
catch (e) {}
|
||||||
|
|
||||||
|
var musicMetadata:SongMetadata = SongMigrator.migrateSongMetadata(jsonData, musicId);
|
||||||
|
musicMetadata = SongValidator.validateSongMetadata(musicMetadata, musicId);
|
||||||
|
|
||||||
|
return musicMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function loadMusicMetadataFile(musicPath:String, variation:String = ''):String
|
||||||
|
{
|
||||||
|
var musicMetadataFilePath:String = (variation != '') ? Paths.file('$MUSIC_DATA_PATH$musicPath/$musicPath-metadata-$variation.json') : Paths.file('$MUSIC_DATA_PATH$musicPath/$musicPath-metadata.json');
|
||||||
|
|
||||||
|
var rawJson:String = Assets.getText(musicMetadataFilePath).trim();
|
||||||
|
|
||||||
|
while (!rawJson.endsWith("}"))
|
||||||
|
{
|
||||||
|
rawJson = rawJson.substr(0, rawJson.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawJson;
|
||||||
|
}
|
||||||
|
|
||||||
public static function parseSongChartData(songId:String, variation:String = ""):SongChartData
|
public static function parseSongChartData(songId:String, variation:String = ""):SongChartData
|
||||||
{
|
{
|
||||||
var rawJson:String = loadSongChartDataFile(songId, variation);
|
var rawJson:String = loadSongChartDataFile(songId, variation);
|
||||||
|
@ -369,8 +402,7 @@ abstract SongNoteData(RawSongNoteData)
|
||||||
|
|
||||||
public function get_stepTime():Float
|
public function get_stepTime():Float
|
||||||
{
|
{
|
||||||
// TODO: Account for changes in BPM.
|
return Conductor.getTimeInSteps(this.t);
|
||||||
return this.t / Conductor.stepLengthMs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -555,8 +587,7 @@ abstract SongEventData(RawSongEventData)
|
||||||
|
|
||||||
public function get_stepTime():Float
|
public function get_stepTime():Float
|
||||||
{
|
{
|
||||||
// TODO: Account for changes in BPM.
|
return Conductor.getTimeInSteps(this.t);
|
||||||
return this.t / Conductor.stepLengthMs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public var event(get, set):String;
|
public var event(get, set):String;
|
||||||
|
@ -769,7 +800,7 @@ typedef RawSongTimeChange =
|
||||||
* Time in beats (int). The game will calculate further beat values based on this one,
|
* Time in beats (int). The game will calculate further beat values based on this one,
|
||||||
* so it can do it in a simple linear fashion.
|
* so it can do it in a simple linear fashion.
|
||||||
*/
|
*/
|
||||||
var b:Int;
|
var b:Null<Float>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Quarter notes per minute (float). Cannot be empty in the first element of the list,
|
* Quarter notes per minute (float). Cannot be empty in the first element of the list,
|
||||||
|
@ -799,9 +830,9 @@ typedef RawSongTimeChange =
|
||||||
* Add aliases to the minimalized property names of the typedef,
|
* Add aliases to the minimalized property names of the typedef,
|
||||||
* to improve readability.
|
* to improve readability.
|
||||||
*/
|
*/
|
||||||
abstract SongTimeChange(RawSongTimeChange)
|
abstract SongTimeChange(RawSongTimeChange) from RawSongTimeChange
|
||||||
{
|
{
|
||||||
public function new(timeStamp:Float, beatTime:Int, bpm:Float, timeSignatureNum:Int = 4, timeSignatureDen:Int = 4, beatTuplets:Array<Int>)
|
public function new(timeStamp:Float, beatTime:Null<Float>, bpm:Float, timeSignatureNum:Int = 4, timeSignatureDen:Int = 4, beatTuplets:Array<Int>)
|
||||||
{
|
{
|
||||||
this =
|
this =
|
||||||
{
|
{
|
||||||
|
@ -826,14 +857,14 @@ abstract SongTimeChange(RawSongTimeChange)
|
||||||
return this.t = value;
|
return this.t = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public var beatTime(get, set):Int;
|
public var beatTime(get, set):Null<Float>;
|
||||||
|
|
||||||
public function get_beatTime():Int
|
public function get_beatTime():Null<Float>
|
||||||
{
|
{
|
||||||
return this.b;
|
return this.b;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function set_beatTime(value:Int):Int
|
public function set_beatTime(value:Null<Float>):Null<Float>
|
||||||
{
|
{
|
||||||
return this.b = value;
|
return this.b = value;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1877,7 +1877,7 @@ class ChartEditorState extends HaxeUIState
|
||||||
{
|
{
|
||||||
// Handle extending the note as you drag.
|
// Handle extending the note as you drag.
|
||||||
|
|
||||||
// Since use Math.floor and stepCrochet here, the hold notes will be beat snapped.
|
// Since use Math.floor and stepLengthMs here, the hold notes will be beat snapped.
|
||||||
var dragLengthSteps:Float = Math.floor((cursorMs - currentPlaceNoteData.time) / Conductor.stepLengthMs);
|
var dragLengthSteps:Float = Math.floor((cursorMs - currentPlaceNoteData.time) / Conductor.stepLengthMs);
|
||||||
|
|
||||||
// Without this, the newly placed note feels too short compared to the user's input.
|
// Without this, the newly placed note feels too short compared to the user's input.
|
||||||
|
|
|
@ -15,6 +15,7 @@ import funkin.modding.events.ScriptEventDispatcher;
|
||||||
import funkin.play.PlayState;
|
import funkin.play.PlayState;
|
||||||
import funkin.play.PlayStatePlaylist;
|
import funkin.play.PlayStatePlaylist;
|
||||||
import funkin.play.song.Song;
|
import funkin.play.song.Song;
|
||||||
|
import funkin.play.song.SongData.SongMetadata;
|
||||||
import funkin.play.song.SongData.SongDataParser;
|
import funkin.play.song.SongData.SongDataParser;
|
||||||
import funkin.util.Constants;
|
import funkin.util.Constants;
|
||||||
|
|
||||||
|
@ -115,12 +116,7 @@ class StoryMenuState extends MusicBeatState
|
||||||
transIn = FlxTransitionableState.defaultTransIn;
|
transIn = FlxTransitionableState.defaultTransIn;
|
||||||
transOut = FlxTransitionableState.defaultTransOut;
|
transOut = FlxTransitionableState.defaultTransOut;
|
||||||
|
|
||||||
if (!FlxG.sound.music.playing)
|
playMenuMusic();
|
||||||
{
|
|
||||||
FlxG.sound.playMusic(Paths.music('freakyMenu'));
|
|
||||||
FlxG.sound.music.fadeIn(4, 0, 0.7);
|
|
||||||
}
|
|
||||||
Conductor.forceBPM(Constants.FREAKY_MENU_BPM);
|
|
||||||
|
|
||||||
if (stickerSubState != null)
|
if (stickerSubState != null)
|
||||||
{
|
{
|
||||||
|
@ -129,8 +125,6 @@ class StoryMenuState extends MusicBeatState
|
||||||
|
|
||||||
openSubState(stickerSubState);
|
openSubState(stickerSubState);
|
||||||
stickerSubState.degenStickers();
|
stickerSubState.degenStickers();
|
||||||
|
|
||||||
// resetSubState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
persistentUpdate = persistentDraw = true;
|
persistentUpdate = persistentDraw = true;
|
||||||
|
@ -203,6 +197,18 @@ class StoryMenuState extends MusicBeatState
|
||||||
#end
|
#end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function playMenuMusic():Void
|
||||||
|
{
|
||||||
|
if (FlxG.sound.music == null || !FlxG.sound.music.playing)
|
||||||
|
{
|
||||||
|
var freakyMenuMetadata:SongMetadata = SongDataParser.parseMusicMetadata('freakyMenu');
|
||||||
|
Conductor.mapTimeChanges(freakyMenuMetadata.timeChanges);
|
||||||
|
|
||||||
|
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0);
|
||||||
|
FlxG.sound.music.fadeIn(4, 0, 0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateData():Void
|
function updateData():Void
|
||||||
{
|
{
|
||||||
currentLevel = LevelRegistry.instance.fetchEntry(currentLevelId);
|
currentLevel = LevelRegistry.instance.fetchEntry(currentLevelId);
|
||||||
|
|
|
@ -118,27 +118,72 @@ class Constants
|
||||||
public static final DEFAULT_SONG:String = 'tutorial';
|
public static final DEFAULT_SONG:String = 'tutorial';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OTHER
|
* TIMING
|
||||||
*/
|
*/
|
||||||
// ==============================
|
// ==============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of seconds in a minute.
|
||||||
|
*/
|
||||||
|
public static final SECS_PER_MIN:Int = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of milliseconds in a second.
|
||||||
|
*/
|
||||||
|
public static final MS_PER_SEC:Int = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of microseconds in a millisecond.
|
||||||
|
*/
|
||||||
|
public static final US_PER_MS:Int = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of microseconds in a second.
|
||||||
|
*/
|
||||||
|
public static final US_PER_SEC:Int = US_PER_MS * MS_PER_SEC;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of nanoseconds in a microsecond.
|
||||||
|
*/
|
||||||
|
public static final NS_PER_US:Int = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of nanoseconds in a millisecond.
|
||||||
|
*/
|
||||||
|
public static final NS_PER_MS:Int = NS_PER_US * US_PER_MS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of nanoseconds in a second.
|
||||||
|
*/
|
||||||
|
public static final NS_PER_SEC:Int = NS_PER_US * US_PER_MS * MS_PER_SEC;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All MP3 decoders introduce a playback delay of `528` samples,
|
* All MP3 decoders introduce a playback delay of `528` samples,
|
||||||
* which at 44,100 Hz (samples per second) is ~12 ms.
|
* which at 44,100 Hz (samples per second) is ~12 ms.
|
||||||
*/
|
*/
|
||||||
public static final MP3_DELAY_MS:Float = 528 / 44100 * 1000;
|
public static final MP3_DELAY_MS:Float = 528 / 44100 * MS_PER_SEC;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default BPM of the conductor.
|
||||||
|
*/
|
||||||
|
public static final DEFAULT_BPM:Float = 100.0;
|
||||||
|
|
||||||
|
public static final DEFAULT_TIME_SIGNATURE_NUM:Int = 4;
|
||||||
|
|
||||||
|
public static final DEFAULT_TIME_SIGNATURE_DEN:Int = 4;
|
||||||
|
|
||||||
|
public static final STEPS_PER_BEAT:Int = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTHER
|
||||||
|
*/
|
||||||
|
// ==============================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The scale factor to use when increasing the size of pixel art graphics.
|
* The scale factor to use when increasing the size of pixel art graphics.
|
||||||
*/
|
*/
|
||||||
public static final PIXEL_ART_SCALE:Float = 6;
|
public static final PIXEL_ART_SCALE:Float = 6;
|
||||||
|
|
||||||
/**
|
|
||||||
* The BPM of the title screen and menu music.
|
|
||||||
* TODO: Move to metadata file.
|
|
||||||
*/
|
|
||||||
public static final FREAKY_MENU_BPM:Float = 102;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The volume at which to play the countdown before the song starts.
|
* The volume at which to play the countdown before the song starts.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in a new issue