Merge branch 'new-conductor' into new-editors-conductor

# Conflicts:
#	source/funkin/editors/charter/Charter.hx
#	source/funkin/editors/charter/CharterBackdropGroup.hx
#	source/funkin/editors/charter/CharterEvent.hx
This commit is contained in:
Ralty
2025-07-19 19:17:27 +07:00
19 changed files with 1295 additions and 345 deletions
-1
View File
@@ -157,7 +157,6 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
if(!animEnabled) return;
if (lastAnimContext != LOCK && beatAnims.length > 0 && (curBeat + beatOffset) % beatInterval == 0)
{
if(skipNegativeBeats && curBeat < 0) return;
// TODO: find a solution without countedBeat
var anim = beatAnims[FlxMath.wrap(countedBeat++, 0, beatAnims.length - 1)];
if (anim.name != null && anim.name != "null" && anim.name != "none")
+8 -4
View File
@@ -10,7 +10,7 @@ import openfl.Assets;
using StringTools;
class EventsData {
public static var defaultEventsList:Array<String> = ["HScript Call", "Camera Movement", "Camera Position", "Add Camera Zoom", "Camera Zoom", "Camera Modulo Change", "Camera Flash", "BPM Change", "Scroll Speed Change", "Alt Animation Toggle", "Play Animation"];
public static var defaultEventsList:Array<String> = ["HScript Call", "Camera Movement", "Camera Position", "Add Camera Zoom", "Camera Zoom", "Camera Modulo Change", "Camera Flash", "BPM Change", "Continuous BPM Change", "Time Signature Change", "Scroll Speed Change", "Alt Animation Toggle", "Play Animation"];
public static var defaultEventsParams:Map<String, Array<EventParamInfo>> = [
"HScript Call" => [
{name: "Function Name", type: TString, defValue: "myFunc"},
@@ -75,8 +75,10 @@ class EventsData {
{name: "Multiplicative?", type: TBool, defValue: true}
],
"Camera Modulo Change" => [
{name: "Modulo Interval (Beats)", type: TInt(1, 9999999, 1), defValue: 4},
{name: "Bump Strength", type: TFloat(0.1, 10, 0.01, 2), defValue: 1}
{name: "Modulo Interval", type: TInt(1, 9999999, 1), defValue: 4},
{name: "Bump Strength", type: TFloat(0.1, 10, 0.01, 2), defValue: 1},
{name: "Every Beat Type", type: TDropDown(['BEAT', 'MEASURE', 'STEP']), defValue: 'BEAT'},
{name: "Beat Offset", type: TFloat(-10, 10, 0.25, 2), defValue: 0}
],
"Camera Flash" => [
{name: "Reversed?", type: TBool, defValue: false},
@@ -84,7 +86,9 @@ class EventsData {
{name: "Time (Steps)", type: TFloat(0.25, 9999, 0.25, 2), defValue: 4},
{name: "Camera", type: TDropDown(['camGame', 'camHUD']), defValue: "camHUD"}
],
"BPM Change" => [{name: "Target BPM", type: TFloat(1.00, null, 0.001, 3), defValue: 100}],
"BPM Change" => [{name: "Target BPM", type: TFloat(1.00, 9999, 0.001, 3), defValue: 100}],
"Continuous BPM Change" => [{name: "Target BPM", type: TFloat(1.00, 9999, 0.001, 3), defValue: 100}, {name: "Time (steps)", type: TFloat(0.25, 9999, 0.25, 2), defValue: 4}],
"Time Signature Change" => [{name: "Target Numerator", type: TFloat(1), defValue: 4}, {name: "Target Denominator", type: TFloat(1), defValue: 4}, {name: "Denominator is Steps Per Beat", type: TBool, defValue: false}],
"Scroll Speed Change" => [
{name: "Tween Speed?", type: TBool, defValue: true},
{name: "New Speed", type: TFloat(0.01, 99, 0.01, 2), defValue: 1.},
+16 -1
View File
@@ -55,6 +55,18 @@ class FNFLegacyParser {
continue; // Yoshi Engine charts crash fix
}
var newBeatsPerMeasure:Float = section.sectionBeats != null ? section.sectionBeats : data.beatsPerMeasure.getDefault(4); // Default to 4 if sectionBeats is null or undefined (oops :3)
if (newBeatsPerMeasure != beatsPerMeasure) {
beatsPerMeasure = newBeatsPerMeasure;
result.events.push({
time: curTime,
name: "Time Signature Change",
params: [newBeatsPerMeasure, 4]
});
}
if (camFocusedBF != (camFocusedBF = section.mustHitSection)) {
result.events.push({
time: curTime,
@@ -178,7 +190,8 @@ class FNFLegacyParser {
mustHitSection: notes[section-1] != null ? notes[section-1].mustHitSection : false,
bpm: notes[section-1] != null ? notes[section-1].bpm : chart.meta.bpm,
changeBPM: false,
altAnim: notes[section-1] != null ? notes[section-1].altAnim : false
altAnim: notes[section-1] != null ? notes[section-1].altAnim : false,
sectionBeats: notes[section-1] != null ? notes[section-1].sectionBeats : chart.meta.beatsPerMeasure.getDefault(4)
};
var sectionEndTime:Float = Conductor.getTimeForStep(Conductor.getMeasureLength() * (section+1));
@@ -192,6 +205,8 @@ class FNFLegacyParser {
case "BPM Change":
baseSection.changeBPM = true;
baseSection.bpm = event.params[0];
case "Time Signature Change":
baseSection.sectionBeats = event.params[0];
}
}
notes[section] = baseSection;
+472 -171
View File
@@ -4,56 +4,58 @@ import flixel.FlxState;
import flixel.util.FlxSignal.FlxTypedSignal;
import funkin.backend.chart.ChartData;
import funkin.backend.system.interfaces.IBeatReceiver;
import funkin.editors.charter.Charter;
typedef BPMChangeEvent =
{
var stepTime:Float;
var songTime:Float;
var bpm:Float;
enum BeatType {
BEAT;
MEASURE;
STEP;
}
class Conductor
@:structInit
class BPMChangeEvent
{
public var songTime:Float;
public var bpm:Float;
public var beatsPerMeasure:Float = 4;
public var stepsPerBeat:Int = 4;
public var endSongTime:Float = 0;
public var endStepTime:Float = 0;
public var continuous:Bool = false;
public var stepTime:Float;
public var beatTime:Float;
public var measureTime:Float;
}
final class Conductor
{
public static function getBeats(?every:BeatType, interval:Float, offset:Float = 0):Float {
final beat = switch(every) {
case MEASURE: curMeasureFloat;
case STEP: curStepFloat;
default: curBeatFloat;
}
if (interval <= 0) return beat - offset;
else return Math.floor((beat - offset) / interval) * interval;
}
/**
* FlxSignals
*/
public static var onMeasureHit:FlxTypedSignal<Int->Void> = new FlxTypedSignal();
public static var onBeatHit:FlxTypedSignal<Int->Void> = new FlxTypedSignal();
public static var onStepHit:FlxTypedSignal<Int->Void> = new FlxTypedSignal();
public static var onBPMChange:FlxTypedSignal<Float->Void> = new FlxTypedSignal();
/**
* Current BPM
*/
public static var bpm:Float = 100;
/**
* Current Crochet (time per beat), in milliseconds.
*/
public static var crochet:Float = ((60 / bpm) * 1000); // beats in milliseconds
/**
* Current StepCrochet (time per step), in milliseconds.
*/
public static var stepCrochet:Float = crochet / 4; // steps in milliseconds
/**
* Number of beats per measure (top number in time signature). Defaults to 4.
*/
public static var beatsPerMeasure:Float = 4;
/**
* Number of steps per beat (bottom number in time signature). Defaults to 4.
*/
public static var stepsPerBeat:Int = 4;
public static var onBPMChange:FlxTypedSignal<(Float,Float)->Void> = new FlxTypedSignal();
public static var onTimeSignatureChange:FlxTypedSignal<(Float,Float)->Void> = new FlxTypedSignal();
/**
* Current position of the song, in milliseconds.
*/
public static var songPosition(get, default):Float;
private static function get_songPosition() {
if (songOffset != Options.songOffset) trace(songOffset = Options.songOffset);
if (songOffset != Options.songOffset) songOffset = Options.songOffset;
return songPosition - songOffset;
}
@@ -62,6 +64,88 @@ class Conductor
*/
public static var songOffset:Float = 0;
/**
* Current bpmChangeMap index
*/
public static var curChangeIndex:Int = 0;
/**
* Current bpmChangeMap
*/
public static var curChange(get, never):BPMChangeEvent;
private static var dummyChange:BPMChangeEvent = {bpm: 100, beatsPerMeasure: 4, stepsPerBeat: 4, songTime: 0, stepTime: 0, beatTime: 0, measureTime: 0};
private static function get_curChange()
return bpmChangeMap.length == 0 ? dummyChange : bpmChangeMap[curChangeIndex];
/**
* Current BPM
*/
public static var bpm(get, never):Float;
private static function get_bpm()
return (curChangeIndex == 0 || bpmChangeMap.length == 0) ? startingBPM : getTimeWithIndexInBPM(songPosition, curChangeIndex);
/**
* Starting BPM
*/
public static var startingBPM(get, never):Float;
private static function get_startingBPM()
return (bpmChangeMap.length == 0 ? dummyChange : bpmChangeMap[0]).bpm;
/**
* Current Crochet (time per beat), in milliseconds.
* It should be crotchet but ehhh, now it's there for backward compatibility.
*/
public static var crochet(get, never):Float;
private static function get_crochet() return 15000 * stepsPerBeat / bpm;
/**
* Current StepCrochet (time per step), in milliseconds.
*/
public static var stepCrochet(get, never):Float;
private static function get_stepCrochet() return 15000 / bpm;
/**
* Number of beats per mesure (top number in time signature). Defaults to 4.
*/
public static var beatsPerMeasure(get, never):Float;
private static function get_beatsPerMeasure()
return (bpmChangeMap.length == 0 ? dummyChange : bpmChangeMap[curChangeIndex]).beatsPerMeasure;
/**
* Number of steps per beat. Defaults to 4.
* Not a divisor number for time signature, it does the complete opposite.
* It's because CNE Conductor is based in sixteenth note instead of beat.
*/
public static var stepsPerBeat(get, never):Int;
private static function get_stepsPerBeat()
return (bpmChangeMap.length == 0 ? dummyChange : bpmChangeMap[curChangeIndex]).stepsPerBeat;
/**
* How much value notes to divide for beat (bottom or divisor number in time signature).
* Only for a convinient way to access divisor instead of multiply by steps per beat.
*/
public static var denominator(get, never):Float;
private static function get_denominator() return FlxMath.roundDecimal(16 / stepsPerBeat, 2);
/**
* Last step from BPM Change
*/
public static var lastStepChange(get, never):Float;
private static function get_lastStepChange() return curChange.stepTime;
/**
* Last beat from BPM Change
*/
public static var lastBeatChange(get, never):Float;
private static function get_lastBeatChange() return curChange.beatTime;
/**
* Last measure from BPM Change
*/
public static var lastMeasureChange(get, never):Float;
private static function get_lastMeasureChange() return curChange.measureTime;
/**
* Current step
*/
@@ -77,7 +161,6 @@ class Conductor
*/
public static var curMeasure:Int = 0;
/**
* Current step, as a `Float` (ex: 4.94, instead of 4)
*/
@@ -100,54 +183,164 @@ class Conductor
/**
* Array of all BPM changes that have been mapped.
*/
public static var bpmChangeMap:Array<BPMChangeEvent> = [];
public static var bpmChangeMap:Array<BPMChangeEvent>;
/**
* Array of all events that have been rejected by the Conductor.
*/
public static var invalidEvents:Array<ChartEvent> = [];
private static var validEventNames:Array<String> = ["BPM Change", "Time Signature Change", "Continuous BPM Change"];
@:dox(hide) public function new() {}
public static function reset() {
songPosition = lastSongPos = curBeatFloat = curStepFloat = curBeat = curStep = 0;
bpmChangeMap = [];
changeBPM(0);
curChangeIndex = 0;
changeBPM();
}
public static function changeBPM(bpm:Float = 100, beatsPerMeasure:Float = 4, stepsPerBeat:Int = 4)
bpmChangeMap = [{bpm: bpm, beatsPerMeasure: beatsPerMeasure, stepsPerBeat: stepsPerBeat, songTime: 0, stepTime: 0, beatTime: 0, measureTime: 0}];
public static function setupSong(SONG:ChartData) {
reset();
mapBPMChanges(SONG);
changeBPM(SONG.meta.bpm, cast SONG.meta.beatsPerMeasure.getDefault(4), cast SONG.meta.stepsPerBeat.getDefault(4));
}
private static function mapBPMChange(curChange:BPMChangeEvent, time:Float, bpm:Float):BPMChangeEvent {
var beatTime:Float, measureTime:Float, stepTime:Float;
stepTime = (curChange.continuous ? curChange.endStepTime : curChange.stepTime) + (time - (curChange.continuous ? curChange.endSongTime : curChange.songTime)) / (15000 / curChange.bpm);
beatTime = curChange.beatTime + (stepTime - curChange.stepTime) / curChange.stepsPerBeat;
measureTime = curChange.measureTime + (beatTime - curChange.beatTime) / curChange.beatsPerMeasure;
bpmChangeMap.push(curChange = {
songTime: time,
stepTime: stepTime,
beatTime: beatTime,
measureTime: measureTime,
bpm: bpm,
beatsPerMeasure: curChange.beatsPerMeasure,
stepsPerBeat: curChange.stepsPerBeat
});
return curChange;
}
/**
* Maps BPM changes from a song.
* @param song Song to map BPM changes from.
*/
public static function mapBPMChanges(song:ChartData)
{
bpmChangeMap = [
{
stepTime: 0,
songTime: 0,
bpm: song.meta.bpm
}
];
public static function mapBPMChanges(song:ChartData) {
var curChange:BPMChangeEvent = {
songTime: 0,
stepTime: 0,
beatTime: 0,
measureTime: 0,
bpm: song.meta.bpm,
beatsPerMeasure: song.meta.beatsPerMeasure.getDefault(4),
stepsPerBeat: CoolUtil.floorInt(song.meta.stepsPerBeat.getDefault(4))
};
curChangeIndex = 0;
bpmChangeMap = [curChange];
invalidEvents = [];
if (song.events == null) return;
var curBPM:Float = song.meta.bpm;
var songTime:Float = 0;
var stepTime:Float = 0;
// fix the sort first...
var events:Array<ChartEvent> = [for (e in song.events) if (e.params != null && validEventNames.contains(e.name)) e];
events.sort((a, b) -> MathUtil.equal(a.time, b.time) ? (a.name == "Time Signature Change" ? -1 : 1) : Std.int(a.time - b.time));
for(e in song.events) if (e.name == "BPM Change" && e.params != null && e.params[0] is Float) {
if (e.params[0] == curBPM) continue;
var steps = (e.time - songTime) / ((60 / curBPM) * 1000 / 4);
stepTime += steps;
songTime = e.time;
curBPM = e.params[0];
for (e in events) curChange = mapEvent(e, curChange);
}
bpmChangeMap.push({
stepTime: stepTime,
songTime: songTime,
bpm: curBPM
});
private static function mapEvent(e:ChartEvent, curChange:BPMChangeEvent) {
var name = e.name, params = e.params, time = e.time;
if (curChange.continuous && MathUtil.lessThan(time, curChange.endSongTime)) { //ensure that you cannot place any conductor events during a continuous change
invalidEvents.push(e);
Logs.trace('Invalid Conductor event "${e.name}" at ${e.time} (Intersecting continuous change!)', WARNING);
return curChange;
}
if (name == "BPM Change" && params[0] is Float && curChange.bpm != params[0])
curChange = mapBPMChange(curChange, time, params[0]);
else if (name == "Time Signature Change") {
//if (beatsPerMeasure == curChange.beatsPerMeasure && stepsPerBeat == curChange.stepsPerBeat) continue;
/* TODO: make so time sigs doesnt stop the bpm change if its in the duration of bpm change */
if (curChange.songTime != time) curChange = mapBPMChange(curChange, time, curChange.bpm);
curChange.beatsPerMeasure = params[0];
if (params[2]) curChange.stepsPerBeat = params[1];
else curChange.stepsPerBeat = Math.floor(16 / params[1]); // convert from denominator to stepsPerBeat
curChange.stepTime = CoolUtil.floorInt(curChange.stepTime + .99998);
curChange.beatTime = CoolUtil.floorInt(curChange.beatTime + .99998);
curChange.measureTime = CoolUtil.floorInt(curChange.measureTime + .99998);
}
else if (name == "Continuous BPM Change") {
var prevBPM = curChange.bpm;
if (curChange.bpm == params[0]) {
invalidEvents.push(e);
return curChange; //DO NOT!!!!
}
curChange = mapBPMChange(curChange, time, params[0]);
curChange.endSongTime = time + (params[1]) / (curChange.bpm - prevBPM) * Math.log(curChange.bpm / prevBPM) * 15000;
curChange.endStepTime = curChange.stepTime + params[1];
curChange.continuous = true;
}
return curChange;
}
public static function mapCharterBPMChanges(song:ChartData) {
var curChange:BPMChangeEvent = {
songTime: 0,
stepTime: 0,
beatTime: 0,
measureTime: 0,
bpm: song.meta.bpm,
beatsPerMeasure: song.meta.beatsPerMeasure.getDefault(4),
stepsPerBeat: CoolUtil.floorInt(song.meta.stepsPerBeat.getDefault(4))
};
bpmChangeMap = [curChange];
invalidEvents = [];
var grpEvents = Charter.instance.leftEventsGroup.members.concat(Charter.instance.rightEventsGroup.members);
var n = grpEvents.length;
if (n != 0) {
grpEvents.sort((a, b) -> a.step < b.step ? -1 : 1);
curChangeIndex = 0;
var i = 0, i2 = 0, j = 0, x = 0, events = [], ce, e;
while (i < n) {
x += (ce = grpEvents[i]).events.length;
if (++i < n && grpEvents[i].step == ce.step) continue;
var eventTime = Conductor.getStepsInTime(ce.step, curChangeIndex);
if (Math.isNaN(eventTime)) eventTime = 0;
events.resize(x);
while (i2 < i) {
x = 0;
ce = grpEvents[i2++];
while (x < ce.events.length) if (validEventNames.contains((e = ce.events[x++]).name)) {
events[j++] = e;
if (eventTime != 0) e.time = eventTime;
}
}
events.resize(x = j);
events.sort((a, b) -> MathUtil.equal(a.time, b.time) ? (a.name == "Time Signature Change" ? -1 : 1) : 0);
j = 0;
while (j < x) curChange = mapEvent(events[j++], curChange);
curChangeIndex = bpmChangeMap.length - 1;
x = j = 0;
}
}
curChangeIndex = 0;
}
private static var elapsed:Float;
@@ -164,12 +357,10 @@ class Conductor
return;
}
if (lastSongPos != (lastSongPos = FlxG.sound.music.time - songOffset)) {
// update conductor
if (lastSongPos != (lastSongPos = FlxG.sound.music.time - songOffset))
songPosition = lastSongPos;
} else {
else
songPosition += songOffset + elapsed * 1000;
}
}
private static function onStateSwitch(newState:FlxState) {
@@ -177,137 +368,247 @@ class Conductor
reset();
}
private static var __lastChange:BPMChangeEvent;
private static var __updateStep:Bool;
private static var __updateBeat:Bool;
private static var __updateMeasure:Bool;
private static function update() {
if (FlxG.state != null && FlxG.state is MusicBeatState) {
var state:MusicBeatState = cast FlxG.state;
if(state.cancelConductorUpdate)
return;
}
if (FlxG.state != null && FlxG.state is MusicBeatState && cast(FlxG.state, MusicBeatState).cancelConductorUpdate) return;
__updateSongPos(FlxG.elapsed);
if (bpm > 0) {
// updates curBeat and stuff
__lastChange = {
stepTime: 0,
songTime: 0,
bpm: 0
};
for (change in Conductor.bpmChangeMap)
{
if (Conductor.songPosition >= change.songTime)
__lastChange = change;
var oldStep = curStep, oldBeat = curBeat, oldMeasure = curMeasure, oldChangeIndex = curChangeIndex;
if ((curChangeIndex = getTimeInChangeIndex(songPosition, curChangeIndex)) > 0) {
var change = curChange;
curStepFloat = getTimeWithBPMInSteps(songPosition, curChangeIndex, getTimeWithIndexInBPM(songPosition, curChangeIndex));
curBeatFloat = change.beatTime + (curStepFloat - change.stepTime) / change.stepsPerBeat;
curMeasureFloat = change.measureTime + (curBeatFloat - change.beatTime) / change.beatsPerMeasure;
}
else
curMeasureFloat = (curBeatFloat = (curStepFloat = songPosition / stepCrochet) / stepsPerBeat) / beatsPerMeasure;
if (curChangeIndex != oldChangeIndex) {
var prev = bpmChangeMap[oldChangeIndex];
if (prev != null) {
if (beatsPerMeasure != prev.beatsPerMeasure || stepsPerBeat != prev.stepsPerBeat)
onTimeSignatureChange.dispatch(beatsPerMeasure, stepsPerBeat);
if (curChange.bpm != prev.bpm) onBPMChange.dispatch(curChange.bpm, curChange.endSongTime);
}
else {
onTimeSignatureChange.dispatch(beatsPerMeasure, stepsPerBeat);
onBPMChange.dispatch(curChange.bpm, curChange.endSongTime);
}
}
if (__lastChange.bpm > 0 && bpm != __lastChange.bpm) changeBPM(__lastChange.bpm);
if (__updateStep = (curStep != (curStep = CoolUtil.floorInt(curStepFloat)))) {
if (curStep > oldStep) for (i in oldStep...curStep) onStepHit.dispatch(i + 1);
else onStepHit.dispatch(curStep);
}
curStepFloat = __lastChange.stepTime + ((Conductor.songPosition - __lastChange.songTime) / Conductor.stepCrochet);
curBeatFloat = curStepFloat / stepsPerBeat;
curMeasureFloat = curBeatFloat / beatsPerMeasure;
if (__updateBeat = (curBeat != (curBeat = CoolUtil.floorInt(curBeatFloat)))) {
if (curBeat > oldBeat) for (i in oldBeat...curBeat) onBeatHit.dispatch(i + 1);
else onBeatHit.dispatch(curBeat);
}
var oldStep = curStep;
var oldBeat = curBeat;
var oldMeasure = curMeasure;
if (curStep != (curStep = CoolUtil.floorInt(curStepFloat))) {
if (curStep < oldStep && oldStep - curStep < 2) return;
// updates step
__updateBeat = curBeat != (curBeat = CoolUtil.floorInt(curBeatFloat));
__updateMeasure = __updateBeat && (curMeasure != (curMeasure = CoolUtil.floorInt(curMeasureFloat)));
if (__updateMeasure = (curMeasure != (curMeasure = CoolUtil.floorInt(curMeasureFloat)))) {
if (curMeasure > oldMeasure) for (i in oldMeasure...curMeasure) onMeasureHit.dispatch(i + 1);
else onMeasureHit.dispatch(curMeasure);
}
if (curStep > oldStep) {
for(i in oldStep...curStep) {
onStepHit.dispatch(i+1);
if (__updateStep || __updateBeat || __updateMeasure) {
var state = FlxG.state;
while (state != null) {
if (state is IBeatReceiver && (state.subState == null || state.persistentUpdate)) {
var st = cast(state, IBeatReceiver);
if (__updateStep) {
if (curStep > oldStep) for (i in oldStep...curStep) st.stepHit(i + 1);
else st.stepHit(curStep);
}
if (__updateBeat) {
if (curBeat > oldBeat) for (i in oldBeat...curBeat) st.beatHit(i + 1);
else st.beatHit(curBeat);
}
if (__updateMeasure) {
if (curMeasure > oldMeasure) for (i in oldMeasure...curMeasure) st.measureHit(i + 1);
else st.measureHit(curMeasure);
}
}
if (__updateBeat && curBeat > oldBeat) {
for(i in oldBeat...curBeat) {
onBeatHit.dispatch(i+1);
}
}
if (__updateMeasure && curMeasure > oldMeasure) {
for(i in oldMeasure...curMeasure) {
onMeasureHit.dispatch(i+1);
}
}
if (FlxG.state is IBeatReceiver) {
var state = FlxG.state;
while(state != null) {
if (state is IBeatReceiver && (state.subState == null || state.persistentUpdate)) {
var st:IBeatReceiver = cast state;
if (curStep > oldStep) {
for(i in oldStep...curStep) {
st.stepHit(i+1);
}
}
if (__updateBeat && curBeat > oldBeat) {
for(i in oldBeat...curBeat) {
st.beatHit(i+1);
}
}
if (__updateMeasure && curMeasure > oldMeasure) {
for(i in oldMeasure...curMeasure) {
st.measureHit(i+1);
}
}
}
state = state.subState;
}
}
state = state.subState;
}
}
}
public static function changeBPM(newBpm:Float, beatsPerMeasure:Float = 4, stepsPerBeat:Int = 4)
{
bpm = newBpm;
crochet = ((60 / bpm) * 1000);
stepCrochet = crochet / stepsPerBeat;
Conductor.beatsPerMeasure = beatsPerMeasure;
Conductor.stepsPerBeat = stepsPerBeat;
onBPMChange.dispatch(bpm);
public static function getTimeInChangeIndex(time:Float, index:Int = 0):Int {
if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].songTime > time) {
while (--index > 0) if (time > bpmChangeMap[index].songTime) return index;
return 0;
}
else {
for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].songTime > time) return i - 1;
return bpmChangeMap.length - 1;
}
}
public static function getTimeForStep(step:Float) {
var bpmChange:BPMChangeEvent = {
stepTime: 0,
songTime: 0,
bpm: bpm
};
for(change in bpmChangeMap)
if (change.stepTime < step && change.stepTime >= bpmChange.stepTime)
bpmChange = change;
return bpmChange.songTime + ((step - bpmChange.stepTime) * ((60 / bpmChange.bpm) * (1000/stepsPerBeat)));
public static function getStepsInChangeIndex(stepTime:Float, index:Int = 0):Int {
if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].stepTime > stepTime) {
while (--index > 0) if (stepTime > bpmChangeMap[index].stepTime) return index;
return 0;
}
else {
for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].stepTime > stepTime) return i - 1;
return bpmChangeMap.length - 1;
}
}
public static function getStepForTime(time:Float) {
var bpmChange:BPMChangeEvent = {
stepTime: 0,
songTime: 0,
bpm: bpm
};
for(change in bpmChangeMap)
if (change.songTime < time && change.songTime >= bpmChange.songTime)
bpmChange = change;
return bpmChange.stepTime + ((time - bpmChange.songTime) / ((60 / bpmChange.bpm) * (1000/stepsPerBeat)));
public static function getBeatsInChangeIndex(beatTime:Float, index:Int = 0):Int {
if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].beatTime > beatTime) {
while (--index > 0) if (beatTime > bpmChangeMap[index].beatTime) return index;
return 0;
}
else {
for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].beatTime > beatTime) return i - 1;
return bpmChangeMap.length - 1;
}
}
public static function getMeasuresInChangeIndex(measureTime:Float, index:Int = 0):Int {
if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].measureTime > measureTime) {
while (--index > 0) if (measureTime > bpmChangeMap[index].measureTime) return index;
return 0;
}
else {
for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].measureTime > measureTime) return i - 1;
return bpmChangeMap.length - 1;
}
}
public static function getTimeWithIndexInBPM(time:Float, index:Int):Float {
var bpmChange = bpmChangeMap[index];
if (bpmChange.continuous && time < bpmChange.endSongTime && index > 0) {
var prevBPM = bpmChangeMap[index - 1].bpm;
if (time <= bpmChange.songTime) return prevBPM;
var ratio = (time - bpmChange.songTime) / (bpmChange.endSongTime - bpmChange.songTime);
return Math.pow(prevBPM, 1 - ratio) * Math.pow(bpmChange.bpm, ratio);
}
return bpmChange.bpm;
}
public static function getStepsWithIndexInBPM(stepTime:Float, index:Int):Float {
var bpmChange = bpmChangeMap[index];
if (bpmChange.continuous && index > 0) {
var prevBPM = bpmChangeMap[index - 1].bpm;
if (stepTime <= bpmChange.stepTime) return prevBPM;
var endStepTime = bpmChange.stepTime + (bpmChange.endSongTime - bpmChange.songTime) * (bpmChange.bpm - prevBPM) / Math.log(bpmChange.bpm / prevBPM) / 15000;
if (stepTime < endStepTime) return FlxMath.remapToRange(stepTime, bpmChange.stepTime, endStepTime, prevBPM, bpmChange.bpm);
}
return bpmChange.bpm;
}
public static function getTimeInBPM(time:Float):Float {
if (bpmChangeMap.length == 0) return dummyChange.bpm;
return getTimeWithIndexInBPM(time, getTimeInChangeIndex(time));
}
public static function getTimeWithBPMInSteps(time:Float, index:Int, bpm:Float):Float {
var bpmChange = bpmChangeMap[index];
if (bpmChange.continuous && time > bpmChange.songTime && index > 0) {
var prevBPM = bpmChangeMap[index - 1].bpm;
if (time > bpmChange.endSongTime)
return bpmChange.stepTime + (((bpmChange.endSongTime - bpmChange.songTime) * (bpmChange.bpm - prevBPM))
/ Math.log(bpmChange.bpm / prevBPM) + (time - bpmChange.endSongTime) * bpm) / 15000;
else
return bpmChange.stepTime + (time - bpmChange.songTime) * (bpm - prevBPM) / Math.log(bpm / prevBPM) / 15000;
}
else {
return bpmChange.stepTime + (time - bpmChange.songTime) / (15000 / bpm);
}
}
public static function getTimeInBeats(time:Float, from:Int = 0):Float {
var index = getTimeInChangeIndex(time, from);
if (index == -1) return time / (60000 / dummyChange.bpm);
else if (index == 0) return time / (15000 / bpmChangeMap[index].bpm) / bpmChangeMap[index].stepsPerBeat;
else {
var change = bpmChangeMap[index];
return change.beatTime + (getTimeWithBPMInSteps(time, index, getTimeWithIndexInBPM(time, index)) - change.stepTime) / change.stepsPerBeat;
}
}
public static function getTimeInSteps(time:Float, from:Int = 0):Float {
var index = getTimeInChangeIndex(time, from);
return index < 1 ? time / (15000 / getTimeInBPM(time)) : getTimeWithBPMInSteps(time, index, getTimeWithIndexInBPM(time, index));
}
@:noCompletion
@:haxe.warning("-WDeprecated")
public static inline function getStepForTime(time:Float):Float return getTimeInSteps(time);
public static function getStepsWithBPMInTime(stepTime:Float, index:Int, bpm:Float):Float {
var bpmChange = bpmChangeMap[index];
if (bpmChange.continuous && stepTime > bpmChange.stepTime && index > 0) {
var prevBPM = bpmChangeMap[index - 1].bpm;
var time = bpmChange.songTime + (stepTime - bpmChange.stepTime) / (bpm - prevBPM) * Math.log(bpm / prevBPM) * 15000;
if (time > bpmChange.endSongTime)
return (15000 * (stepTime - bpmChange.stepTime) - ((bpmChange.endSongTime - bpmChange.songTime) * (bpm - prevBPM))
/ Math.log(bpm / prevBPM)) / bpm + bpmChange.endSongTime;
else
return time;
}
else {
return bpmChange.songTime + (stepTime - bpmChange.stepTime) * (15000 / bpm);
}
}
public static function getMeasuresInTime(measureTime:Float, from:Int = 0):Float {
var index = getMeasuresInChangeIndex(measureTime, from);
if (index == -1) return measureTime * (15000 / dummyChange.bpm);
else if (index == 0) return measureTime * (15000 / bpmChangeMap[index].bpm) * bpmChangeMap[index].stepsPerBeat * bpmChangeMap[index].beatsPerMeasure;
else {
var change = bpmChangeMap[index];
var stepTime = change.stepTime + (measureTime - change.measureTime) * change.stepsPerBeat * change.beatsPerMeasure;
return getStepsWithBPMInTime(stepTime, index, getStepsWithIndexInBPM(stepTime, index));
}
}
public static function getBeatsInTime(beatTime:Float, from:Int = 0):Float {
var index = getBeatsInChangeIndex(beatTime, from);
if (index == -1) return beatTime * (15000 / dummyChange.bpm);
else if (index == 0) return beatTime * (15000 / bpmChangeMap[index].bpm) * bpmChangeMap[index].stepsPerBeat;
else {
var change = bpmChangeMap[index];
var stepTime = change.stepTime + (beatTime - change.beatTime) * change.stepsPerBeat;
return getStepsWithBPMInTime(stepTime, index, getStepsWithIndexInBPM(stepTime, index));
}
}
public static function getStepsInTime(stepTime:Float, from:Int = 0):Float {
var index = getStepsInChangeIndex(stepTime, from);
return index < 1 ? stepTime * (15000 / bpmChangeMap[index].bpm) : getStepsWithBPMInTime(stepTime, index, getStepsWithIndexInBPM(stepTime, index));
}
@:noCompletion
@:haxe.warning("-WDeprecated")
public static inline function getTimeForStep(steps:Float):Float return getStepsInTime(steps);
public static inline function getMeasureLength()
return stepsPerBeat * beatsPerMeasure;
public static inline function getMeasuresLength() {
if (FlxG.sound.music == null) return 0.0;
return getStepForTime(FlxG.sound.music.length) / getMeasureLength();
var length = FlxG.sound.music.length;
var index = getTimeInChangeIndex(length, bpmChangeMap.length - 1);
var change = bpmChangeMap[index];
return change.measureTime + (getTimeInBeats(length, index) - change.beatTime) / change.beatsPerMeasure;
}
}
}
+3 -1
View File
@@ -104,7 +104,9 @@ class Flags {
public static var DEFAULT_GAMEOVER_LOSS_SFX:String = "gameOverSFX";
public static var DEFAULT_GAMEOVER_RETRY_SFX:String = "gameOverEnd";
public static var DEFAULT_CAM_ZOOM_INTERVAL:Int = 4;
public static var DEFAULT_CAM_ZOOM_INTERVAL:Int = 1;
public static var DEFAULT_CAM_ZOOM_OFFSET:Float = 0;
//public static var DEFAULT_CAM_ZOOM_EVERY:BeatType = MEASURE;
public static var DEFAULT_CAM_ZOOM_STRENGTH:Int = 1;
public static var DEFAULT_CAM_ZOOM:Float = 1.05; // what zoom level it defaults to
public static var DEFAULT_HUD_ZOOM:Float = 1.0;
@@ -12,7 +12,7 @@ class ConductorInfo extends FramerateCategory {
_text += '\n - ${Conductor.curStep} steps';
_text += '\n - ${Conductor.curMeasure} measures';
_text += '\nCurrent BPM: ${Conductor.bpm}';
_text += '\nTime Signature: ${Conductor.beatsPerMeasure}/${Conductor.stepsPerBeat}';
_text += '\nTime Signature: ${Conductor.beatsPerMeasure}/${Conductor.denominator}';
this.text.text = _text;
super.__enterFrame(t);
+1 -1
View File
@@ -471,7 +471,7 @@ final class CoolUtil
}
var bpm:Null<Float> = Std.parseFloat(musicInfo["BPM"]).getDefault(DefaultBPM);
Conductor.changeBPM(bpm, beatsPerMeasure, stepsPerBeat);
Conductor.changeBPM(bpm, beatsPerMeasure, floorInt(stepsPerBeat));
} else
Conductor.changeBPM(DefaultBPM);
}
+78
View File
@@ -73,6 +73,84 @@ final class MathUtil {
return min;
}
/**
* Checks if a is less than b with considering a margin of error.
*
* @param a Float
* @param b Float
* @param margin Float (Default: EPSILON)
*
* @return Bool
**/
public static function lessThan(a:Float, b:Float, margin:Float = 0.0000001):Bool {
return a < b - margin;
}
/**
* Checks if a is less than or equally b with considering a margin of error.
*
* @param a Float
* @param b Float
* @param margin Float (Default: EPSILON)
*
* @return Bool
**/
public static function lessThanEqual(a:Float, b:Float, margin:Float = 0.0000001):Bool {
return a <= b - margin;
}
/**
* Checks if a is greater than b with considering a margin of error.
*
* @param a Float
* @param b Float
* @param margin Float (Default: EPSILON)
*
* @return Bool
**/
public static function greaterThan(a:Float, b:Float, margin:Float = 0.0000001):Bool {
return a > b + margin;
}
/**
* Checks if a is greater than or equally b with considering a margin of error.
*
* @param a Float
* @param b Float
* @param margin Float (Default: EPSILON)
*
* @return Bool
**/
public static function greaterThanEqual(a:Float, b:Float, margin:Float = 0.0000001):Bool {
return a >= b + margin;
}
/**
* Checks if a is approximately equal to b.
*
* @param a Float
* @param b Float
* @param margin Float (Default: EPSILON)
*
* @return Bool
**/
public static function equal(a:Float, b:Float, margin:Float = 0.0000001):Bool {
return Math.abs(a - b) <= margin;
}
/**
* Checks if a are not approximately equal to b.
*
* @param a Float
* @param b Float
* @param margin Float (Default: EPSILON)
*
* @return Bool
**/
public static function notEqual(a:Float, b:Float, margin:Float = 0.0000001):Bool {
return Math.abs(a - b) > margin;
}
/**
* Shortcut to `Math.max` but with infinite amount of arguments
*