From 4c6cdaf8b5233e0df3b861ecfb5a3577b3cf7d8c Mon Sep 17 00:00:00 2001 From: MidyGamy Date: Mon, 27 Jan 2025 16:23:58 -0400 Subject: [PATCH 1/4] bpmChangeEvent + danceevery overhaul --- source/funkin/Conductor.hx | 26 +++++++ source/funkin/modding/IScriptedClass.hx | 5 ++ .../modding/events/ScriptEventDispatcher.hx | 3 + .../funkin/modding/events/ScriptEventType.hx | 7 ++ source/funkin/modding/module/Module.hx | 2 + source/funkin/play/character/BaseCharacter.hx | 1 + source/funkin/play/song/Song.hx | 2 + source/funkin/play/stage/Bopper.hx | 71 ++++++++++++++++--- source/funkin/play/stage/Stage.hx | 5 ++ source/funkin/ui/MusicBeatState.hx | 11 +++ source/funkin/ui/MusicBeatSubState.hx | 11 +++ source/funkin/ui/charSelect/CharSelectGF.hx | 2 + .../funkin/ui/charSelect/CharSelectPlayer.hx | 2 + .../ui/debug/charting/ChartEditorState.hx | 4 ++ .../ui/haxeui/components/CharacterPlayer.hx | 10 +++ 15 files changed, 153 insertions(+), 9 deletions(-) diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx index 803b9e1b36..4183763810 100644 --- a/source/funkin/Conductor.hx +++ b/source/funkin/Conductor.hx @@ -75,6 +75,17 @@ class Conductor */ public var onStepHit(default, null):FlxSignal = new FlxSignal(); + /** + * Signal fired when the current Conductor instance changes BPM. + */ + public static var bpmChange(default, null):FlxSignal = new FlxSignal(); + + /** + * Signal fired when THIS Conductor instance changes BPM. + * TODO: This naming sucks but we can't make a static and instance field with the same name! + */ + public var onBpmChange(default, null):FlxSignal = new FlxSignal(); + /** * The list of time changes in the song. * There should be at least one time change (at the beginning of the song) to define the BPM. @@ -328,6 +339,11 @@ class Conductor Conductor.stepHit.dispatch(); } + static function dispatchBpmChange():Void + { + Conductor.bpmChange.dispatch(); + } + static function setupSingleton(input:Conductor):Void { input.onMeasureHit.add(dispatchMeasureHit); @@ -335,6 +351,8 @@ class Conductor input.onBeatHit.add(dispatchBeatHit); input.onStepHit.add(dispatchStepHit); + + input.onBpmChange.add(dispatchBpmChange); } static function clearSingleton(input:Conductor):Void @@ -344,6 +362,8 @@ class Conductor input.onBeatHit.remove(dispatchBeatHit); input.onStepHit.remove(dispatchStepHit); + + input.onBpmChange.remove(dispatchBpmChange); } static function get_instance():Conductor @@ -418,6 +438,7 @@ class Conductor var oldMeasure:Float = this.currentMeasure; var oldBeat:Float = this.currentBeat; var oldStep:Float = this.currentStep; + var oldBpm:Float = this.bpm; // If the song is playing, limit the song position to the length of the song or beginning of the song. if (FlxG.sound.music != null && FlxG.sound.music.playing) @@ -469,6 +490,11 @@ class Conductor } // FlxSignals are really cool. + if (bpm != oldBpm) + { + this.onBpmChange.dispatch(); + } + if (currentStep != oldStep) { this.onStepHit.dispatch(); diff --git a/source/funkin/modding/IScriptedClass.hx b/source/funkin/modding/IScriptedClass.hx index 11f1d5d66d..f92444be92 100644 --- a/source/funkin/modding/IScriptedClass.hx +++ b/source/funkin/modding/IScriptedClass.hx @@ -81,6 +81,11 @@ interface INoteScriptedClass extends IScriptedClass */ interface IBPMSyncedScriptedClass extends IScriptedClass { + /** + * Called when the BPM changes. + */ + public function onBpmChange(event:SongTimeScriptEvent):Void; + /** * Called once every step of the song. */ diff --git a/source/funkin/modding/events/ScriptEventDispatcher.hx b/source/funkin/modding/events/ScriptEventDispatcher.hx index 15d4dfaf67..cda6a7b3da 100644 --- a/source/funkin/modding/events/ScriptEventDispatcher.hx +++ b/source/funkin/modding/events/ScriptEventDispatcher.hx @@ -105,6 +105,9 @@ class ScriptEventDispatcher case SONG_STEP_HIT: t.onStepHit(cast event); return; + case SONG_BPM_CHANGE: + t.onBpmChange(cast event); + return; default: // Continue; } } diff --git a/source/funkin/modding/events/ScriptEventType.hx b/source/funkin/modding/events/ScriptEventType.hx index e44780dcfb..284ec7ca38 100644 --- a/source/funkin/modding/events/ScriptEventType.hx +++ b/source/funkin/modding/events/ScriptEventType.hx @@ -63,6 +63,13 @@ enum abstract ScriptEventType(String) from String to String */ var SONG_STEP_HIT = 'STEP_HIT'; + /** + * Called when the BPM changes. + * + * This event is not cancelable. + */ + var SONG_BPM_CHANGE = 'BPM_CHANGE'; + /** * Called when a note comes on screen and starts approaching the strumline. * diff --git a/source/funkin/modding/module/Module.hx b/source/funkin/modding/module/Module.hx index 57f0cc2dab..e390870688 100644 --- a/source/funkin/modding/module/Module.hx +++ b/source/funkin/modding/module/Module.hx @@ -91,6 +91,8 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {} + public function onBpmChange(event:SongTimeScriptEvent) {} + public function onStepHit(event:SongTimeScriptEvent) {} public function onBeatHit(event:SongTimeScriptEvent) {} diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index a7fdee3fba..1cd1a357e6 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -429,6 +429,7 @@ class BaseCharacter extends Bopper // super.onBeatHit handles the regular `dance()` calls. } FlxG.watch.addQuick('holdTimer-${characterId}', holdTimer); + FlxG.watch.addQuick('_realDanceEvery-${characterId}', _realDanceEvery); } public function isSinging():Bool diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 17585973fb..cebd0aa566 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -655,6 +655,8 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry 0 && (event.step % (danceEvery * Constants.STEPS_PER_BEAT)) == 0) + if (_realDanceEvery > 0 && (event.step % (_realDanceEvery * Constants.STEPS_PER_BEAT)) == 0) { dance(shouldBop); } @@ -172,6 +191,40 @@ class Bopper extends StageProp implements IPlayStateScriptedClass public function onBeatHit(event:SongTimeScriptEvent):Void {} + function update_danceEvery():Void + { + if (danceEvery == 0 || shouldAlternate || this.animation.getByName('idle$idleSuffix') == null || shouldBop) + { + // for forced bopping, alternating dance and non-existing animation, it just works the same as before + _realDanceEvery = danceEvery; + return; + } + + var daIdle = this.animation.getByName('idle$idleSuffix'); + var numeratorTweak:Int = (Conductor.instance.timeSignatureNumerator % 2 == 0) ? 2 : Conductor.instance.timeSignatureNumerator; // hopefully we get only prime numbers... + if (FlxMath.getDecimals(danceEvery) == 0) // for int danceEvery + { + trace("THAT'S AN INT FOR DANCEEVERY"); + var idlePerBeat:Float = (daIdle.numFrames / daIdle.frameRate) / (Conductor.instance.beatLengthMs / 1000); + var danceEveryNumBeats:Int = Math.ceil(idlePerBeat); + var numeratorTweak:Int = (Conductor.instance.timeSignatureNumerator % 2 == 0) ? 2 : 3; + if (danceEveryNumBeats > numeratorTweak) + { + while (danceEveryNumBeats % numeratorTweak != 0) + danceEveryNumBeats++; + } + _realDanceEvery = Math.max(danceEvery, danceEveryNumBeats); + } + else // to do: for danceEvery with decimals + else // for decymal danceEvery (X.25, X.50 and X.75) + { + // maybe to rework, for the moment it tries to have the same patern every sections + _realDanceEvery = danceEvery; + while ((4 * _realDanceEvery * Conductor.instance.stepLengthMs) < (daIdle.numFrames / daIdle.frameRate)) + _realDanceEvery *= numeratorTweak; + } + } + /** * Called every `danceEvery` beats of the song. */ diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index 87938644fe..3c1b1f9162 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -771,6 +771,11 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements } } + /** + * A function that gets called when the BPM of the song changes. + */ + public function onBpmChange(event:SongTimeScriptEvent):Void {} + /** * A function that gets called once per step in the song. * @param curStep The current step number. diff --git a/source/funkin/ui/MusicBeatState.hx b/source/funkin/ui/MusicBeatState.hx index 31e3bec509..c712842e63 100644 --- a/source/funkin/ui/MusicBeatState.hx +++ b/source/funkin/ui/MusicBeatState.hx @@ -65,6 +65,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler Conductor.beatHit.add(this.beatHit); Conductor.stepHit.add(this.stepHit); + Conductor.bpmChange.add(this.bpmChange); } public override function destroy():Void @@ -72,6 +73,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler super.destroy(); Conductor.beatHit.remove(this.beatHit); Conductor.stepHit.remove(this.stepHit); + Conductor.bpmChange.remove(this.bpmChange); } function handleFunctionControls():Void @@ -133,6 +135,15 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler FlxG.resetState(); } + public function bpmChange():Bool + { + var event = new SongTimeScriptEvent(SONG_BPM_CHANGE, conductorInUse.currentBeat, conductorInUse.currentStep); + + dispatchEvent(event); + + return true; + } + public function stepHit():Bool { var event = new SongTimeScriptEvent(SONG_STEP_HIT, conductorInUse.currentBeat, conductorInUse.currentStep); diff --git a/source/funkin/ui/MusicBeatSubState.hx b/source/funkin/ui/MusicBeatSubState.hx index d046ab94b8..89f43f4c3f 100644 --- a/source/funkin/ui/MusicBeatSubState.hx +++ b/source/funkin/ui/MusicBeatSubState.hx @@ -64,6 +64,7 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler Conductor.beatHit.add(this.beatHit); Conductor.stepHit.add(this.stepHit); + Conductor.bpmChange.add(this.bpmChange); initConsoleHelpers(); } @@ -73,6 +74,7 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler super.destroy(); Conductor.beatHit.remove(this.beatHit); Conductor.stepHit.remove(this.stepHit); + Conductor.bpmChange.remove(this.bpmChange); } override function update(elapsed:Float):Void @@ -122,6 +124,15 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler sort(SortUtil.byZIndex, FlxSort.ASCENDING); } + public function bpmChange():Bool + { + var event = new SongTimeScriptEvent(SONG_BPM_CHANGE, conductorInUse.currentBeat, conductorInUse.currentStep); + + dispatchEvent(event); + + return true; + } + /** * Called when a step is hit in the current song. * Continues outside of PlayState, for things like animations in menus. diff --git a/source/funkin/ui/charSelect/CharSelectGF.hx b/source/funkin/ui/charSelect/CharSelectGF.hx index 89fc6deb05..c5741067af 100644 --- a/source/funkin/ui/charSelect/CharSelectGF.hx +++ b/source/funkin/ui/charSelect/CharSelectGF.hx @@ -73,6 +73,8 @@ class CharSelectGF extends FlxAtlasSprite implements IBPMSyncedScriptedClass #end } + public function onBpmChange(event:SongTimeScriptEvent):Void {} + public function onStepHit(event:SongTimeScriptEvent):Void {} var danceEvery:Int = 2; diff --git a/source/funkin/ui/charSelect/CharSelectPlayer.hx b/source/funkin/ui/charSelect/CharSelectPlayer.hx index 1eef52bedc..c73c9195c2 100644 --- a/source/funkin/ui/charSelect/CharSelectPlayer.hx +++ b/source/funkin/ui/charSelect/CharSelectPlayer.hx @@ -35,6 +35,8 @@ class CharSelectPlayer extends FlxAtlasSprite implements IBPMSyncedScriptedClass }); } + public function onBpmChange(event:SongTimeScriptEvent):Void {} + public function onStepHit(event:SongTimeScriptEvent):Void {} public function onBeatHit(event:SongTimeScriptEvent):Void diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 44c14be06e..15266e4b1e 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -2167,6 +2167,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState currentPlayerCharacterPlayer.onBeatHit(cast event); case SONG_STEP_HIT: currentPlayerCharacterPlayer.onStepHit(cast event); + case SONG_BPM_CHANGE: + currentPlayerCharacterPlayer.onBpmChange(cast event); case NOTE_HIT: currentPlayerCharacterPlayer.onNoteHit(cast event); default: // Continue @@ -2183,6 +2185,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState currentOpponentCharacterPlayer.onBeatHit(cast event); case SONG_STEP_HIT: currentOpponentCharacterPlayer.onStepHit(cast event); + case SONG_BPM_CHANGE: + currentPlayerCharacterPlayer.onBpmChange(cast event); case NOTE_HIT: currentOpponentCharacterPlayer.onNoteHit(cast event); default: // Continue diff --git a/source/funkin/ui/haxeui/components/CharacterPlayer.hx b/source/funkin/ui/haxeui/components/CharacterPlayer.hx index 77b23d68a7..5826513769 100644 --- a/source/funkin/ui/haxeui/components/CharacterPlayer.hx +++ b/source/funkin/ui/haxeui/components/CharacterPlayer.hx @@ -197,6 +197,16 @@ class CharacterPlayer extends Box if (character != null) character.onUpdate(event); } + /** + * Called when the BPM changes in the song + * Used to play character animations. + * @param event The event. + */ + public function onBpmChange(event:SongTimeScriptEvent):Void + { + if (character != null) character.onBpmChange(event); + } + /** * Called when an beat is hit in the song * Used to play character animations. From 4094bb771fdfe34d5a4113acf9c81c1362d5844a Mon Sep 17 00:00:00 2001 From: MidyGamy Date: Wed, 29 Jan 2025 22:41:55 -0400 Subject: [PATCH 2/4] 2 "else"? why? --- source/funkin/play/stage/Bopper.hx | 1 - 1 file changed, 1 deletion(-) diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx index 9b317ad3d2..277e7fccb3 100644 --- a/source/funkin/play/stage/Bopper.hx +++ b/source/funkin/play/stage/Bopper.hx @@ -215,7 +215,6 @@ class Bopper extends StageProp implements IPlayStateScriptedClass } _realDanceEvery = Math.max(danceEvery, danceEveryNumBeats); } - else // to do: for danceEvery with decimals else // for decymal danceEvery (X.25, X.50 and X.75) { // maybe to rework, for the moment it tries to have the same patern every sections From 8fa744c48141c16db44a607117c161d9b1087f75 Mon Sep 17 00:00:00 2001 From: MidyGamy Date: Wed, 29 Jan 2025 23:00:21 -0400 Subject: [PATCH 3/4] make update_danceEvery() when the bobber is actually fully configured --- source/funkin/play/stage/Bopper.hx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx index 277e7fccb3..1179539b3f 100644 --- a/source/funkin/play/stage/Bopper.hx +++ b/source/funkin/play/stage/Bopper.hx @@ -125,6 +125,12 @@ class Bopper extends StageProp implements IPlayStateScriptedClass } } + override function onCreate(event:ScriptEvent):Void + { + super.onCreate(event); + this.update_danceEvery(); + } + /** * Called when an animation finishes. * @param name The name of the animation that just finished. From a144fc28db1c5260aaa29d4ac2459230816abbc8 Mon Sep 17 00:00:00 2001 From: MidyGamy Date: Sat, 8 Feb 2025 16:32:08 -0400 Subject: [PATCH 4/4] 1 frame safe --- source/funkin/play/stage/Bopper.hx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx index 1179539b3f..36d846bcd9 100644 --- a/source/funkin/play/stage/Bopper.hx +++ b/source/funkin/play/stage/Bopper.hx @@ -211,7 +211,7 @@ class Bopper extends StageProp implements IPlayStateScriptedClass if (FlxMath.getDecimals(danceEvery) == 0) // for int danceEvery { trace("THAT'S AN INT FOR DANCEEVERY"); - var idlePerBeat:Float = (daIdle.numFrames / daIdle.frameRate) / (Conductor.instance.beatLengthMs / 1000); + var idlePerBeat:Float = ((daIdle.numFrames + 1) / daIdle.frameRate) / (Conductor.instance.beatLengthMs / 1000); var danceEveryNumBeats:Int = Math.ceil(idlePerBeat); var numeratorTweak:Int = (Conductor.instance.timeSignatureNumerator % 2 == 0) ? 2 : 3; if (danceEveryNumBeats > numeratorTweak) @@ -225,7 +225,7 @@ class Bopper extends StageProp implements IPlayStateScriptedClass { // maybe to rework, for the moment it tries to have the same patern every sections _realDanceEvery = danceEvery; - while ((4 * _realDanceEvery * Conductor.instance.stepLengthMs) < (daIdle.numFrames / daIdle.frameRate)) + while ((4 * _realDanceEvery * Conductor.instance.stepLengthMs) < ((daIdle.numFrames + 1) / daIdle.frameRate)) _realDanceEvery *= numeratorTweak; } }