diff --git a/.gitignore b/.gitignore index 0b7c366da..2931e2d59 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ latest/windows/haxe/ pages/ docs/doc.xml mods/* -!mods/readme.txt \ No newline at end of file +!mods/readme.txt diff --git a/assets/images/editors/charter/event-icons/BPM Change End.png b/assets/images/editors/charter/event-icons/BPM Change End.png new file mode 100644 index 000000000..6d5dd527c Binary files /dev/null and b/assets/images/editors/charter/event-icons/BPM Change End.png differ diff --git a/assets/images/editors/charter/event-icons/BPM Change Start.png b/assets/images/editors/charter/event-icons/BPM Change Start.png new file mode 100644 index 000000000..832a02357 Binary files /dev/null and b/assets/images/editors/charter/event-icons/BPM Change Start.png differ diff --git a/assets/images/editors/charter/event-icons/BPM Change.png b/assets/images/editors/charter/event-icons/BPM Change.png index 40dba09a6..4eefebc39 100644 Binary files a/assets/images/editors/charter/event-icons/BPM Change.png and b/assets/images/editors/charter/event-icons/BPM Change.png differ diff --git a/assets/images/editors/charter/event-icons/Time Signature Change.png b/assets/images/editors/charter/event-icons/Time Signature Change.png new file mode 100644 index 000000000..967146006 Binary files /dev/null and b/assets/images/editors/charter/event-icons/Time Signature Change.png differ diff --git a/assets/images/editors/charter/event-icons/components/arrow-down.png b/assets/images/editors/charter/event-icons/components/arrow-down.png new file mode 100644 index 000000000..f44a87114 Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/arrow-down.png differ diff --git a/assets/images/editors/charter/event-icons/components/arrow-down.xml b/assets/images/editors/charter/event-icons/components/arrow-down.xml new file mode 100644 index 000000000..1e58cc38e --- /dev/null +++ b/assets/images/editors/charter/event-icons/components/arrow-down.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/assets/images/editors/charter/event-icons/components/arrow-right.png b/assets/images/editors/charter/event-icons/components/arrow-right.png new file mode 100644 index 000000000..a5da3a70a Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/arrow-right.png differ diff --git a/assets/images/editors/charter/event-icons/components/cross.png b/assets/images/editors/charter/event-icons/components/cross.png new file mode 100644 index 000000000..a3dee356f Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/cross.png differ diff --git a/assets/images/editors/charter/event-icons/components/end-plus.png b/assets/images/editors/charter/event-icons/components/end-plus.png new file mode 100644 index 000000000..7629e754e Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/end-plus.png differ diff --git a/assets/images/editors/charter/event-icons/components/eventNums.png b/assets/images/editors/charter/event-icons/components/eventNums.png new file mode 100644 index 000000000..8ae1513cb Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/eventNums.png differ diff --git a/assets/images/editors/charter/event-icons/components/flash.png b/assets/images/editors/charter/event-icons/components/flash.png new file mode 100644 index 000000000..2ef5f9492 Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/flash.png differ diff --git a/assets/images/editors/charter/event-icons/components/note.png b/assets/images/editors/charter/event-icons/components/note.png new file mode 100644 index 000000000..4b08e4f5b Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/note.png differ diff --git a/assets/images/editors/charter/event-icons/components/plus.png b/assets/images/editors/charter/event-icons/components/plus.png new file mode 100644 index 000000000..939d4c46f Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/plus.png differ diff --git a/assets/images/editors/charter/event-icons/components/start-plus.png b/assets/images/editors/charter/event-icons/components/start-plus.png new file mode 100644 index 000000000..84b42f136 Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/start-plus.png differ diff --git a/assets/images/editors/charter/event-icons/components/warning.png b/assets/images/editors/charter/event-icons/components/warning.png new file mode 100644 index 000000000..03a423dbf Binary files /dev/null and b/assets/images/editors/charter/event-icons/components/warning.png differ diff --git a/source/funkin/backend/chart/ChartData.hx b/source/funkin/backend/chart/ChartData.hx index c46164f54..93f18ae4b 100644 --- a/source/funkin/backend/chart/ChartData.hx +++ b/source/funkin/backend/chart/ChartData.hx @@ -19,7 +19,7 @@ typedef ChartMetaData = { public var ?bpm:Float; public var ?displayName:String; public var ?beatsPerMeasure:Float; - public var ?stepsPerBeat:Float; + public var ?stepsPerBeat:Int; public var ?needsVoices:Bool; public var ?icon:String; public var ?color:Dynamic; diff --git a/source/funkin/backend/chart/EventsData.hx b/source/funkin/backend/chart/EventsData.hx index 79f0e210b..97eefd7f1 100644 --- a/source/funkin/backend/chart/EventsData.hx +++ b/source/funkin/backend/chart/EventsData.hx @@ -10,7 +10,7 @@ import funkin.backend.assets.Paths; using StringTools; class EventsData { - public static var defaultEventsList:Array = ["HScript Call", "Camera Movement", "Add Camera Zoom", "Camera Modulo Change", "Camera Flash", "BPM Change", "Scroll Speed Change", "Alt Animation Toggle", "Play Animation"]; + public static var defaultEventsList:Array = ["HScript Call", "Camera Movement", "Add 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> = [ "HScript Call" => [ {name: "Function Name", type: TString, defValue: "myFunc"}, @@ -31,7 +31,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 Beat Count", type: TFloat(1), defValue: 4}, {name: "Target Step Count", type: TFloat(1), defValue: 4}], "Scroll Speed Change" => [ {name: "Tween Speed?", type: TBool, defValue: true}, {name: "New Speed", type: TFloat(0.01, 99, 0.01, 2), defValue: 1.}, @@ -48,7 +50,7 @@ class EventsData { } ], "Alt Animation Toggle" => [{name: "Enable On Sing Poses", type: TBool, defValue: true}, {name: "Enable On Idle", type: TBool, defValue: true}, {name: "Strumline", type: TStrumLine, defValue: 0}], - "Play Animation" => [{name: "Character", type: TStrumLine, defValue: 0}, {name: "Animation", type: TString, defValue: "animation"}, {name: "Is forced?", type: TBool, defValue: true}], + "Play Animation" => [{name: "Character", type: TStrumLine, defValue: 0}, {name: "Animation", type: TString, defValue: "animation"}, {name: "Is forced?", type: TBool, defValue: true}] ]; public static var eventsList:Array = defaultEventsList.copy(); @@ -75,11 +77,12 @@ class EventsData { hscriptParser.allowJSON = hscriptParser.allowMetadata = false; for (file in Paths.getFolderContent('data/events/', true, BOTH)) { - if (Path.extension(file) != "json" && Path.extension(file) != "pack") continue; - var eventName:String = Path.withoutExtension(Path.withoutDirectory(file)); + var ext = Path.extension(file); + if (ext != "json" && ext != "pack") continue; + var eventName:String = CoolUtil.getFilename(file); var fileTxt:String = Assets.getText(file); - if (Path.extension(file) == "pack") { + if (ext == "pack") { var arr = fileTxt.split("________PACKSEP________"); eventName = Path.withoutExtension(arr[0]); fileTxt = arr[2]; @@ -97,7 +100,14 @@ class EventsData { var finalParams:Array = []; for (paramData in cast(data.params, Array)) { try { - finalParams.push({name: paramData.name, type: hscriptInterp.expr(hscriptParser.parseString(paramData.type)), defValue: paramData.defaultValue}); + finalParams.push({ + name: paramData.name, + type: hscriptInterp.expr(hscriptParser.parseString(paramData.type)), + defValue: paramData.defaultValue, + + x: paramData.x, + y: paramData.y + }); } catch (e) {trace('Error parsing event param ${paramData.name} - ${eventName}: $e'); finalParams.push(null);} } eventsParams.set(eventName, finalParams); @@ -117,6 +127,9 @@ typedef EventParamInfo = { var name:String; var type:EventParamType; var defValue:Dynamic; + + @:optional var x:Float; + @:optional var y:Float; } enum EventParamType { diff --git a/source/funkin/backend/chart/FNFLegacyParser.hx b/source/funkin/backend/chart/FNFLegacyParser.hx index 61a0f837f..6bef760e8 100644 --- a/source/funkin/backend/chart/FNFLegacyParser.hx +++ b/source/funkin/backend/chart/FNFLegacyParser.hx @@ -51,6 +51,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, @@ -174,7 +186,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)); @@ -188,6 +201,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; diff --git a/source/funkin/backend/system/Conductor.hx b/source/funkin/backend/system/Conductor.hx index bd5fca4cf..10bc8a42e 100644 --- a/source/funkin/backend/system/Conductor.hx +++ b/source/funkin/backend/system/Conductor.hx @@ -1,18 +1,29 @@ package funkin.backend.system; -import funkin.backend.chart.ChartData; import flixel.FlxState; -import funkin.backend.system.interfaces.IBeatReceiver; import flixel.util.FlxSignal.FlxTypedSignal; +import funkin.backend.chart.ChartData; +import funkin.backend.system.interfaces.IBeatReceiver; +import funkin.editors.charter.Charter; -typedef BPMChangeEvent = +@:structInit +class BPMChangeEvent { - var stepTime:Float; - var songTime:Float; - var bpm:Float; + 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; } -class Conductor +final class Conductor { /** * FlxSignals @@ -20,47 +31,76 @@ class Conductor public static var onMeasureHit:FlxTypedSignalVoid> = new FlxTypedSignal(); public static var onBeatHit:FlxTypedSignalVoid> = new FlxTypedSignal(); public static var onStepHit:FlxTypedSignalVoid> = new FlxTypedSignal(); - public static var onBPMChange:FlxTypedSignalVoid> = new FlxTypedSignal(); + public static var onBPMChange:FlxTypedSignal<(Float,Float)->Void> = new FlxTypedSignal(); + public static var onTimeSignatureChange:FlxTypedSignal<(Float,Float)->Void> = new FlxTypedSignal(); /** - * Current BPM + * Current position of the song, in milliseconds. */ - public static var bpm:Float = 100; + public static var songPosition(get, default):Float; + private static function get_songPosition() { + if (songOffset != Options.songOffset) songOffset = Options.songOffset; + return songPosition - songOffset; + } /** - * Current Crochet (time per beat), in milliseconds. + * Offset of the song */ - public static var crochet:Float = ((60 / bpm) * 1000); // beats in milliseconds + public static var songOffset:Float = 0; + /** - * Current StepCrochet (time per step), in milliseconds. + * Current bpmChangeMap index */ - public static var stepCrochet:Float = crochet / 4; // steps in milliseconds + public static var curChangeIndex:Int = 0; /** - * Number of beats per mesure (top number in time signature). Defaults to 4. + * Current bpmChangeMap */ - public static var beatsPerMeasure:Float = 4; + public static var curChange(get, never):Null; + private static function get_curChange() + return bpmChangeMap[curChangeIndex]; /** - * Number of steps per beat (bottom number in time signature). Defaults to 4. + * Current BPM */ - public static var stepsPerBeat:Float = 4; + public static var bpm(get, never):Float; + private static function get_bpm() + return curChangeIndex == 0 ? startingBPM : getTimeWithIndexInBPM(songPosition, curChangeIndex); + /** + * Starting BPM + */ + public static var startingBPM(get, never):Float; + private static function get_startingBPM() + return bpmChangeMap.length == 0 ? 100 : bpmChangeMap[0].bpm; /** - * Current position of the song, in milliseconds. + * Current Crochet (time per beat), in milliseconds. + * It should be crotchet but ehhh, now it's there for backward compatibility. */ - public static var songPosition(get, default):Float; - private static function get_songPosition() { - if (songOffset != Options.songOffset) trace(songOffset = Options.songOffset); - return songPosition - songOffset; - } + public static var crochet(get, never):Float; + private static function get_crochet() return 15000 * stepsPerBeat / bpm; /** - * Offset of the song + * Current StepCrochet (time per step), in milliseconds. */ - public static var songOffset:Float = 0; + 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 ? 4 : bpmChangeMap[curChangeIndex].beatsPerMeasure; + + /** + * Number of steps per beat (bottom number in time signature). Defaults to 4. + */ + public static var stepsPerBeat(get, never):Float; + private static function get_stepsPerBeat() + return bpmChangeMap.length == 0 ? 4 : bpmChangeMap[curChangeIndex].stepsPerBeat; /** * Current step @@ -77,7 +117,6 @@ class Conductor */ public static var curMeasure:Int = 0; - /** * Current step, as a `Float` (ex: 4.94, instead of 4) */ @@ -100,54 +139,154 @@ class Conductor /** * Array of all BPM changes that have been mapped. */ - public static var bpmChangeMap:Array = []; + public static var bpmChangeMap:Array; + + /** + * Array of all events that have been rejected by the Conductor. + */ + public static var invalidEvents:Array = []; @:dox(hide) public function new() {} public static function reset() { songPosition = lastSongPos = curBeatFloat = curStepFloat = curBeat = curStep = 0; - bpmChangeMap = []; - 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) { 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; + + // fix the sort first... + var events:Array = []; + for (e in song.events) if (e.params != null && (e.name == "BPM Change" || e.name == "Time Signature Change" || e.name == "Continuous BPM Change")) events.push(e); + events.sort(function(a, b) { + if (MathUtil.equal(a.time, b.time)) { + if (a.name == "Continuous BPM Change") return 1; + if (b.name == "Continuous BPM Change") return -1; } - ]; + return Std.int(a.time - b.time); + }); - if (song.events == null) return; + for (e in events) { + curChange = mapEvent(e, curChange); + } + } + + 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; + } - var curBPM:Float = song.meta.bpm; - var songTime:Float = 0; - var stepTime:Float = 0; - - 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]; - - bpmChangeMap.push({ - stepTime: stepTime, - songTime: songTime, - bpm: curBPM + 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]; + curChange.stepsPerBeat = params[1]; + + 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]); + var endTime = time + (params[1]) / (curChange.bpm - prevBPM) * Math.log(curChange.bpm / prevBPM) * 15000; + curChange.endStepTime = curChange.stepTime + params[1]; + curChange.continuous = true; + curChange.endSongTime = endTime; + } + 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(function(a, b) { + if (MathUtil.equal(a.time, b.time)) { + if (a.name == "Continuous BPM Change") return 1; + if (b.name == "Continuous BPM Change") return -1; + } + return 0; }); + + var eventTime = Conductor.getTimeForStep(event.step); + for (e in event.events) { + e.time = eventTime; + + if ((e.name == "BPM Change" || e.name == "Time Signature Change" || e.name == "Continuous BPM Change")) { + curChange = mapEvent(e, curChange); + } + + } } + + } private static var elapsed:Float; @@ -177,6 +316,7 @@ class Conductor reset(); } private static var __lastChange:BPMChangeEvent; + private static var __updateStep:Bool; private static var __updateBeat:Bool; private static var __updateMeasure:Bool; @@ -185,125 +325,238 @@ class Conductor __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 (__updateBeat && curBeat > oldBeat) { - for(i in oldBeat...curBeat) { - onBeatHit.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 (__updateMeasure && curMeasure > oldMeasure) { - for(i in oldMeasure...curMeasure) { - onMeasureHit.dispatch(i+1); + + if (__updateBeat) { + if (curBeat > oldBeat) for (i in oldBeat...curBeat) st.beatHit(i + 1); + else st.beatHit(curBeat); } - } - if (FlxG.state is IBeatReceiver) { - var state = FlxG.state; - while(state != null) { - if (state is IBeatReceiver && (state.subState == null || state.persistentUpdate)) { - var st = cast(state, IBeatReceiver); - 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; + if (__updateMeasure) { + if (curMeasure > oldMeasure) for (i in oldMeasure...curMeasure) st.measureHit(i + 1); + else st.measureHit(curMeasure); } } - + state = state.subState; } } } - public static function changeBPM(newBpm:Float, beatsPerMeasure:Float = 4, stepsPerBeat:Float = 4) - { - bpm = newBpm; + 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; + } + } - crochet = ((60 / bpm) * 1000); - stepCrochet = crochet / 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; + } + } - Conductor.beatsPerMeasure = beatsPerMeasure; - Conductor.stepsPerBeat = 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; + } + } - onBPMChange.dispatch(bpm); + 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 getTimeForStep(step:Float) { - var bpmChange:BPMChangeEvent = { - stepTime: 0, - songTime: 0, - bpm: bpm - }; + 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; + } - for(change in bpmChangeMap) - if (change.stepTime < step && change.stepTime >= bpmChange.stepTime) - bpmChange = change; + 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; - return bpmChange.songTime + ((step - bpmChange.stepTime) * ((60 / bpmChange.bpm) * (1000/stepsPerBeat))); + 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 getStepForTime(time:Float) { - var bpmChange:BPMChangeEvent = { - stepTime: 0, - songTime: 0, - bpm: bpm - }; + public static function getTimeInBPM(time:Float):Float { + if (bpmChangeMap.length == 0) return 100; + return getTimeWithIndexInBPM(time, getTimeInChangeIndex(time)); + } - for(change in bpmChangeMap) - if (change.songTime < time && change.songTime >= bpmChange.songTime) - bpmChange = change; + 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); + } + } - return bpmChange.stepTime + ((time - bpmChange.songTime) / ((60 / bpmChange.bpm) * (1000/stepsPerBeat))); + public static function getTimeInBeats(time:Float, from:Int = 0):Float { + var index = getTimeInChangeIndex(time, from); + if (index == -1) return time / (60000 / 100); + 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 * (60000 / 100 / 4); + 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 * (60000 / 100); + 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; } -} +} \ No newline at end of file diff --git a/source/funkin/backend/utils/CoolUtil.hx b/source/funkin/backend/utils/CoolUtil.hx index f1fc14d87..ba40f4201 100644 --- a/source/funkin/backend/utils/CoolUtil.hx +++ b/source/funkin/backend/utils/CoolUtil.hx @@ -394,7 +394,7 @@ class CoolUtil var timeSignParsed:Array> = musicInfo["TimeSignature"] == null ? [] : [for(s in musicInfo["TimeSignature"].split("/")) Std.parseFloat(s)]; var beatsPerMeasure:Float = 4; - var stepsPerBeat:Float = 4; + var stepsPerBeat:Int = 4; // Check later, i dont think timeSignParsed can contain null, only nan if (timeSignParsed.length == 2 && !timeSignParsed.contains(null)) { @@ -403,7 +403,7 @@ class CoolUtil } var bpm:Null = Std.parseFloat(musicInfo["BPM"]).getDefault(DefaultBPM); - Conductor.changeBPM(bpm, beatsPerMeasure, stepsPerBeat); + Conductor.changeBPM(bpm, beatsPerMeasure, floorInt(stepsPerBeat)); } else Conductor.changeBPM(DefaultBPM); } @@ -740,6 +740,15 @@ class CoolUtil @:noUsing public static inline function maxInt(p1:Int, p2:Int) 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. * @param e Value to get the floor of. @@ -781,6 +790,36 @@ class CoolUtil 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] * @param input String to parse diff --git a/source/funkin/backend/utils/MathUtil.hx b/source/funkin/backend/utils/MathUtil.hx new file mode 100644 index 000000000..833f7328e --- /dev/null +++ b/source/funkin/backend/utils/MathUtil.hx @@ -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; + } +} \ No newline at end of file diff --git a/source/funkin/editors/charter/Charter.hx b/source/funkin/editors/charter/Charter.hx index daf7aad2d..96855473f 100644 --- a/source/funkin/editors/charter/Charter.hx +++ b/source/funkin/editors/charter/Charter.hx @@ -16,6 +16,7 @@ import flixel.input.keyboard.FlxKey; import flixel.sound.FlxSound; import flixel.math.FlxPoint; import funkin.editors.charter.CharterBackdropGroup.CharterBackdropDummy; +import funkin.editors.charter.CharterBackdropGroup.CharterGridSeperatorBase; import funkin.backend.system.Conductor; import funkin.backend.chart.*; import funkin.backend.chart.ChartData; @@ -622,6 +623,8 @@ class Charter extends UIState { gridBackdrops.bottomLimitY = __endStep * 40; eventsBackdrop.bottomSeparator.y = gridBackdrops.bottomLimitY-2; + CharterGridSeperatorBase.lastConductorSprY = Math.NEGATIVE_INFINITY; + updateWaveforms(); } @@ -840,6 +843,7 @@ class Charter extends UIState { if (s is CharterNote) cast(s, CharterNote).snappedToStrumline = true; if (s is UISprite) cast(s, UISprite).cursor = BUTTON; } + checkSelectionForBPMUpdates(); if (!(verticalChange == 0 && horizontalChange == 0)) { notesGroup.sortNotes(); eventsGroup.sortEvents(); undos.addToUndo(CSelectionDrag(undoDrags)); @@ -1028,11 +1032,7 @@ class Charter extends UIState { notesGroup.sortNotes(); notesGroup.autoSort = true; - for (s in selection) - if (s is CharterEvent) { - Charter.instance.updateBPMEvents(); - break; - } + checkSelectionForBPMUpdates(); if (addToUndo) undos.addToUndo(CCreateSelection(selection)); @@ -1056,11 +1056,7 @@ class Charter extends UIState { notesGroup.sortNotes(); notesGroup.autoSort = true; - for (s in selection) - if (s is CharterEvent) { - Charter.instance.updateBPMEvents(); - break; - } + checkSelectionForBPMUpdates(); if (addToUndo) undos.addToUndo(CDeleteSelection(selection)); @@ -1251,7 +1247,7 @@ class Charter extends UIState { + '\nStep: ${curStep}' + '\nBeat: ${curBeat}' + '\nMeasure: ${curMeasure}' - + '\nBPM: ${Conductor.bpm}' + + '\nBPM: ${Math.floor(Conductor.bpm*1000)/1000}' + '\nTime Signature: ${Conductor.beatsPerMeasure}/${Conductor.stepsPerBeat}'; if (charterCamera.zoom != (charterCamera.zoom = lerp(charterCamera.zoom, __camZoom, __firstFrame ? 1 : 0.125))) @@ -1429,6 +1425,8 @@ class Charter extends UIState { selection = sObjects; _edit_copy(_); // to fix stupid bugs + checkSelectionForBPMUpdates(); + undos.addToUndo(CCreateSelection(sObjects.copy())); } @@ -1474,6 +1472,7 @@ class Charter extends UIState { if (s.selectable.draggable) s.selectable.handleDrag(s.change * -1); selection = [for (s in selectionDrags) s.selectable]; + Charter.instance.updateBPMEvents(); case CEditSustains(changes): for(n in changes) n.note.updatePos(n.note.step, n.note.id, n.before, n.note.type); @@ -1519,6 +1518,7 @@ class Charter extends UIState { for (s in selectionDrags) if (s.selectable.draggable) s.selectable.handleDrag(s.change); //this.selection = selection; + Charter.instance.updateBPMEvents(); case CEditSustains(changes): for(n in changes) n.note.updatePos(n.note.step, n.note.id, n.after, n.note.type); @@ -1631,11 +1631,9 @@ class Charter extends UIState { } function _view_showeventSecSeparator(t) { t.icon = (Options.charterShowSections = !Options.charterShowSections) ? 1 : 0; - eventsBackdrop.eventSecSeparator.visible = gridBackdrops.sectionsVisible = Options.charterShowSections; } function _view_showeventBeatSeparator(t) { t.icon = (Options.charterShowBeats = !Options.charterShowBeats) ? 1 : 0; - eventsBackdrop.eventBeatSeparator.visible = gridBackdrops.beatsVisible = Options.charterShowBeats; } function _view_switchWaveformDetail(t) { t.icon = (Options.charterLowDetailWaveforms = !Options.charterLowDetailWaveforms) ? 1 : 0; @@ -1835,14 +1833,30 @@ class Charter extends UIState { } public function updateBPMEvents() { + eventsGroup.sortEvents(); + Conductor.mapCharterBPMChanges(PlayState.SONG); buildEvents(); - Conductor.mapBPMChanges(PlayState.SONG); - Conductor.changeBPM(PlayState.SONG.meta.bpm, cast PlayState.SONG.meta.beatsPerMeasure.getDefault(4), cast PlayState.SONG.meta.stepsPerBeat.getDefault(4)); - + for(e in eventsGroup.members) { + for(event in e.events) { + if (event.name == "BPM Change" || event.name == "Time Signature Change" || event.name == "Continuous BPM Change") { + e.refreshEventIcons(); + break; + } + } + } + refreshBPMSensitive(); } + public inline function checkSelectionForBPMUpdates() { + for (s in selection) + if (s is CharterEvent) { + Charter.instance.updateBPMEvents(); + break; + } + } + public inline function hitsoundsEnabled(id:Int) return strumLines.members[id] != null && strumLines.members[id].hitsounds; diff --git a/source/funkin/editors/charter/CharterBackdropGroup.hx b/source/funkin/editors/charter/CharterBackdropGroup.hx index 21eeb90fb..484b03802 100644 --- a/source/funkin/editors/charter/CharterBackdropGroup.hx +++ b/source/funkin/editors/charter/CharterBackdropGroup.hx @@ -12,8 +12,6 @@ class CharterBackdropGroup extends FlxTypedGroup { public var conductorSprY: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 public var strumlinesAmount:Int = 0; @@ -61,8 +59,6 @@ class CharterBackdropGroup extends FlxTypedGroup { grid.conductorFollowerSpr.y = conductorSprY; grid.bottomSeparator.y = (grid.bottomLimit.y = bottomLimitY)-2; - grid.sectionSeparator.visible = sectionsVisible; - grid.beatSeparator.visible = beatsVisible; grid.waveformSprite.shader = strumLine.waveformShader; @@ -119,8 +115,7 @@ class CharterBackdrop extends FlxTypedGroup { public var waveformSprite:FlxSprite; public var conductorFollowerSpr:FlxSprite; - public var beatSeparator:FlxBackdrop; - public var sectionSeparator:FlxBackdrop; + public var beatSeparator:CharterGridSeperator; public var notesGroup:FlxTypedGroup = new FlxTypedGroup(); public var strumLine:CharterStrumline; @@ -136,23 +131,14 @@ class CharterBackdrop extends FlxTypedGroup { waveformSprite.updateHitbox(); add(waveformSprite); - sectionSeparator = new FlxBackdrop(null, Y, 0, 0); - sectionSeparator.y = -2; - sectionSeparator.visible = Options.charterShowSections; - - beatSeparator = new FlxBackdrop(null, Y, 0, 0); - beatSeparator.y = -1; - 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(); - } + beatSeparator = new CharterGridSeperator(); + beatSeparator.makeSolid(1, 1, -1); + beatSeparator.alpha = 0.5; + beatSeparator.scrollFactor.set(1, 1); + beatSeparator.scale.set((4 * 40), 2); + beatSeparator.updateHitbox(); add(beatSeparator); - add(sectionSeparator); + add(notesGroup); bottomSeparator = new FlxSprite(0,-2); @@ -200,15 +186,12 @@ class CharterBackdrop extends FlxTypedGroup { alpha = strumLine.strumLine.visible ? 0.9 : 0.4; } else alpha = 0.9; - for (spr in [gridBackDrop, sectionSeparator, beatSeparator, topLimit, bottomLimit, + for (spr in [gridBackDrop, beatSeparator, topLimit, bottomLimit, topSeparator, bottomSeparator, conductorFollowerSpr, waveformSprite]) { spr.x = x; if (spr != waveformSprite) spr.alpha = alpha; 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.updateHitbox(); topLimit.y = -topLimit.height; @@ -237,6 +220,159 @@ class CharterBackdrop extends FlxTypedGroup { } } +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 = []; + private static var measureStepTimes:Array = []; + private static var timeSignatureChangeGaps:Array = []; + + 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 { var parent:CharterBackdropGroup; public function new(parent:CharterBackdropGroup) { @@ -258,8 +394,7 @@ class CharterBackdropDummy extends UISprite { } class EventBackdrop extends FlxBackdrop { - public var eventBeatSeparator:FlxBackdrop; - public var eventSecSeparator:FlxBackdrop; + public var eventBeatSeparator:CharterEventGridSeperator; public var topSeparator:FlxSprite; public var bottomSeparator:FlxSprite; @@ -269,25 +404,10 @@ class EventBackdrop extends FlxBackdrop { alpha = 0.9; // Separators - eventSecSeparator = new FlxBackdrop(null, Y, 0, 0); - eventSecSeparator.y = -2; - eventSecSeparator.visible = Options.charterShowSections; - - 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(); + eventBeatSeparator = new CharterEventGridSeperator(); + eventBeatSeparator.makeSolid(1, 1, -1); + eventBeatSeparator.alpha = 0.5; + eventBeatSeparator.scrollFactor.set(1, 1); bottomSeparator = new FlxSprite(0,-2); bottomSeparator.makeSolid(1, 1, -1); @@ -308,23 +428,32 @@ class EventBackdrop extends FlxBackdrop { public override function 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.x = (x+width) - 10; - if (eventBeatSeparator.visible) eventBeatSeparator.draw(); + eventBeatSeparator.xPos = x+width; + eventBeatSeparator.draw(); topSeparator.x = (x+width) - 20; topSeparator.cameras = this.cameras; - if (!eventSecSeparator.visible) topSeparator.draw(); + if (!Options.charterShowSections) topSeparator.draw(); bottomSeparator.x = (x+width) - 20; bottomSeparator.cameras = this.cameras; 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() {} } \ No newline at end of file diff --git a/source/funkin/editors/charter/CharterEvent.hx b/source/funkin/editors/charter/CharterEvent.hx index 8bb7fc75b..9dcee4b1e 100644 --- a/source/funkin/editors/charter/CharterEvent.hx +++ b/source/funkin/editors/charter/CharterEvent.hx @@ -1,12 +1,18 @@ package funkin.editors.charter; +import funkin.backend.shaders.CustomShader; +import funkin.backend.system.Conductor; +import flixel.group.FlxSpriteGroup; +import flixel.math.FlxPoint; import flixel.system.FlxAssets.FlxGraphicAsset; +import funkin.backend.chart.ChartData.ChartEvent; +import funkin.backend.scripting.DummyScript; +import funkin.backend.scripting.Script; import funkin.editors.charter.Charter.ICharterSelectable; -import flixel.math.FlxPoint; +import funkin.editors.charter.CharterBackdropGroup.EventBackdrop; import funkin.game.Character; import funkin.game.HealthIcon; -import funkin.editors.charter.CharterBackdropGroup.EventBackdrop; -import funkin.backend.chart.ChartData.ChartEvent; +import openfl.display.BitmapData; class CharterEvent extends UISliceSprite implements ICharterSelectable { public var events:Array; @@ -45,19 +51,78 @@ class CharterEvent extends UISliceSprite implements ICharterSelectable { 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 { + 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) { var isBase64:Bool = false; var path:String = Paths.image('editors/charter/event-icons/$name'); var defaultPath = Paths.image('editors/charter/event-icons/Unknown'); - if (!Assets.exists(path)) path = defaultPath; - - var packPath = Paths.pack('events/$name'); - if (Assets.exists(packPath)) { - var packText = Assets.getText(packPath).split('________PACKSEP________'); - var packImg = packText[3]; - if(packImg != null && packImg.length > 0) { - isBase64 = !packImg.startsWith("assets/"); - path = packImg; + if(!Assets.exists(path)) { + path = defaultPath; + + var packData = getPackData(name); + if(packData != null) { + var packImg = packData[3]; + if(packImg != null && packImg.length > 0) { + isBase64 = !packImg.startsWith("assets/"); + path = packImg; + } } } path = path.trim(); @@ -78,24 +143,174 @@ class CharterEvent extends UISliceSprite implements ICharterSelectable { return new FlxSprite().loadGraphic(graphic); } - public static function generateEventIcon(event:ChartEvent) { - return switch(event.name) { - default: - generateDefaultIcon(event.name); + /** + * Gets a component sprite from the editors/charter/event-icons/components folder + * If you wanna use a number, please use the EventNumber class instead + * @param type The type of component to get + * @param x The x position of the sprite (optional) + * @param y The y position of the sprite (optional) + * @return The component sprite + **/ + public static function getEventComponent(type:String, x:Float = 0.0, y:Float = 0.0) { + var componentPath = Paths.image("editors/charter/event-icons/components/" + type); + if(Assets.exists(componentPath)) + return new FlxSprite(x, y).loadGraphic(componentPath); + + Logs.trace('Could not find component $type', WARNING); + return null; + } + + /** + * 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) { + 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": - // custom icon for camera movement - var state = cast(FlxG.state, Charter); - if (event.params != null && event.params[0] != null && event.params[0] >= 0 && event.params[0] < state.strumLines.length) { - // camera movement, use health icon - var icon = Character.getIconFromCharName(state.strumLines.members[event.params[0]].strumLine.characters[0]); - var healthIcon = new HealthIcon(icon); - healthIcon.setUnstretchedGraphicSize(32, 32, false); - healthIcon.scrollFactor.set(1, 1); - healthIcon.active = false; - healthIcon; - } else - generateDefaultIcon(event.name); + // 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() { @@ -123,18 +338,152 @@ class CharterEvent extends UISliceSprite implements ICharterSelectable { } for(event in events) { - var spr = generateEventIcon(event); + var spr = generateEventIcon(event, false); icons.push(spr); members.push(spr); } 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)); } +} + +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(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 = []; + 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 = ALIGN_NORMAL, spacing:Float = 6, precision:Int = 3) { + super(x, y); + this.digits = []; + this.align = 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(); + } } \ No newline at end of file diff --git a/source/funkin/editors/charter/CharterEventScreen.hx b/source/funkin/editors/charter/CharterEventScreen.hx index 7c2c03c19..b2feccca9 100644 --- a/source/funkin/editors/charter/CharterEventScreen.hx +++ b/source/funkin/editors/charter/CharterEventScreen.hx @@ -1,10 +1,13 @@ package funkin.editors.charter; -import funkin.backend.chart.ChartData.ChartEvent; -import funkin.backend.system.Conductor; import flixel.group.FlxGroup; -import funkin.backend.chart.EventsData; import flixel.util.FlxColor; +import funkin.backend.chart.ChartData.ChartEvent; +import funkin.backend.chart.EventsData; +import funkin.backend.scripting.DummyScript; +import funkin.backend.scripting.Script; +import funkin.backend.system.Conductor; +import funkin.editors.charter.CharterEvent.EventNumber; using StringTools; @@ -55,7 +58,7 @@ class CharterEventScreen extends UISubstateWindow { }); eventsList.add(new EventButton(events[events.length-1], CharterEvent.generateEventIcon(events[events.length-1]), events.length-1, this, eventsList)); changeTab(events.length-1); - })); + }, chartEvent.step)); for (k=>i in events) eventsList.add(new EventButton(i, CharterEvent.generateEventIcon(i), k, this, eventsList)); add(eventsList); @@ -80,12 +83,12 @@ class CharterEventScreen extends UISubstateWindow { else { chartEvent.events = [for (i in eventsList.buttons.members) i.event]; chartEvent.refreshEventIcons(); - Charter.instance.updateBPMEvents(); Charter.undos.addToUndo(CEditEvent(chartEvent, oldEvents, [for (event in events) Reflect.copy(event)])); } } + Charter.instance.updateBPMEvents(); close(); }); saveButton.x -= saveButton.bWidth; @@ -110,83 +113,115 @@ class CharterEventScreen extends UISubstateWindow { // destroy old elements paramsFields = []; - for(e in paramsPanel) { - e.destroy(); - paramsPanel.remove(e); + while(paramsPanel.members.length > 0) { + var e = paramsPanel.members.pop(); + if(e != null) { + e.destroy(); + } } if (id >= 0 && id < events.length) { curEvent = id; var curEvent = events[curEvent]; 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 lastAdded = switch(param.type) { - case TString: - addLabel(); - var textBox:UITextBox = new UITextBox(eventName.x, y, cast value); - paramsPanel.add(textBox); paramsFields.push(textBox); - textBox; - case TBool: - var checkbox = new UICheckbox(eventName.x, y, param.name, cast value); - paramsPanel.add(checkbox); paramsFields.push(checkbox); - checkbox; - case TInt(min, max, step): - addLabel(); - var numericStepper = new UINumericStepper(eventName.x, y, cast value, step.getDefault(1), 0, min, max); - paramsPanel.add(numericStepper); paramsFields.push(numericStepper); - numericStepper; - case TFloat(min, max, step, precision): - addLabel(); - var numericStepper = new UINumericStepper(eventName.x, y, cast value, step.getDefault(1), precision, min, max); - paramsPanel.add(numericStepper); paramsFields.push(numericStepper); - numericStepper; - case TStrumLine: - addLabel(); - var dropdown = new UIDropDown(eventName.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(eventName.x, y, value is String ? FlxColor.fromString(value) : Std.int(value)); - paramsPanel.add(colorWheel); paramsFields.push(colorWheel); - colorWheel; - case TDropDown(options): - addLabel(); - var dropdown = new UIDropDown(eventName.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; - } + + generateEventUI(curEvent); } else { eventName.text = "No event"; curEvent = -1; } } + 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() { if (curEvent < 0) return; + var dataParams = EventsData.getEventParams(events[curEvent].name); events[curEvent].params = [ - for(p in paramsFields) { + for(i=>p in paramsFields) { if (p is UIDropDown) { - var dataParams = EventsData.getEventParams(events[curEvent].name); - if (dataParams[paramsFields.indexOf(p)].type == TStrumLine) cast(p, UIDropDown).index; + if (dataParams[i].type == TStrumLine) cast(p, UIDropDown).index; else cast(p, UIDropDown).label.text; } else if (p is UINumericStepper) { diff --git a/source/funkin/editors/charter/CharterEventTypeSelection.hx b/source/funkin/editors/charter/CharterEventTypeSelection.hx index bfa6cfe78..1b1754c66 100644 --- a/source/funkin/editors/charter/CharterEventTypeSelection.hx +++ b/source/funkin/editors/charter/CharterEventTypeSelection.hx @@ -1,9 +1,11 @@ package funkin.editors.charter; +import funkin.backend.system.Conductor; import funkin.backend.chart.EventsData; class CharterEventTypeSelection extends UISubstateWindow { var callback:String->Void; + var eventStepTime:Float; var buttons:Array = []; @@ -13,9 +15,10 @@ class CharterEventTypeSelection extends UISubstateWindow { var upIndicator:UIText; var downIndicator:UIText; - public function new(callback:String->Void) { + public function new(callback:String->Void, eventStepTime:Float) { super(); this.callback = callback; + this.eventStepTime = eventStepTime; } public override function create() { @@ -32,6 +35,15 @@ class CharterEventTypeSelection extends UISubstateWindow { buttonsBG.frames = Paths.getFrames('editors/ui/inputbox'); 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) { var button = new UIButton(0, (32 * k), eventName, function() { close(); @@ -52,6 +64,11 @@ class CharterEventTypeSelection extends UISubstateWindow { icon.x = button.x + 8; icon.y = button.y + Math.abs(button.bHeight - icon.height) / 2; 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)); @@ -79,7 +96,7 @@ class CharterEventTypeSelection extends UISubstateWindow { sinner += elapsed; for (button in buttons) - button.selectable = buttonsBG.hovered; + if (button.shouldPress) button.selectable = buttonsBG.hovered; buttonCameras.zoom = subCam.zoom; diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index bff696ced..1744c3acf 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -232,6 +232,15 @@ class PlayState extends MusicBeatState * How strong the cam zooms should be (defaults to 1) */ public var camZoomingStrength:Float = 1; + /** + * Number of Beats to offset camZooming by. + * Will automatically be set when a Time Signature Change Occurs. + */ + public var camZoomingOffset:Float = 0; + /** + * The curBeat position of the last Time Signature Change that occured + */ + public var lastTimeSigBeat:Float = 0; /** * Maximum amount of zoom for the camera. */ @@ -1026,8 +1035,6 @@ class PlayState extends MusicBeatState camZoomingInterval = cast songData.meta.beatsPerMeasure.getDefault(4); - Conductor.changeBPM(songData.meta.bpm, cast songData.meta.beatsPerMeasure.getDefault(4), cast songData.meta.stepsPerBeat.getDefault(4)); - curSong = songData.meta.name.toLowerCase(); inst = FlxG.sound.load(Paths.inst(SONG.meta.name, difficulty)); @@ -1463,6 +1470,9 @@ class PlayState extends MusicBeatState if (strumLines.members[event.params[0]] != null && strumLines.members[event.params[0]].characters != null) for (char in strumLines.members[event.params[0]].characters) if (char != null) char.playAnim(event.params[1], event.params[2], null); + case "Time Signature Change": + lastTimeSigBeat = Conductor.getTimeInBeats(event.time); + // the rest is automatically handled by conductor case "Unknown": // nothing } } @@ -1875,10 +1885,10 @@ class PlayState extends MusicBeatState override function beatHit(curBeat:Int) { super.beatHit(curBeat); + + camZoomingOffset = curBeat - lastTimeSigBeat; - if (camZoomingInterval < 1) camZoomingInterval = 1; - if (Options.camZoomOnBeat && camZooming && FlxG.camera.zoom < maxCamZoom && curBeat % camZoomingInterval == 0) - { + if (Options.camZoomOnBeat && camZooming && FlxG.camera.zoom < maxCamZoom && camZoomingOffset % camZoomingInterval == 0) { FlxG.camera.zoom += 0.015 * camZoomingStrength; camHUD.zoom += 0.03 * camZoomingStrength; }