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 14aa6b494c..155049d78b 100644 --- a/source/funkin/modding/IScriptedClass.hx +++ b/source/funkin/modding/IScriptedClass.hx @@ -78,6 +78,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 7e19173c46..6c667e458a 100644 --- a/source/funkin/modding/events/ScriptEventDispatcher.hx +++ b/source/funkin/modding/events/ScriptEventDispatcher.hx @@ -102,9 +102,11 @@ class ScriptEventDispatcher case SONG_BEAT_HIT: t.onBeatHit(cast event); return; + case SONG_BPM_CHANGE: + t.onBpmChange(cast event); + return; case SONG_STEP_HIT: t.onStepHit(cast event); - return; default: // Continue; } } diff --git a/source/funkin/modding/events/ScriptEventType.hx b/source/funkin/modding/events/ScriptEventType.hx index 6ac85649f9..359160652f 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 be9b7146b0..5b72a90b34 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 20d2f75a4c..256e5d4463 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 +190,37 @@ 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 + { + var idlePerBeat:Float = (daIdle.numFrames / daIdle.frameRate) / (Conductor.instance.beatLengthMs / 1000); + var danceEveryNumBeats:Int = Math.ceil(idlePerBeat); + if (danceEveryNumBeats > numeratorTweak) + { + while (danceEveryNumBeats % numeratorTweak != 0) + danceEveryNumBeats++; + } + _realDanceEvery = Math.max(danceEvery, danceEveryNumBeats); + } + 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 88e19b1912..a9a894f17e 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 8668b64c12..938fbeed59 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 @@ -119,6 +121,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 37e5a31f7c..b9e3449e34 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(); } @@ -108,6 +109,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 3fb63a4f1e..c463ece5a1 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 @@ -5654,7 +5658,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function handleHelpKeybinds():Void { // F1 = Open Help - if (FlxG.keys.justPressed.F1 && !isHaxeUIDialogOpen) { + if (FlxG.keys.justPressed.F1 && !isHaxeUIDialogOpen) + { this.openUserGuideDialog(); } } 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.