Merge branch 'Time-Signature-Changes' into new-conductor

# Conflicts:
#	.gitignore
#	source/funkin/backend/FunkinSprite.hx
#	source/funkin/backend/chart/ChartData.hx
#	source/funkin/backend/chart/EventsData.hx
#	source/funkin/backend/system/Conductor.hx
#	source/funkin/backend/utils/CoolUtil.hx
#	source/funkin/editors/charter/Charter.hx
#	source/funkin/editors/charter/CharterEvent.hx
#	source/funkin/editors/charter/CharterEventScreen.hx
#	source/funkin/editors/charter/CharterMetaDataScreen.hx
#	source/funkin/editors/charter/SongCreationScreen.hx
#	source/funkin/game/PlayState.hx
This commit is contained in:
Ralty
2025-07-16 19:27:05 +07:00
32 changed files with 1288 additions and 311 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 158 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 B

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<TextureAtlas imagePath="arrow-down.png">
<SubTexture name="tail" x="0" y="0" width="8" height="3"/>
<SubTexture name="segment" x="0" y="3" width="8" height="1"/>
<SubTexture name="arrow" x="0" y="6" width="8" height="6"/>
</TextureAtlas>
Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

-1
View File
@@ -145,7 +145,6 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
{ {
if (lastAnimContext != LOCK && beatAnims.length > 0 && (curBeat + beatOffset) % beatInterval == 0) if (lastAnimContext != LOCK && beatAnims.length > 0 && (curBeat + beatOffset) % beatInterval == 0)
{ {
if(skipNegativeBeats && curBeat < 0) return;
// TODO: find a solution without countedBeat // TODO: find a solution without countedBeat
var anim = beatAnims[FlxMath.wrap(countedBeat++, 0, beatAnims.length - 1)]; var anim = beatAnims[FlxMath.wrap(countedBeat++, 0, beatAnims.length - 1)];
if (anim.name != null && anim.name != "null" && anim.name != "none") if (anim.name != null && anim.name != "null" && anim.name != "none")
+8 -4
View File
@@ -10,7 +10,7 @@ import openfl.Assets;
using StringTools; using StringTools;
class EventsData { 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>> = [ public static var defaultEventsParams:Map<String, Array<EventParamInfo>> = [
"HScript Call" => [ "HScript Call" => [
{name: "Function Name", type: TString, defValue: "myFunc"}, {name: "Function Name", type: TString, defValue: "myFunc"},
@@ -75,8 +75,10 @@ class EventsData {
{name: "Multiplicative?", type: TBool, defValue: true} {name: "Multiplicative?", type: TBool, defValue: true}
], ],
"Camera Modulo Change" => [ "Camera Modulo Change" => [
{name: "Modulo Interval (Beats)", type: TInt(1, 9999999, 1), defValue: 4}, {name: "Modulo Interval", type: TInt(1, 9999999, 1), defValue: 4},
{name: "Bump Strength", type: TFloat(0.1, 10, 0.01, 2), defValue: 1} {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" => [ "Camera Flash" => [
{name: "Reversed?", type: TBool, defValue: false}, {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: "Time (Steps)", type: TFloat(0.25, 9999, 0.25, 2), defValue: 4},
{name: "Camera", type: TDropDown(['camGame', 'camHUD']), defValue: "camHUD"} {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" => [ "Scroll Speed Change" => [
{name: "Tween Speed?", type: TBool, defValue: true}, {name: "Tween Speed?", type: TBool, defValue: true},
{name: "New Speed", type: TFloat(0.01, 99, 0.01, 2), defValue: 1.}, {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 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)) { if (camFocusedBF != (camFocusedBF = section.mustHitSection)) {
result.events.push({ result.events.push({
time: curTime, time: curTime,
@@ -178,7 +190,8 @@ class FNFLegacyParser {
mustHitSection: notes[section-1] != null ? notes[section-1].mustHitSection : false, mustHitSection: notes[section-1] != null ? notes[section-1].mustHitSection : false,
bpm: notes[section-1] != null ? notes[section-1].bpm : chart.meta.bpm, bpm: notes[section-1] != null ? notes[section-1].bpm : chart.meta.bpm,
changeBPM: false, 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)); var sectionEndTime:Float = Conductor.getTimeForStep(Conductor.getMeasureLength() * (section+1));
@@ -192,6 +205,8 @@ class FNFLegacyParser {
case "BPM Change": case "BPM Change":
baseSection.changeBPM = true; baseSection.changeBPM = true;
baseSection.bpm = event.params[0]; baseSection.bpm = event.params[0];
case "Time Signature Change":
baseSection.sectionBeats = event.params[0];
} }
} }
notes[section] = baseSection; notes[section] = baseSection;
+451 -170
View File
@@ -4,56 +4,58 @@ import flixel.FlxState;
import flixel.util.FlxSignal.FlxTypedSignal; import flixel.util.FlxSignal.FlxTypedSignal;
import funkin.backend.chart.ChartData; import funkin.backend.chart.ChartData;
import funkin.backend.system.interfaces.IBeatReceiver; import funkin.backend.system.interfaces.IBeatReceiver;
import funkin.editors.charter.Charter;
typedef BPMChangeEvent = enum BeatType {
{ BEAT;
var stepTime:Float; MEASURE;
var songTime:Float; STEP;
var bpm:Float;
} }
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 * FlxSignals
*/ */
public static var onMeasureHit:FlxTypedSignal<Int->Void> = new FlxTypedSignal(); public static var onMeasureHit:FlxTypedSignal<Int->Void> = new FlxTypedSignal();
public static var onBeatHit: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 onStepHit:FlxTypedSignal<Int->Void> = new FlxTypedSignal();
public static var onBPMChange:FlxTypedSignal<Float->Void> = new FlxTypedSignal(); public static var onBPMChange:FlxTypedSignal<(Float,Float)->Void> = new FlxTypedSignal();
public static var onTimeSignatureChange:FlxTypedSignal<(Float,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;
/** /**
* Current position of the song, in milliseconds. * Current position of the song, in milliseconds.
*/ */
public static var songPosition(get, default):Float; public static var songPosition(get, default):Float;
private static function get_songPosition() { private static function get_songPosition() {
if (songOffset != Options.songOffset) trace(songOffset = Options.songOffset); if (songOffset != Options.songOffset) songOffset = Options.songOffset;
return songPosition - songOffset; return songPosition - songOffset;
} }
@@ -62,6 +64,88 @@ class Conductor
*/ */
public static var songOffset:Float = 0; 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 * Current step
*/ */
@@ -77,7 +161,6 @@ class Conductor
*/ */
public static var curMeasure:Int = 0; public static var curMeasure:Int = 0;
/** /**
* Current step, as a `Float` (ex: 4.94, instead of 4) * Current step, as a `Float` (ex: 4.94, instead of 4)
*/ */
@@ -100,53 +183,143 @@ class Conductor
/** /**
* Array of all BPM changes that have been mapped. * 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() {} @:dox(hide) public function new() {}
public static function reset() { public static function reset() {
songPosition = lastSongPos = curBeatFloat = curStepFloat = curBeat = curStep = 0; songPosition = lastSongPos = curBeatFloat = curStepFloat = curBeat = curStep = 0;
bpmChangeMap = []; curChangeIndex = 0;
changeBPM(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) { public static function setupSong(SONG:ChartData) {
reset(); reset();
mapBPMChanges(SONG); 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. * Maps BPM changes from a song.
* @param song Song to map BPM changes from. * @param song Song to map BPM changes from.
*/ */
public static function mapBPMChanges(song:ChartData) public static function mapBPMChanges(song:ChartData) {
{ var curChange:BPMChangeEvent = {
bpmChangeMap = [ songTime: 0,
{ stepTime: 0,
stepTime: 0, beatTime: 0,
songTime: 0, measureTime: 0,
bpm: song.meta.bpm 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; if (song.events == null) return;
var curBPM:Float = song.meta.bpm; // fix the sort first...
var songTime:Float = 0; var events:Array<ChartEvent> = [for (e in song.events) if (e.params != null && validEventNames.contains(e.name)) e];
var stepTime:Float = 0; events.sort((a, b) -> {
if (MathUtil.equal(a.time, b.time)) return a.name == "Continuous BPM Change" ? 1 : -1;
return Std.int(a.time - b.time);
});
for(e in song.events) if (e.name == "BPM Change" && e.params != null && e.params[0] is Float) { for (e in events) curChange = mapEvent(e, curChange);
if (e.params[0] == curBPM) continue; }
var steps = (e.time - songTime) / ((60 / curBPM) * 1000 / 4);
stepTime += steps;
songTime = e.time;
curBPM = e.params[0];
bpmChangeMap.push({ private static function mapEvent(e:ChartEvent, curChange:BPMChangeEvent) {
stepTime: stepTime, var name = e.name, params = e.params, time = e.time;
songTime: songTime, if (curChange.continuous && MathUtil.lessThan(time, curChange.endSongTime)) { //ensure that you cannot place any conductor events during a continuous change
bpm: curBPM 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))
};
curChangeIndex = 0;
bpmChangeMap = [curChange];
invalidEvents = [];
for (event in Charter.instance.eventsGroup.members) {
event.events.sort((a, b) -> {
if (MathUtil.equal(a.time, b.time)) return a.name == "Continuous BPM Change" ? 1 : -1;
return 0;
}); });
var eventTime = Conductor.getStepsInTime(event.step);
for (e in event.events) {
if (!Math.isNaN(eventTime)) e.time = eventTime;
if (validEventNames.contains(e.name)) curChange = mapEvent(e, curChange);
}
} }
} }
@@ -164,12 +337,10 @@ class Conductor
return; return;
} }
if (lastSongPos != (lastSongPos = FlxG.sound.music.time - songOffset)) { if (lastSongPos != (lastSongPos = FlxG.sound.music.time - songOffset))
// update conductor
songPosition = lastSongPos; songPosition = lastSongPos;
} else { else
songPosition += songOffset + elapsed * 1000; songPosition += songOffset + elapsed * 1000;
}
} }
private static function onStateSwitch(newState:FlxState) { private static function onStateSwitch(newState:FlxState) {
@@ -177,137 +348,247 @@ class Conductor
reset(); reset();
} }
private static var __lastChange:BPMChangeEvent; private static var __lastChange:BPMChangeEvent;
private static var __updateStep:Bool;
private static var __updateBeat:Bool; private static var __updateBeat:Bool;
private static var __updateMeasure:Bool; private static var __updateMeasure:Bool;
private static function update() { private static function update() {
if (FlxG.state != null && FlxG.state is MusicBeatState) { if (FlxG.state != null && FlxG.state is MusicBeatState && cast(FlxG.state, MusicBeatState).cancelConductorUpdate) return;
var state:MusicBeatState = cast FlxG.state;
if(state.cancelConductorUpdate)
return;
}
__updateSongPos(FlxG.elapsed); __updateSongPos(FlxG.elapsed);
if (bpm > 0) { var oldStep = curStep, oldBeat = curBeat, oldMeasure = curMeasure, oldChangeIndex = curChangeIndex;
// updates curBeat and stuff
__lastChange = { if ((curChangeIndex = getTimeInChangeIndex(songPosition, curChangeIndex)) > 0) {
stepTime: 0, var change = curChange;
songTime: 0, curStepFloat = getTimeWithBPMInSteps(songPosition, curChangeIndex, getTimeWithIndexInBPM(songPosition, curChangeIndex));
bpm: 0 curBeatFloat = change.beatTime + (curStepFloat - change.stepTime) / change.stepsPerBeat;
}; curMeasureFloat = change.measureTime + (curBeatFloat - change.beatTime) / change.beatsPerMeasure;
for (change in Conductor.bpmChangeMap) }
{ else
if (Conductor.songPosition >= change.songTime) curMeasureFloat = (curBeatFloat = (curStepFloat = songPosition / stepCrochet) / stepsPerBeat) / beatsPerMeasure;
__lastChange = change;
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); if (__updateBeat = (curBeat != (curBeat = CoolUtil.floorInt(curBeatFloat)))) {
curBeatFloat = curStepFloat / stepsPerBeat; if (curBeat > oldBeat) for (i in oldBeat...curBeat) onBeatHit.dispatch(i + 1);
curMeasureFloat = curBeatFloat / beatsPerMeasure; else onBeatHit.dispatch(curBeat);
}
var oldStep = curStep; if (__updateMeasure = (curMeasure != (curMeasure = CoolUtil.floorInt(curMeasureFloat)))) {
var oldBeat = curBeat; if (curMeasure > oldMeasure) for (i in oldMeasure...curMeasure) onMeasureHit.dispatch(i + 1);
var oldMeasure = curMeasure; else onMeasureHit.dispatch(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 (curStep > oldStep) { if (__updateStep || __updateBeat || __updateMeasure) {
for(i in oldStep...curStep) { var state = FlxG.state;
onStepHit.dispatch(i+1); 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) { state = state.subState;
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;
}
}
} }
} }
} }
public static function changeBPM(newBpm:Float, beatsPerMeasure:Float = 4, stepsPerBeat:Int = 4) public static function getTimeInChangeIndex(time:Float, index:Int = 0):Int {
{ if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
bpm = newBpm; else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].songTime > time) {
while (--index > 0) if (time > bpmChangeMap[index].songTime) return index;
crochet = ((60 / bpm) * 1000); return 0;
stepCrochet = crochet / stepsPerBeat; }
else {
Conductor.beatsPerMeasure = beatsPerMeasure; for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].songTime > time) return i - 1;
Conductor.stepsPerBeat = stepsPerBeat; return bpmChangeMap.length - 1;
}
onBPMChange.dispatch(bpm);
} }
public static function getTimeForStep(step:Float) { public static function getStepsInChangeIndex(stepTime:Float, index:Int = 0):Int {
var bpmChange:BPMChangeEvent = { if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
stepTime: 0, else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].stepTime > stepTime) {
songTime: 0, while (--index > 0) if (stepTime > bpmChangeMap[index].stepTime) return index;
bpm: bpm return 0;
}; }
else {
for(change in bpmChangeMap) for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].stepTime > stepTime) return i - 1;
if (change.stepTime < step && change.stepTime >= bpmChange.stepTime) return bpmChangeMap.length - 1;
bpmChange = change; }
return bpmChange.songTime + ((step - bpmChange.stepTime) * ((60 / bpmChange.bpm) * (1000/stepsPerBeat)));
} }
public static function getStepForTime(time:Float) { public static function getBeatsInChangeIndex(beatTime:Float, index:Int = 0):Int {
var bpmChange:BPMChangeEvent = { if (bpmChangeMap.length < 2) return bpmChangeMap.length - 1;
stepTime: 0, else if (bpmChangeMap[index = CoolUtil.boundInt(index, 0, bpmChangeMap.length - 1)].beatTime > beatTime) {
songTime: 0, while (--index > 0) if (beatTime > bpmChangeMap[index].beatTime) return index;
bpm: bpm return 0;
}; }
else {
for(change in bpmChangeMap) for (i in index...bpmChangeMap.length) if (bpmChangeMap[i].beatTime > beatTime) return i - 1;
if (change.songTime < time && change.songTime >= bpmChange.songTime) return bpmChangeMap.length - 1;
bpmChange = change; }
return bpmChange.stepTime + ((time - bpmChange.songTime) / ((60 / bpmChange.bpm) * (1000/stepsPerBeat)));
} }
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() public static inline function getMeasureLength()
return stepsPerBeat * beatsPerMeasure; return stepsPerBeat * beatsPerMeasure;
public static inline function getMeasuresLength() { public static inline function getMeasuresLength() {
if (FlxG.sound.music == null) return 0.0; 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;
} }
} }
@@ -12,7 +12,7 @@ class ConductorInfo extends FramerateCategory {
_text += '\n - ${Conductor.curStep} steps'; _text += '\n - ${Conductor.curStep} steps';
_text += '\n - ${Conductor.curMeasure} measures'; _text += '\n - ${Conductor.curMeasure} measures';
_text += '\nCurrent BPM: ${Conductor.bpm}'; _text += '\nCurrent BPM: ${Conductor.bpm}';
_text += '\nTime Signature: ${Conductor.beatsPerMeasure}/${Conductor.stepsPerBeat}'; _text += '\nTime Signature: ${Conductor.beatsPerMeasure}/${Conductor.denominator}';
this.text.text = _text; this.text.text = _text;
super.__enterFrame(t); super.__enterFrame(t);
+40 -1
View File
@@ -443,7 +443,7 @@ class CoolUtil
} }
var bpm:Null<Float> = Std.parseFloat(musicInfo["BPM"]).getDefault(DefaultBPM); var bpm:Null<Float> = Std.parseFloat(musicInfo["BPM"]).getDefault(DefaultBPM);
Conductor.changeBPM(bpm, beatsPerMeasure, stepsPerBeat); Conductor.changeBPM(bpm, beatsPerMeasure, floorInt(stepsPerBeat));
} else } else
Conductor.changeBPM(DefaultBPM); Conductor.changeBPM(DefaultBPM);
} }
@@ -781,6 +781,15 @@ class CoolUtil
@:noUsing public static inline function maxInt(p1:Int, p2:Int) @:noUsing public static inline function maxInt(p1:Int, p2:Int)
return p1 < p2 ? p2 : p1; return p1 < p2 ? p2 : p1;
/**
* Equivalent of `Math.min`, except doesn't require a Int -> Float -> Int conversion.
* @param p1
* @param p2
* @return return p1 > p2 ? p2 : p1
*/
@:noUsing public static inline function minInt(p1:Int, p2:Int)
return p1 > p2 ? p2 : p1;
/** /**
* Equivalent of `Math.floor`, except doesn't require a Int -> Float -> Int conversion. * Equivalent of `Math.floor`, except doesn't require a Int -> Float -> Int conversion.
* @param e Value to get the floor of. * @param e Value to get the floor of.
@@ -880,6 +889,36 @@ class CoolUtil
return file.file; return file.file;
} }
public static inline function bound(Value:Float, Min:Float, Max:Float):Float {
#if cpp
var _hx_tmp1:Float = Value;
var _hx_tmp2:Float = Min;
var _hx_tmp3:Float = Max;
return untyped __cpp__("((({0}) < ({1})) ? ({1}) : (({0}) > ({2})) ? ({2}) : ({0}))", _hx_tmp1, _hx_tmp2, _hx_tmp3);
#else
return (Value < Min) ? Min : (Value > Max) ? Max : Value;
#end
}
public static inline function boundInt(Value:Int, Min:Int, Max:Int):Int {
#if cpp
var _hx_tmp1:Int = Value;
var _hx_tmp2:Int = Min;
var _hx_tmp3:Int = Max;
return untyped __cpp__("((({0}) < ({1})) ? ({1}) : (({0}) > ({2})) ? ({2}) : ({0}))", _hx_tmp1, _hx_tmp2, _hx_tmp3);
#else
return (Value < Min) ? Min : (Value > Max) ? Max : Value;
#end
}
public static inline function boolToInt(b:Bool):Int {
#if cpp
return untyped __cpp__("(({0}) ? 1 : 0)", b);
#else
return b ? 1 : 0;
#end
}
/** /**
* Converts a string of "1..3,5,7..9,8..5" into an array of numbers like [1,2,3,5,7,8,9,8,7,6,5] * Converts a string of "1..3,5,7..9,8..5" into an array of numbers like [1,2,3,5,7,8,9,8,7,6,5]
* @param input String to parse * @param input String to parse
+28
View File
@@ -0,0 +1,28 @@
package funkin.backend.utils;
final class MathUtil {
//Remind me to add the descriptions for the Wiki later - sen
public static inline function lessThan(aVal:Float, bVal:Float, diff:Float = FlxMath.EPSILON):Bool {
return aVal < bVal - diff;
}
public static inline function lessThanEqual(aVal:Float, bVal:Float, diff:Float = FlxMath.EPSILON):Bool {
return aVal <= bVal + diff;
}
public static inline function greaterThan(aVal:Float, bVal:Float, diff:Float = FlxMath.EPSILON):Bool {
return aVal > bVal + diff;
}
public static inline function greaterThanEqual(aVal:Float, bVal:Float, diff:Float = FlxMath.EPSILON):Bool {
return aVal >= bVal - diff;
}
public static inline function equal(aVal:Float, bVal:Float, diff:Float = FlxMath.EPSILON):Bool {
return Math.abs(aVal - bVal) <= diff;
}
public static inline function notEqual(aVal:Float, bVal:Float, diff:Float = FlxMath.EPSILON):Bool {
return Math.abs(aVal - bVal) > diff;
}
}
+35 -21
View File
@@ -613,6 +613,8 @@ class Charter extends UIState {
gridBackdrops.bottomLimitY = __endStep * 40; gridBackdrops.bottomLimitY = __endStep * 40;
eventsBackdrop.bottomSeparator.y = gridBackdrops.bottomLimitY-2; eventsBackdrop.bottomSeparator.y = gridBackdrops.bottomLimitY-2;
CharterGridSeperatorBase.lastConductorSprY = Math.NEGATIVE_INFINITY;
updateWaveforms(); updateWaveforms();
} }
@@ -846,6 +848,7 @@ class Charter extends UIState {
s.cursor = BUTTON; s.cursor = BUTTON;
} }
} }
checkSelectionForBPMUpdates();
if (!(verticalChange == 0 && horizontalChange == 0)) { if (!(verticalChange == 0 && horizontalChange == 0)) {
notesGroup.sortNotes(); eventsGroup.sortEvents(); notesGroup.sortNotes(); eventsGroup.sortEvents();
undos.addToUndo(CSelectionDrag(undoDrags)); undos.addToUndo(CSelectionDrag(undoDrags));
@@ -1037,11 +1040,7 @@ class Charter extends UIState {
notesGroup.sortNotes(); notesGroup.sortNotes();
notesGroup.autoSort = true; notesGroup.autoSort = true;
for (s in selection) checkSelectionForBPMUpdates();
if (s is CharterEvent) {
Charter.instance.updateBPMEvents();
break;
}
if (addToUndo) if (addToUndo)
undos.addToUndo(CCreateSelection(selection)); undos.addToUndo(CCreateSelection(selection));
@@ -1065,11 +1064,7 @@ class Charter extends UIState {
notesGroup.sortNotes(); notesGroup.sortNotes();
notesGroup.autoSort = true; notesGroup.autoSort = true;
for (s in selection) checkSelectionForBPMUpdates();
if (s is CharterEvent) {
Charter.instance.updateBPMEvents();
break;
}
if (addToUndo) if (addToUndo)
undos.addToUndo(CDeleteSelection(selection)); undos.addToUndo(CDeleteSelection(selection));
@@ -1256,12 +1251,13 @@ class Charter extends UIState {
for (strumLine in strumLines.members) strumLine.vocals.pause(); for (strumLine in strumLines.members) strumLine.vocals.pause();
} }
songPosInfo.text = '${CoolUtil.timeToStr(Conductor.songPosition)} / ${CoolUtil.timeToStr(songLength)}' var curChange = Conductor.curChange;
+ '\nStep: ${curStep}' songPosInfo.text = '${CoolUtil.timeToStr(songPos)} / ${CoolUtil.timeToStr(songLength)}'
+ '\nBeat: ${curBeat}' + '\nStep: $curStep'
+ '\nMeasure: ${curMeasure}' + '\nBeat: $curBeat'
+ '\nBPM: ${Conductor.bpm}' + '\nMeasure: $curMeasure'
+ '\nTime Signature: ${Conductor.beatsPerMeasure}/${Conductor.stepsPerBeat}'; + '\nBPM: ${(curChange != null && curChange.continuous && curChange.endSongTime > songPos) ? FlxMath.roundDecimal(Conductor.bpm, 3) : Conductor.bpm}'
+ '\nTime Signature: ${Conductor.beatsPerMeasure}/${Conductor.denominator}';
if (charterCamera.zoom != (charterCamera.zoom = lerp(charterCamera.zoom, __camZoom, __firstFrame ? 1 : 0.125))) if (charterCamera.zoom != (charterCamera.zoom = lerp(charterCamera.zoom, __camZoom, __firstFrame ? 1 : 0.125)))
updateDisplaySprites(); updateDisplaySprites();
@@ -1441,6 +1437,8 @@ class Charter extends UIState {
selection = sObjects; selection = sObjects;
_edit_copy(_); // to fix stupid bugs _edit_copy(_); // to fix stupid bugs
checkSelectionForBPMUpdates();
undos.addToUndo(CCreateSelection(sObjects.copy())); undos.addToUndo(CCreateSelection(sObjects.copy()));
} }
@@ -1486,6 +1484,7 @@ class Charter extends UIState {
if (s.selectable.draggable) s.selectable.handleDrag(s.change * -1); if (s.selectable.draggable) s.selectable.handleDrag(s.change * -1);
selection = [for (s in selectionDrags) s.selectable]; selection = [for (s in selectionDrags) s.selectable];
Charter.instance.updateBPMEvents();
case CEditSustains(changes): case CEditSustains(changes):
for(n in changes) for(n in changes)
n.note.updatePos(n.note.step, n.note.id, n.before, n.note.type); n.note.updatePos(n.note.step, n.note.id, n.before, n.note.type);
@@ -1531,6 +1530,7 @@ class Charter extends UIState {
for (s in selectionDrags) for (s in selectionDrags)
if (s.selectable.draggable) s.selectable.handleDrag(s.change); if (s.selectable.draggable) s.selectable.handleDrag(s.change);
//this.selection = selection; //this.selection = selection;
Charter.instance.updateBPMEvents();
case CEditSustains(changes): case CEditSustains(changes):
for(n in changes) for(n in changes)
n.note.updatePos(n.note.step, n.note.id, n.after, n.note.type); n.note.updatePos(n.note.step, n.note.id, n.after, n.note.type);
@@ -1643,11 +1643,9 @@ class Charter extends UIState {
} }
function _view_showeventSecSeparator(t) { function _view_showeventSecSeparator(t) {
t.icon = (Options.charterShowSections = !Options.charterShowSections) ? 1 : 0; t.icon = (Options.charterShowSections = !Options.charterShowSections) ? 1 : 0;
eventsBackdrop.eventSecSeparator.visible = gridBackdrops.sectionsVisible = Options.charterShowSections;
} }
function _view_showeventBeatSeparator(t) { function _view_showeventBeatSeparator(t) {
t.icon = (Options.charterShowBeats = !Options.charterShowBeats) ? 1 : 0; t.icon = (Options.charterShowBeats = !Options.charterShowBeats) ? 1 : 0;
eventsBackdrop.eventBeatSeparator.visible = gridBackdrops.beatsVisible = Options.charterShowBeats;
} }
function _view_switchWaveformDetail(t) { function _view_switchWaveformDetail(t) {
t.icon = (Options.charterLowDetailWaveforms = !Options.charterLowDetailWaveforms) ? 1 : 0; t.icon = (Options.charterLowDetailWaveforms = !Options.charterLowDetailWaveforms) ? 1 : 0;
@@ -1847,14 +1845,30 @@ class Charter extends UIState {
} }
public function updateBPMEvents() { public function updateBPMEvents() {
eventsGroup.sortEvents();
Conductor.mapCharterBPMChanges(PlayState.SONG);
buildEvents(); buildEvents();
Conductor.mapBPMChanges(PlayState.SONG); for(e in eventsGroup.members) {
Conductor.changeBPM(PlayState.SONG.meta.bpm, cast PlayState.SONG.meta.beatsPerMeasure.getDefault(4), cast PlayState.SONG.meta.stepsPerBeat.getDefault(4)); for(event in e.events) {
if (event.name == "BPM Change" || event.name == "Time Signature Change" || event.name == "Continuous BPM Change") {
e.refreshEventIcons();
break;
}
}
}
refreshBPMSensitive(); refreshBPMSensitive();
} }
public inline function checkSelectionForBPMUpdates() {
for (s in selection)
if (s is CharterEvent) {
Charter.instance.updateBPMEvents();
break;
}
}
public inline function hitsoundsEnabled(id:Int) public inline function hitsoundsEnabled(id:Int)
return strumLines.members[id] != null && strumLines.members[id].hitsounds; return strumLines.members[id] != null && strumLines.members[id].hitsounds;
@@ -12,8 +12,6 @@ class CharterBackdropGroup extends FlxTypedGroup<CharterBackdrop> {
public var conductorSprY:Float = 0; public var conductorSprY:Float = 0;
public var bottomLimitY:Float = 0; public var bottomLimitY:Float = 0;
public var sectionsVisible:Bool = true;
public var beatsVisible:Bool = true;
// Just here so you can update display sprites all dat and above // Just here so you can update display sprites all dat and above
public var strumlinesAmount:Int = 0; public var strumlinesAmount:Int = 0;
@@ -61,8 +59,6 @@ class CharterBackdropGroup extends FlxTypedGroup<CharterBackdrop> {
grid.conductorFollowerSpr.y = conductorSprY; grid.conductorFollowerSpr.y = conductorSprY;
grid.bottomSeparator.y = (grid.bottomLimit.y = bottomLimitY)-2; grid.bottomSeparator.y = (grid.bottomLimit.y = bottomLimitY)-2;
grid.sectionSeparator.visible = sectionsVisible;
grid.beatSeparator.visible = beatsVisible;
grid.waveformSprite.shader = strumLine.waveformShader; grid.waveformSprite.shader = strumLine.waveformShader;
@@ -119,8 +115,7 @@ class CharterBackdrop extends FlxTypedGroup<Dynamic> {
public var waveformSprite:FlxSprite; public var waveformSprite:FlxSprite;
public var conductorFollowerSpr:FlxSprite; public var conductorFollowerSpr:FlxSprite;
public var beatSeparator:FlxBackdrop; public var beatSeparator:CharterGridSeperator;
public var sectionSeparator:FlxBackdrop;
public var notesGroup:FlxTypedGroup<CharterNote> = new FlxTypedGroup<CharterNote>(); public var notesGroup:FlxTypedGroup<CharterNote> = new FlxTypedGroup<CharterNote>();
public var strumLine:CharterStrumline; public var strumLine:CharterStrumline;
@@ -136,23 +131,14 @@ class CharterBackdrop extends FlxTypedGroup<Dynamic> {
waveformSprite.updateHitbox(); waveformSprite.updateHitbox();
add(waveformSprite); add(waveformSprite);
sectionSeparator = new FlxBackdrop(null, Y, 0, 0); beatSeparator = new CharterGridSeperator();
sectionSeparator.y = -2; beatSeparator.makeSolid(1, 1, -1);
sectionSeparator.visible = Options.charterShowSections; beatSeparator.alpha = 0.5;
beatSeparator.scrollFactor.set(1, 1);
beatSeparator = new FlxBackdrop(null, Y, 0, 0); beatSeparator.scale.set((4 * 40), 2);
beatSeparator.y = -1; beatSeparator.updateHitbox();
beatSeparator.visible = Options.charterShowBeats;
for(sep in [sectionSeparator, beatSeparator]) {
sep.makeSolid(1, 1, -1);
sep.alpha = 0.5;
sep.scrollFactor.set(1, 1);
sep.scale.set((4 * 40), sep == sectionSeparator ? 4 : 2);
sep.updateHitbox();
}
add(beatSeparator); add(beatSeparator);
add(sectionSeparator);
add(notesGroup); add(notesGroup);
bottomSeparator = new FlxSprite(0,-2); bottomSeparator = new FlxSprite(0,-2);
@@ -200,15 +186,12 @@ class CharterBackdrop extends FlxTypedGroup<Dynamic> {
alpha = strumLine.strumLine.visible ? 0.9 : 0.4; alpha = strumLine.strumLine.visible ? 0.9 : 0.4;
} else alpha = 0.9; } else alpha = 0.9;
for (spr in [gridBackDrop, sectionSeparator, beatSeparator, topLimit, bottomLimit, for (spr in [gridBackDrop, beatSeparator, topLimit, bottomLimit,
topSeparator, bottomSeparator, conductorFollowerSpr, waveformSprite]) { topSeparator, bottomSeparator, conductorFollowerSpr, waveformSprite]) {
spr.x = x; if (spr != waveformSprite) spr.alpha = alpha; spr.x = x; if (spr != waveformSprite) spr.alpha = alpha;
spr.cameras = this.cameras; spr.cameras = this.cameras;
} }
sectionSeparator.spacing.y = (10 * Conductor.beatsPerMeasure * Conductor.stepsPerBeat) - 1;
beatSeparator.spacing.y = (20 * Conductor.stepsPerBeat) - 1;
topLimit.scale.set(4 * 40, Math.ceil(FlxG.height / cameras[0].zoom)); topLimit.scale.set(4 * 40, Math.ceil(FlxG.height / cameras[0].zoom));
topLimit.updateHitbox(); topLimit.updateHitbox();
topLimit.y = -topLimit.height; topLimit.y = -topLimit.height;
@@ -237,6 +220,159 @@ class CharterBackdrop extends FlxTypedGroup<Dynamic> {
} }
} }
class CharterGridSeperatorBase extends FlxSprite {
private static var minStep:Float = 0;
private static var maxStep:Float = 0;
private static var minBeat:Float = 0;
private static var maxBeat:Float = 0;
private static var minMeasure:Float = 0;
private static var maxMeasure:Float = 0;
private static var lastMinBeat:Float = -1;
private static var lastMaxBeat:Float = -1;
private static var lastMinMeasure:Float = -1;
private static var lastMaxMeasure:Float = -1;
public static var lastConductorSprY:Float = Math.NEGATIVE_INFINITY;
private static var beatStepTimes:Array<Float> = [];
private static var measureStepTimes:Array<Float> = [];
private static var timeSignatureChangeGaps:Array<Float> = [];
private function recalculateBeats() {
var conductorSprY = Charter.instance.gridBackdrops.conductorSprY;
if (conductorSprY == lastConductorSprY) return;
var zoomOffset = ((FlxG.height * (1/cameras[0].zoom)) * 0.5);
minStep = (conductorSprY - zoomOffset)/40;
maxStep = (conductorSprY + zoomOffset)/40;
var minTime:Float = Conductor.getStepsInTime(minStep);
var maxTime:Float = Conductor.getStepsInTime(maxStep);
var minBpmChange = Conductor.bpmChangeMap[Conductor.getTimeInChangeIndex(minTime)];
var maxBpmChange = Conductor.bpmChangeMap[Conductor.getTimeInChangeIndex(maxTime)];
minBeat = Conductor.getTimeInBeats(minTime);
maxBeat = Conductor.getTimeInBeats(maxTime);
minMeasure = minBpmChange.measureTime + (minBeat - minBpmChange.beatTime) / minBpmChange.beatsPerMeasure;
maxMeasure = maxBpmChange.measureTime + (maxBeat - maxBpmChange.beatTime) / maxBpmChange.beatsPerMeasure;
//cap out the beats/measures at the end of the song
var endTime = Conductor.getStepsInTime(Charter.instance.__endStep);
var endBeat = Conductor.getTimeInBeats(endTime);
var endBpmChange = Conductor.bpmChangeMap[Conductor.getTimeInChangeIndex(endTime)];
var endMeasure = endBpmChange.measureTime + (endBeat - endBpmChange.beatTime) / endBpmChange.beatsPerMeasure;
if (maxBeat > endBeat) maxBeat = endBeat;
if (maxMeasure > endMeasure) maxMeasure = endMeasure;
if (minMeasure < 0) minMeasure = 0;
if (minBeat < 0) minBeat = 0;
//only calculate if needed
if ((minBeat != lastMinBeat) || (maxBeat != lastMaxBeat) || (minMeasure != lastMinMeasure) || (maxMeasure != lastMaxMeasure) || lastConductorSprY == Math.NEGATIVE_INFINITY) {
calculateTimeSignatureGaps();
calculateStepTimes();
lastMinBeat = minBeat;
lastMaxBeat = maxBeat;
lastMinMeasure = minMeasure;
lastMaxMeasure = maxMeasure;
}
lastConductorSprY = conductorSprY;
}
private inline function calculateTimeSignatureGaps() {
//for time signatures that start mid step
timeSignatureChangeGaps.splice(0, timeSignatureChangeGaps.length);
for (i => change in Conductor.bpmChangeMap) {
if (change.stepTime >= minStep && change.stepTime <= maxStep) {
//get step while ignoring the current change
var index:Int = CoolUtil.boundInt(i-1, 0, Conductor.bpmChangeMap.length - 1);
var step:Float = Conductor.getTimeWithBPMInSteps(change.songTime, index, Conductor.getTimeWithIndexInBPM(change.songTime, index));
if (Math.ceil(step) - step > 0 && (step - Math.floor(step)) > FlxMath.EPSILON) { //mid step change
timeSignatureChangeGaps.push(step);
}
}
}
}
private inline function calculateStepTimes() {
beatStepTimes.splice(0, beatStepTimes.length);
for (i in Math.floor(minBeat)...Math.ceil(maxBeat)) {
beatStepTimes.push(Conductor.getTimeInSteps(Conductor.getBeatsInTime(i)));
}
measureStepTimes.splice(0, measureStepTimes.length);
for (i in Math.floor(minMeasure)...Math.ceil(maxMeasure)) {
measureStepTimes.push(Conductor.getTimeInSteps(Conductor.getMeasuresInTime(i)));
}
}
override public function draw() {
//should only need to recalculate once per frame and will be shared across each instance
recalculateBeats();
drawTimeSignatureChangeGaps();
if (Options.charterShowBeats) drawBeats();
if (Options.charterShowSections) drawMeasures();
}
private function drawBeats(offset:Float = 0.0) {
for (i in beatStepTimes) {
y = (i*40)+offset;
super.draw();
}
}
private function drawMeasures(offset:Float = 0.0) {
for (i in measureStepTimes) {
y = (i*40)+offset;
super.draw();
}
}
private function drawTimeSignatureChangeGaps() {
if (timeSignatureChangeGaps.length == 0) return;
var prevColor = color;
var prevBlend = blend;
color = 0xFF888888;
blend = MULTIPLY;
for (step in timeSignatureChangeGaps) {
y = step*40;
var diff = Math.ceil(step) - step;
scale.y = diff*40;
updateHitbox();
super.draw();
}
color = prevColor;
blend = prevBlend;
}
}
class CharterGridSeperator extends CharterGridSeperatorBase {
override private function drawBeats(offset:Float = 0.0) {
scale.y = 2;
updateHitbox();
super.drawBeats(-2);
}
override private function drawMeasures(offset:Float = 0.0) {
scale.y = 4;
updateHitbox();
super.drawMeasures(-3);
}
}
class CharterBackdropDummy extends UISprite { class CharterBackdropDummy extends UISprite {
var parent:CharterBackdropGroup; var parent:CharterBackdropGroup;
public function new(parent:CharterBackdropGroup) { public function new(parent:CharterBackdropGroup) {
@@ -258,8 +394,7 @@ class CharterBackdropDummy extends UISprite {
} }
class EventBackdrop extends FlxBackdrop { class EventBackdrop extends FlxBackdrop {
public var eventBeatSeparator:FlxBackdrop; public var eventBeatSeparator:CharterEventGridSeperator;
public var eventSecSeparator:FlxBackdrop;
public var topSeparator:FlxSprite; public var topSeparator:FlxSprite;
public var bottomSeparator:FlxSprite; public var bottomSeparator:FlxSprite;
@@ -269,25 +404,10 @@ class EventBackdrop extends FlxBackdrop {
alpha = 0.9; alpha = 0.9;
// Separators // Separators
eventSecSeparator = new FlxBackdrop(null, Y, 0, 0); eventBeatSeparator = new CharterEventGridSeperator();
eventSecSeparator.y = -2; eventBeatSeparator.makeSolid(1, 1, -1);
eventSecSeparator.visible = Options.charterShowSections; eventBeatSeparator.alpha = 0.5;
eventBeatSeparator.scrollFactor.set(1, 1);
eventBeatSeparator = new FlxBackdrop(null, Y, 0, 0);
eventBeatSeparator.y = -1;
eventBeatSeparator.visible = Options.charterShowBeats;
for(sep in [eventSecSeparator, eventBeatSeparator]) {
sep.makeSolid(1, 1, -1);
sep.alpha = 0.5;
sep.scrollFactor.set(1, 1);
}
eventSecSeparator.scale.set(20, 4);
eventSecSeparator.updateHitbox();
eventBeatSeparator.scale.set(10, 2);
eventBeatSeparator.updateHitbox();
bottomSeparator = new FlxSprite(0,-2); bottomSeparator = new FlxSprite(0,-2);
bottomSeparator.makeSolid(1, 1, -1); bottomSeparator.makeSolid(1, 1, -1);
@@ -308,23 +428,32 @@ class EventBackdrop extends FlxBackdrop {
public override function draw() { public override function draw() {
super.draw(); super.draw();
eventSecSeparator.spacing.y = (10 * Conductor.beatsPerMeasure * Conductor.stepsPerBeat) - 1;
eventBeatSeparator.spacing.y = (20 * Conductor.stepsPerBeat) - 1;
eventSecSeparator.cameras = cameras;
eventSecSeparator.x = (x+width) - 20;
if (eventSecSeparator.visible) eventSecSeparator.draw();
eventBeatSeparator.cameras = cameras; eventBeatSeparator.cameras = cameras;
eventBeatSeparator.x = (x+width) - 10; eventBeatSeparator.xPos = x+width;
if (eventBeatSeparator.visible) eventBeatSeparator.draw(); eventBeatSeparator.draw();
topSeparator.x = (x+width) - 20; topSeparator.x = (x+width) - 20;
topSeparator.cameras = this.cameras; topSeparator.cameras = this.cameras;
if (!eventSecSeparator.visible) topSeparator.draw(); if (!Options.charterShowSections) topSeparator.draw();
bottomSeparator.x = (x+width) - 20; bottomSeparator.x = (x+width) - 20;
bottomSeparator.cameras = this.cameras; bottomSeparator.cameras = this.cameras;
bottomSeparator.draw(); bottomSeparator.draw();
} }
}
class CharterEventGridSeperator extends CharterGridSeperatorBase {
public var xPos:Float = 0.0;
override private function drawBeats(offset:Float = 0.0) {
scale.set(10, 2);
updateHitbox();
x = xPos-10;
super.drawBeats(-2);
}
override private function drawMeasures(offset:Float = 0.0) {
scale.set(20, 4);
updateHitbox();
x = xPos-20;
super.drawMeasures(-3);
}
override private function drawTimeSignatureChangeGaps() {}
} }
+379 -32
View File
@@ -1,12 +1,18 @@
package funkin.editors.charter; package funkin.editors.charter;
import funkin.backend.shaders.CustomShader;
import funkin.backend.system.Conductor;
import flixel.group.FlxSpriteGroup;
import flixel.math.FlxPoint; import flixel.math.FlxPoint;
import flixel.system.FlxAssets.FlxGraphicAsset; import flixel.system.FlxAssets.FlxGraphicAsset;
import funkin.backend.chart.ChartData.ChartEvent; import funkin.backend.chart.ChartData.ChartEvent;
import funkin.backend.scripting.DummyScript;
import funkin.backend.scripting.Script;
import funkin.editors.charter.Charter.ICharterSelectable; import funkin.editors.charter.Charter.ICharterSelectable;
import funkin.editors.charter.CharterBackdropGroup.EventBackdrop; import funkin.editors.charter.CharterBackdropGroup.EventBackdrop;
import funkin.game.Character; import funkin.game.Character;
import funkin.game.HealthIcon; import funkin.game.HealthIcon;
import openfl.display.BitmapData;
class CharterEvent extends UISliceSprite implements ICharterSelectable { class CharterEvent extends UISliceSprite implements ICharterSelectable {
public var events:Array<ChartEvent>; public var events:Array<ChartEvent>;
@@ -45,19 +51,78 @@ class CharterEvent extends UISliceSprite implements ICharterSelectable {
sprite.colorTransform = colorTransform; sprite.colorTransform = colorTransform;
} }
/**
* Pack data is a list of 4 strings separated by `________PACKSEP________`
* [0] Event Name
* [1] Event Script
* [2] Event JSON Info
* [3] Event Icon
* [4] Event UI Script / Icon Script
**/
@:dox(hide) public static function getPackData(name:String):Array<String> {
var packFile = Paths.pack('events/${name}');
if (Assets.exists(packFile)) {
return Assets.getText(packFile).split('________PACKSEP________');
}
return null;
}
@:dox(hide) public static function getUIScript(event:ChartEvent, caller:String):Script {
var uiScript = Paths.script('data/events/${event.name}.ui');
var script:Script = null;
if(Assets.exists(uiScript)) {
script = Script.create(uiScript);
} else {
var packData = getPackData(event.name);
if(packData != null) {
var scriptFile = packData[4];
if(scriptFile != null) {
script = Script.fromString(scriptFile, uiScript);
}
}
}
if(script != null && !(script is DummyScript)) {
// classes and functions
script.set("EventIconGroup", EventIconGroup); // automatically imported
script.set("EventNumber", EventNumber); // automatically imported
script.set("getIconFromStrumline", getIconFromStrumline);
script.set("getIconFromCharName", getIconFromCharName);
script.set("generateDefaultIcon", generateDefaultIcon);
script.set("generateEventIconDurationArrow", generateEventIconDurationArrow);
script.set("generateEventIconNumbers", generateEventIconNumbers);
script.set("generateEventIconWarning", generateEventIconWarning);
script.set("getPackData", getPackData);
script.set("getEventComponent", getEventComponent);
// data
script.set("event", event);
script.set("caller", caller);
script.load();
}
return script;
}
/**
* Generates the default event icon for the wanted event
* @param name The name of the event
* @return The icon
**/
private static function generateDefaultIcon(name:String) { private static function generateDefaultIcon(name:String) {
var isBase64:Bool = false; var isBase64:Bool = false;
var path:String = Paths.image('editors/charter/event-icons/$name'); var path:String = Paths.image('editors/charter/event-icons/$name');
var defaultPath = Paths.image('editors/charter/event-icons/Unknown'); var defaultPath = Paths.image('editors/charter/event-icons/Unknown');
if (!Assets.exists(path)) path = defaultPath; if(!Assets.exists(path)) {
path = defaultPath;
var packPath = Paths.pack('events/$name'); var packData = getPackData(name);
if (Assets.exists(packPath)) { if(packData != null) {
var packText = Assets.getText(packPath).split('________PACKSEP________'); var packImg = packData[3];
var packImg = packText[3]; if(packImg != null && packImg.length > 0) {
if(packImg != null && packImg.length > 0) { isBase64 = !packImg.startsWith("assets/");
isBase64 = !packImg.startsWith("assets/"); path = packImg;
path = packImg; }
} }
} }
path = path.trim(); path = path.trim();
@@ -75,27 +140,177 @@ class CharterEvent extends UISliceSprite implements ICharterSelectable {
graphic = defaultPath; graphic = defaultPath;
} }
return new FlxSprite().loadGraphic(graphic); return new FlxSprite(graphic);
} }
public static function generateEventIcon(event:ChartEvent) { /**
return switch(event.name) { * Gets a component sprite from the editors/charter/event-icons/components folder
default: * If you wanna use a number, please use the EventNumber class instead
generateDefaultIcon(event.name); * @param type The type of component to get
case "Camera Movement": * @param x The x position of the sprite (optional)
// custom icon for camera movement * @param y The y position of the sprite (optional)
var state:Charter = cast FlxG.state; * @return The component sprite
if (event.params != null && event.params[0] != null && event.params[0] >= 0 && event.params[0] < state.strumLines.length) { **/
// camera movement, use health icon public static function getEventComponent(type:String, x:Float = 0.0, y:Float = 0.0) {
var icon = Character.getIconFromCharName(state.strumLines.members[event.params[0]].strumLine.characters[0]); var componentPath = Paths.image("editors/charter/event-icons/components/" + type);
var healthIcon = new HealthIcon(icon); if(Assets.exists(componentPath))
healthIcon.setUnstretchedGraphicSize(32, 32, false); return new FlxSprite(x, y, componentPath);
healthIcon.scrollFactor.set(1, 1);
healthIcon.active = false; Logs.trace('Could not find component $type', WARNING);
healthIcon; return null;
} else }
generateDefaultIcon(event.name);
/**
* Expected to be called from inside of a ui script,
* calling this elsewhere might cause unexpected results or crashes
**/
public static function getIconFromStrumline(index:Null<Int>) {
var state = cast(FlxG.state, Charter);
if (index != null && index >= 0 && index < state.strumLines.length) {
return getIconFromCharName(state.strumLines.members[index].strumLine.characters[0]);
} }
return null;
}
public static function getIconFromCharName(name:String) {
var icon = Character.getIconFromCharName(name);
var healthIcon = new HealthIcon(icon);
CoolUtil.setUnstretchedGraphicSize(healthIcon, 32, 32, false);
healthIcon.scrollFactor.set(1, 1);
healthIcon.active = false;
return healthIcon;
}
public static function generateEventIcon(event:ChartEvent, inMenu:Bool = true):FlxSprite {
var script = getUIScript(event, "event-icon");
if(script != null && !(script is DummyScript)) {
script.set("inMenu", inMenu);
if(script.get("generateIcon") != null) {
var res:FlxSprite = script.call("generateIcon");
if(res != null)
return res;
}
}
switch(event.name) {
case "Time Signature Change":
if(event.params != null && (event.params[0] >= 0 || event.params[1] >= 0)) {
var group = new EventIconGroup();
group.add(generateDefaultIcon(event.name));
group.add({ // top
var num = new EventNumber(9, -1, event.params[0], EventNumber.ALIGN_CENTER);
num.active = false;
num;
});
group.add({ // bottom
var num = new EventNumber(9, 10, event.params[1], EventNumber.ALIGN_CENTER);
num.active = false;
num;
});
if (Conductor.invalidEvents.contains(event)) generateEventIconWarning(group);
return group;
}
case "Continuous BPM Change":
if(event.params != null && event.params[1] != null) {
var group = new EventIconGroup();
group.add(generateDefaultIcon("BPM Change Start"));
if (!inMenu) {
generateEventIconDurationArrow(group, event.params[1]);
group.members[0].y -= 2;
generateEventIconNumbers(group, event.params[0], 3);
}
if (Conductor.invalidEvents.contains(event)) generateEventIconWarning(group);
return group;
} else {
return generateDefaultIcon("BPM Change Start");
}
case "BPM Change":
if(event.params != null && event.params[0] != null) {
var group = new EventIconGroup();
group.add(generateDefaultIcon(event.name));
if (!inMenu) {
group.members[0].y -= 2;
generateEventIconNumbers(group, event.params[0], 3);
}
if (Conductor.invalidEvents.contains(event)) generateEventIconWarning(group);
return group;
}
case "Scroll Speed Change":
if(event.params != null && !inMenu) {
var group = new EventIconGroup();
group.add(generateDefaultIcon(event.name));
if (event.params[0]) generateEventIconDurationArrow(group, event.params[2]);
group.members[0].y -= 2;
generateEventIconNumbers(group, event.params[1]);
return group;
}
case "Camera Movement":
// camera movement, use health icon
if(event.params != null) {
var icon = getIconFromStrumline(event.params[0]);
if(icon != null) return icon;
}
}
return generateDefaultIcon(event.name);
}
private static function generateEventIconNumbers(group:EventIconGroup, number:Float, x:Float = 4, y:Float = 15, spacing:Float = 5, precision:Int = 3) {
group.add({
var num = new EventNumber(x, y, number, EventNumber.ALIGN_CENTER, spacing, precision);
if (num.numWidth > 20) {
num.scale.x = num.scale.y = 20 / num.numWidth;
}
num.active = false;
num;
});
}
private static function generateEventIconDurationArrow(group:EventIconGroup, stepDuration:Float) {
//var group = new EventIconGroup();
//group.add(generateDefaultIcon(startIcon));
var xOffset = 4;
var yGap = 24;
var endGap = 2;
if (stepDuration >= 0.55) { //min time for showing arrow
var tail = new FlxSprite(xOffset, yGap);
var arrow = new FlxSprite(xOffset, (stepDuration * 40) + endGap);
var arrowSegment = new FlxSprite(xOffset, yGap);
tail.frames = arrow.frames = arrowSegment.frames = Paths.getSparrowAtlas("editors/charter/event-icons/components/arrow-down");
group.add({
tail.animation.addByPrefix("tail", "tail");
tail.animation.play("tail");
tail;
});
group.add({
arrowSegment.animation.addByPrefix("segment", "segment");
arrowSegment.animation.play("segment");
arrowSegment.scale.y = endGap + (stepDuration * 40) - (tail.height + yGap);
arrowSegment.updateHitbox();
arrowSegment.y += tail.height;
arrowSegment;
});
group.add({
arrow.animation.addByPrefix("arrow", "arrow");
arrow.animation.play("arrow");
arrow;
});
}
}
private static function generateEventIconWarning(group:EventIconGroup) {
for (spr in group) {
spr.colorTransform.redMultiplier = spr.colorTransform.greenMultiplier = spr.colorTransform.blueMultiplier = 0.5;
spr.colorTransform.redOffset = 100;
}
group.add(getEventComponent("warning", 16, -8));
group.copyColorTransformToChildren = false;
} }
public override function onHovered() { public override function onHovered() {
@@ -123,18 +338,150 @@ class CharterEvent extends UISliceSprite implements ICharterSelectable {
} }
for(event in events) { for(event in events) {
var spr = generateEventIcon(event); var spr = generateEventIcon(event, false);
icons.push(spr); icons.push(spr);
members.push(spr); members.push(spr);
} }
draggable = true; draggable = true;
for (event in events)
if (event.name == "BPM Change") {
draggable = false;
break;
}
x = (snappedToGrid && eventsBackdrop != null ? eventsBackdrop.x : 0) - (bWidth = 37 + (icons.length * 22)); x = (snappedToGrid && eventsBackdrop != null ? eventsBackdrop.x : 0) - (bWidth = 37 + (icons.length * 22));
} }
}
class EventIconGroup extends FlxSpriteGroup {
public var forceWidth:Float = 16;
public var forceHeight:Float = 16;
public var dontTransformChildren:Bool = true;
public var copyColorTransformToChildren:Bool = true;
public function new() {
super();
scrollFactor.set(1, 1);
}
override function preAdd(sprite:FlxSprite):Void
{
super.preAdd(sprite);
sprite.scrollFactor.set(1, 1);
}
public override function transformChildren<V>(Function:FlxSprite->V->Void, Value:V):Void
{
if (dontTransformChildren)
return;
super.transformChildren(Function, Value);
}
override function set_x(Value:Float):Float
{
if (exists && x != Value)
transformChildren(xTransform, Value - x); // offset
return x = Value;
}
override function set_y(Value:Float):Float
{
if (exists && y != Value)
transformChildren(yTransform, Value - y); // offset
return y = Value;
}
override function get_width() {
return forceWidth;
}
override function get_height() {
return forceHeight;
}
override public function draw() {
@:privateAccess
if (copyColorTransformToChildren && colorTransform != null) for (child in members) child.colorTransform.__copyFrom(colorTransform);
super.draw();
}
override function update(elapsed:Float) {
super.update(elapsed);
}
}
class EventNumber extends FlxSprite {
public static inline final ALIGN_NORMAL:Int = 0;
public static inline final ALIGN_CENTER:Int = 1;
public var digits:Array<Int> = [];
public static inline final FRAME_POINT:Int = 10;
public static inline final FRAME_NEGATIVE:Int = 11;
public var align:Int = ALIGN_NORMAL;
public var spacing:Float = 6;
public function new(x:Float, y:Float, number:Float, ?align:Int, spacing:Float = 6, precision:Int = 3) {
super(x, y);
this.digits = [];
this.align = align == null ? ALIGN_NORMAL : align;
this.spacing = spacing;
if (number == 0) {
this.digits.insert(0, 0);
}
else {
var decimals:Float = FlxMath.roundDecimal(Math.abs(number % 1), precision);
if (decimals > 0) this.digits.insert(0, FRAME_POINT);
while(decimals > 0) {
this.digits.push(Math.floor(decimals * 10));
decimals = FlxMath.roundDecimal((decimals * 10) % 1, precision);
}
var ints = Std.int(Math.abs(number));
if (ints == 0) this.digits.insert(0, 0);
while (ints > 0) {
this.digits.insert(0, ints % 10);
ints = Std.int(ints / 10);
}
if (number < 0) {
this.digits.insert(0, FRAME_NEGATIVE);
}
}
loadGraphic(Paths.image('editors/charter/event-icons/components/eventNums'), true, 6, 7);
}
override function update(elapsed:Float) {
super.update(elapsed);
}
override function draw() {
var baseX = x;
var offsetX = 0.0;
if(align == ALIGN_CENTER) offsetX = -(digits.length - 1) * spacing * Math.abs(scale.x) / 2;
x = baseX + offsetX;
for (i in 0...digits.length) {
frame = frames.frames[digits[i]];
super.draw();
x += spacing * Math.abs(scale.x);
}
x = baseX;
}
public var numWidth(get, never):Float;
private function get_numWidth():Float {
return Math.abs(scale.x) * spacing * digits.length;
}
public var numHeight(get, never):Float;
private function get_numHeight():Float {
return Math.abs(scale.y) * frameHeight;
}
public override function updateHitbox():Void {
var numWidth = this.numWidth;
var numHeight = this.numHeight;
width = numWidth;
height = numHeight;
offset.set(-0.5 * (numWidth - spacing * digits.length), -0.5 * (numHeight - frameHeight));
centerOrigin();
}
} }
@@ -57,7 +57,7 @@ class CharterEventScreen extends UISubstateWindow {
}); });
eventsList.add(new EventButton(events[events.length-1], CharterEvent.generateEventIcon(events[events.length-1]), events.length-1, this, eventsList)); eventsList.add(new EventButton(events[events.length-1], CharterEvent.generateEventIcon(events[events.length-1]), events.length-1, this, eventsList));
changeTab(events.length-1); changeTab(events.length-1);
})); }, chartEvent.step));
for (k=>i in events) for (k=>i in events)
eventsList.add(new EventButton(i, CharterEvent.generateEventIcon(i), k, this, eventsList)); eventsList.add(new EventButton(i, CharterEvent.generateEventIcon(i), k, this, eventsList));
add(eventsList); add(eventsList);
@@ -82,12 +82,12 @@ class CharterEventScreen extends UISubstateWindow {
else { else {
chartEvent.events = [for (i in eventsList.buttons.members) i.event]; chartEvent.events = [for (i in eventsList.buttons.members) i.event];
chartEvent.refreshEventIcons(); chartEvent.refreshEventIcons();
Charter.instance.updateBPMEvents();
Charter.undos.addToUndo(CEditEvent(chartEvent, oldEvents, [for (event in events) Reflect.copy(event)])); Charter.undos.addToUndo(CEditEvent(chartEvent, oldEvents, [for (event in events) Reflect.copy(event)]));
} }
} }
Charter.instance.updateBPMEvents();
close(); close();
}); });
saveButton.x -= saveButton.bWidth; saveButton.x -= saveButton.bWidth;
@@ -112,23 +112,17 @@ class CharterEventScreen extends UISubstateWindow {
// destroy old elements // destroy old elements
paramsFields = []; paramsFields = [];
for(e in paramsPanel) { while(paramsPanel.members.length > 0) {
e.destroy(); var e = paramsPanel.members.pop();
paramsPanel.remove(e); if(e != null) {
e.destroy();
}
} }
if (id >= 0 && id < events.length) { if (id >= 0 && id < events.length) {
curEvent = id; curEvent = id;
var curEvent = events[curEvent]; var curEvent = events[curEvent];
eventName.text = curEvent.name; eventName.text = curEvent.name;
// add new elements
var y:Float = eventName.y + eventName.height + 10;
for(k=>param in EventsData.getEventParams(curEvent.name)) {
function addLabel() {
var label:UIText = new UIText(eventName.x, y, 0, param.name);
y += label.height + 4;
paramsPanel.add(label);
};
var value:Dynamic = CoolUtil.getDefault(curEvent.params[k], param.defValue); var value:Dynamic = CoolUtil.getDefault(curEvent.params[k], param.defValue);
var lastAdded = switch(param.type) { var lastAdded = switch(param.type) {
@@ -199,6 +193,88 @@ class CharterEventScreen extends UISubstateWindow {
} }
} }
function generateEventUI(event:ChartEvent):Void {
var script = CharterEvent.getUIScript(event, "event-ui");
if(script != null && !(script is DummyScript)) {
script.set("paramsPanel", paramsPanel);
script.set("paramsFields", paramsFields);
if(script.get("generateUI") != null) {
if(script.call("generateUI") == false)
return;
}
}
// add new elements
var _y:Float = eventName.y + eventName.height + 10;
var params = EventsData.getEventParams(event.name);
for(k=>param in params) {
var x = eventName.x + (param.x == null ? 0 : param.x);
var y = (param.y == null ? _y : param.y);
function addLabel() {
var label:UIText = new UIText(x, y, 0, param.name);
_y += label.height + 4;
y += label.height + 4;
paramsPanel.add(label);
};
var value:Dynamic = CoolUtil.getDefault(event.params[k], param.defValue);
var lastAdded = switch(param.type) {
case TString:
addLabel();
var textBox:UITextBox = new UITextBox(x, y, cast value);
paramsPanel.add(textBox); paramsFields.push(textBox);
textBox;
case TBool:
var checkbox = new UICheckbox(x, y, param.name, cast value);
paramsPanel.add(checkbox); paramsFields.push(checkbox);
checkbox;
case TInt(min, max, step):
addLabel();
var numericStepper = new UINumericStepper(x, y, cast value, CoolUtil.getDefault(step, 1), 0, min, max);
paramsPanel.add(numericStepper); paramsFields.push(numericStepper);
numericStepper;
case TFloat(min, max, step, precision):
addLabel();
var numericStepper = new UINumericStepper(x, y, cast value, CoolUtil.getDefault(step, 1), precision, min, max);
paramsPanel.add(numericStepper); paramsFields.push(numericStepper);
numericStepper;
case TStrumLine:
addLabel();
var dropdown = new UIDropDown(x, y, 320, 32, [
for(k=>s in cast(FlxG.state, Charter).strumLines.members)
'Strumline #${k+1} (${s.strumLine.characters[0]})'
], cast value);
paramsPanel.add(dropdown); paramsFields.push(dropdown);
dropdown;
case TColorWheel:
addLabel();
var colorWheel = new UIColorwheel(x, y, CoolUtil.getColorFromDynamic(value));
paramsPanel.add(colorWheel); paramsFields.push(colorWheel);
colorWheel;
case TDropDown(options):
addLabel();
var dropdown = new UIDropDown(x, y, 320, 32, options, Std.int(Math.abs(options.indexOf(cast value))));
paramsPanel.add(dropdown); paramsFields.push(dropdown);
dropdown;
default:
paramsFields.push(null);
null;
}
if (lastAdded is UISliceSprite)
_y += cast(lastAdded, UISliceSprite).bHeight + 4;
else if (lastAdded is FlxSprite)
_y += cast(lastAdded, FlxSprite).height + 6;
}
if(script != null && !(script is DummyScript)) {
if(script.get("postGenerateUI") != null) {
script.set("params", params);
script.call("postGenerateUI");
}
}
}
public function saveCurTab() { public function saveCurTab() {
if (curEvent < 0) return; if (curEvent < 0) return;
@@ -1,9 +1,11 @@
package funkin.editors.charter; package funkin.editors.charter;
import funkin.backend.system.Conductor;
import funkin.backend.chart.EventsData; import funkin.backend.chart.EventsData;
class CharterEventTypeSelection extends UISubstateWindow { class CharterEventTypeSelection extends UISubstateWindow {
var callback:String->Void; var callback:String->Void;
var eventStepTime:Float;
var buttons:Array<UIButton> = []; var buttons:Array<UIButton> = [];
@@ -13,9 +15,10 @@ class CharterEventTypeSelection extends UISubstateWindow {
var upIndicator:UIText; var upIndicator:UIText;
var downIndicator:UIText; var downIndicator:UIText;
public function new(callback:String->Void) { public function new(callback:String->Void, eventStepTime:Float) {
super(); super();
this.callback = callback; this.callback = callback;
this.eventStepTime = eventStepTime;
} }
public override function create() { public override function create() {
@@ -32,6 +35,15 @@ class CharterEventTypeSelection extends UISubstateWindow {
buttonsBG.frames = Paths.getFrames('editors/ui/inputbox'); buttonsBG.frames = Paths.getFrames('editors/ui/inputbox');
add(buttonsBG); add(buttonsBG);
var disableConductorEvents:Bool = false;
var disableOnlyContinuousChanges:Bool = false;
for (change in Conductor.bpmChangeMap) {
if (change.continuous && MathUtil.greaterThanEqual(eventStepTime, change.stepTime) && MathUtil.lessThan(eventStepTime, change.endStepTime)) {
disableOnlyContinuousChanges = MathUtil.equal(eventStepTime, change.stepTime); //allow time sig and instant bpm changes on the same event
disableConductorEvents = !disableOnlyContinuousChanges;
}
}
for(k=>eventName in EventsData.eventsList) { for(k=>eventName in EventsData.eventsList) {
var button = new UIButton(0, (32 * k), eventName, function() { var button = new UIButton(0, (32 * k), eventName, function() {
close(); close();
@@ -52,6 +64,11 @@ class CharterEventTypeSelection extends UISubstateWindow {
icon.x = button.x + 8; icon.x = button.x + 8;
icon.y = button.y + Math.abs(button.bHeight - icon.height) / 2; icon.y = button.y + Math.abs(button.bHeight - icon.height) / 2;
add(icon); add(icon);
if (disableConductorEvents && (eventName == "Time Signature Change" || eventName == "Continuous BPM Change" || eventName == "BPM Change") || (disableOnlyContinuousChanges && eventName == "Continuous BPM Change")) {
button.selectable = button.shouldPress = false;
button.autoAlpha = true;
}
} }
windowSpr.bHeight = 61 + (32 * (17)); windowSpr.bHeight = 61 + (32 * (17));
@@ -79,7 +96,7 @@ class CharterEventTypeSelection extends UISubstateWindow {
sinner += elapsed; sinner += elapsed;
for (button in buttons) for (button in buttons)
button.selectable = buttonsBG.hovered; if (button.shouldPress) button.selectable = buttonsBG.hovered;
buttonCameras.zoom = subCam.zoom; buttonCameras.zoom = subCam.zoom;
@@ -62,8 +62,8 @@ class CharterMetaDataScreen extends UISubstateWindow {
add(new UIText(beatsPerMeasureStepper.x + 30, beatsPerMeasureStepper.y + 3, 0, "/", 22)); add(new UIText(beatsPerMeasureStepper.x + 30, beatsPerMeasureStepper.y + 3, 0, "/", 22));
stepsPerBeatStepper = new UINumericStepper(beatsPerMeasureStepper.x + 30 + 24, beatsPerMeasureStepper.y, metadata.stepsPerBeat, 1, 0, 1, null, 54); denominatorStepper = new UINumericStepper(beatsPerMeasureStepper.x + 30 + 24, beatsPerMeasureStepper.y, Math.floor(16 / metadata.stepsPerBeat), 1, 0, 1, null, 54);
add(stepsPerBeatStepper); add(denominatorStepper);
add(title = new UIText(songNameTextBox.x, songNameTextBox.y + 10 + 46, 0, "Menus Data (Freeplay/Story)", 28)); add(title = new UIText(songNameTextBox.x, songNameTextBox.y + 10 + 46, 0, "Menus Data (Freeplay/Story)", 28));
@@ -136,7 +136,7 @@ class CharterMetaDataScreen extends UISubstateWindow {
} }
public function saveMeta() { public function saveMeta() {
for (stepper in [bpmStepper, beatsPerMeasureStepper, stepsPerBeatStepper]) for (stepper in [bpmStepper, beatsPerMeasureStepper, denominatorStepper])
@:privateAccess stepper.__onChange(stepper.label.text); @:privateAccess stepper.__onChange(stepper.label.text);
var customVals = {}; var customVals = {};
@@ -107,8 +107,8 @@ class SongCreationScreen extends UISubstateWindow {
songDataGroup.add(new UIText(beatsPerMeasureStepper.x + 30, beatsPerMeasureStepper.y + 3, 0, "/", 22)); songDataGroup.add(new UIText(beatsPerMeasureStepper.x + 30, beatsPerMeasureStepper.y + 3, 0, "/", 22));
stepsPerBeatStepper = new UINumericStepper(beatsPerMeasureStepper.x + 30 + 24, beatsPerMeasureStepper.y, 4, 1, 0, 1, null, 54); denominatorStepper = new UINumericStepper(beatsPerMeasureStepper.x + 30 + 24, beatsPerMeasureStepper.y, 4, 1, 0, 1, null, 54);
songDataGroup.add(stepsPerBeatStepper); songDataGroup.add(denominatorStepper);
instExplorer = new UIFileExplorer(songNameTextBox.x, songNameTextBox.y + 32 + 36, null, null, Flags.SOUND_EXT, function (res) { instExplorer = new UIFileExplorer(songNameTextBox.x, songNameTextBox.y + 32 + 36, null, null, Flags.SOUND_EXT, function (res) {
var audioPlayer:UIAudioPlayer = new UIAudioPlayer(instExplorer.x + 8, instExplorer.y + 8, res); var audioPlayer:UIAudioPlayer = new UIAudioPlayer(instExplorer.x + 8, instExplorer.y + 8, res);
+3 -2
View File
@@ -173,13 +173,14 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset
} }
/** /**
* Whenever the character should dance on beat or not. Set to false for `gf`, since the dance animation is automatically handled by PlayState. * Whenever the character should dance on beat or not.
*/ */
public var danceOnBeat:Bool = true; public var danceOnBeat:Bool = true;
public override function beatHit(curBeat:Int) { public override function beatHit(curBeat:Int) {
scripts.call("beatHit", [curBeat]); scripts.call("beatHit", [curBeat]);
if (danceOnBeat && (curBeat + beatOffset) % beatInterval == 0 && !__lockAnimThisFrame) if (skipNegativeBeats && curBeat < 0) return;
if (danceOnBeat && (curBeat + beatOffset) % (beatInterval * CoolUtil.maxInt(Math.floor(4 / Conductor.stepsPerBeat), 1)) == 0 && !__lockAnimThisFrame)
tryDance(); tryDance();
} }
+21
View File
@@ -1064,7 +1064,13 @@ class PlayState extends MusicBeatState
executeEvent(e); executeEvent(e);
break; break;
} }
];
if (!foundSigs) {
camZoomingInterval = 4;
camZoomingEvery = BEAT;
} }
events.sort(function(p1, p2) { events.sort(function(p1, p2) {
return FlxSort.byValues(FlxSort.DESCENDING, p1.time, p2.time); return FlxSort.byValues(FlxSort.DESCENDING, p1.time, p2.time);
}); });
@@ -1319,6 +1325,15 @@ class PlayState extends MusicBeatState
if (canAccessDebugMenus && chartingMode && controls.DEV_ACCESS) if (canAccessDebugMenus && chartingMode && controls.DEV_ACCESS)
FlxG.switchState(new funkin.editors.charter.Charter(SONG.meta.name, difficulty, false)); FlxG.switchState(new funkin.editors.charter.Charter(SONG.meta.name, difficulty, false));
if (Options.camZoomOnBeat && camZooming && FlxG.camera.zoom < maxCamZoom) {
var beat = Conductor.getBeats(camZoomingEvery, camZoomingInterval, camZoomingOffset);
if (camZoomingLastBeat != beat) {
camZoomingLastBeat = beat;
FlxG.camera.zoom += 0.015 * camZoomingStrength;
camHUD.zoom += 0.03 * camZoomingStrength;
}
}
if (doIconBop) if (doIconBop)
for (icon in iconArray) for (icon in iconArray)
if (icon.updateBump != null) if (icon.updateBump != null)
@@ -1540,6 +1555,12 @@ class PlayState extends MusicBeatState
case "Camera Modulo Change": case "Camera Modulo Change":
camZoomingInterval = event.params[0]; camZoomingInterval = event.params[0];
camZoomingStrength = event.params[1]; camZoomingStrength = event.params[1];
if (event.params[2] != null) camZoomingEvery = switch (event.params[2].toUpperCase()) {
case "STEP": STEP;
case "MEASURE": MEASURE;
default: BEAT;
}
if (event.params[3] != null) camZoomingOffset = event.params[3];
case "Camera Flash": case "Camera Flash":
var camera:FlxCamera = event.params[3] == "camHUD" ? camHUD : camGame; var camera:FlxCamera = event.params[3] == "camHUD" ? camHUD : camGame;
@@ -53,7 +53,7 @@ class GameplayOptions extends OptionsScreen {
__lastBeat = Conductor.curBeat; __lastBeat = Conductor.curBeat;
} }
var beat = Math.floor(Conductor.getStepForTime(FlxG.sound.music.time) / Conductor.stepsPerBeat); var beat = Math.floor(Conductor.getTimeInBeats(FlxG.sound.music.time));
if (__lastSongBeat != beat) { if (__lastSongBeat != beat) {
__metronome.replay(); __metronome.replay();
__lastSongBeat = beat; __lastSongBeat = beat;