From cdb3acc6912b03d0f18d6f70ccd4e27f20546153 Mon Sep 17 00:00:00 2001 From: AbnormalPoof Date: Mon, 29 Sep 2025 01:54:10 -0500 Subject: [PATCH] Add multiple new callbacks for Freeplay and Character Select --- source/funkin/modding/IScriptedClass.hx | 57 +++++++++++++++ source/funkin/modding/events/ScriptEvent.hx | 73 +++++++++++++++++++ .../modding/events/ScriptEventDispatcher.hx | 45 ++++++++++++ .../funkin/modding/events/ScriptEventType.hx | 63 ++++++++++++++++ source/funkin/modding/module/Module.hx | 53 +++++++++++++- source/funkin/play/PlayState.hx | 5 +- .../ui/charSelect/CharSelectSubState.hx | 6 ++ source/funkin/ui/freeplay/FreeplayState.hx | 12 +++ .../ui/freeplay/backcards/BackingCard.hx | 34 ++++++++- .../funkin/ui/freeplay/dj/BaseFreeplayDJ.hx | 62 +++++++++++++++- 10 files changed, 405 insertions(+), 5 deletions(-) diff --git a/source/funkin/modding/IScriptedClass.hx b/source/funkin/modding/IScriptedClass.hx index f739f59b7..d1162cf8f 100644 --- a/source/funkin/modding/IScriptedClass.hx +++ b/source/funkin/modding/IScriptedClass.hx @@ -176,6 +176,63 @@ interface IPlayStateScriptedClass extends INoteScriptedClass extends IBPMSyncedS public function onCountdownEnd(event:CountdownScriptEvent):Void; } +/** + * Defines a set of callbacks activated in Freeplay. + */ +interface IFreeplayScriptedClass extends IScriptedClass +{ + /** + * Called when a capsule is selected. + */ + public function onCapsuleSelected(event:CapsuleScriptEvent):Void; + + /** + * Called when the current difficulty is changed. + */ + public function onDifficultySwitch(event:CapsuleScriptEvent):Void; + + /** + * Called when a song is selected. + */ + public function onSongSelected(event:CapsuleScriptEvent):Void; + + /** + * Called when the intro for Freeplay finishes. + */ + public function onFreeplayIntroDone(event:FreeplayScriptEvent):Void; + + /** + * Called when the Freeplay outro begins. + */ + public function onFreeplayOutro(event:FreeplayScriptEvent):Void; + + /** + * Called when Freeplay is closed. + */ + public function onFreeplayClose(event:FreeplayScriptEvent):Void; +} + +/** + * Defines a set of callbacks activated in Character Select. + */ +interface ICharacterSelectScriptedClass extends IScriptedClass +{ + /** + * Called when a character is selected. + */ + public function onCharacterSelect(event:CharacterSelectScriptEvent):Void; + + /** + * Called when the user presses BACK after confirming a character. + */ + public function onCharacterDeselect(event:CharacterSelectScriptEvent):Void; + + /** + * Called when a character has been confirmed. + */ + public function onCharacterConfirm(event:CharacterSelectScriptEvent):Void; +} + /** * Defines a set of callbacks activated during a dialogue conversation. */ diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx index 153795bf1..24c25b47a 100644 --- a/source/funkin/modding/events/ScriptEvent.hx +++ b/source/funkin/modding/events/ScriptEvent.hx @@ -9,6 +9,7 @@ import funkin.play.notes.SustainTrail; import funkin.play.cutscene.dialogue.Conversation; import funkin.play.Countdown.CountdownStep; import funkin.play.notes.NoteDirection; +import funkin.ui.freeplay.SongMenuItem; import openfl.events.KeyboardEvent; /** @@ -518,6 +519,78 @@ class FocusScriptEvent extends ScriptEvent } } +/** + * An event that is fired when a capsule is selected. + */ +class CapsuleScriptEvent extends ScriptEvent +{ + /** + * The capsule that was selected. + */ + public var capsule(default, null):SongMenuItem; + + /** + * The difficulty ID of the selected song. + */ + public var difficultyId(default, null):String; + + /** + * The variation ID of the selected song. + */ + public var variationId(default, null):String; + + public function new(type:ScriptEventType, capsule:SongMenuItem, difficultyId:String, variationId:String):Void + { + super(type, false); + this.capsule = capsule; + this.difficultyId = difficultyId; + this.variationId = variationId; + } + + public override function toString():String + { + return 'CapsuleScriptEvent(type=' + type + ')'; + } +} + +/** + * An event that is fired when Freeplay is entered or exited. + */ +class FreeplayScriptEvent extends ScriptEvent +{ + public function new(type:ScriptEventType):Void + { + super(type, false); + } + + public override function toString():String + { + return 'FreeplayScriptEvent(type=' + type + ')'; + } +} + +/** + * An event that is fired when a character is selected or deselected. + */ +class CharacterSelectScriptEvent extends ScriptEvent +{ + /** + * The character ID of the selected character. + */ + public var characterId(default, null):String; + + public function new(type:ScriptEventType, characterId:String):Void + { + super(type, false); + this.characterId = characterId; + } + + public override function toString():String + { + return 'CharacterSelectScriptEvent(type=' + type + ')'; + } +} + /** * An event that is fired when moving out of or into an FlxSubState. */ diff --git a/source/funkin/modding/events/ScriptEventDispatcher.hx b/source/funkin/modding/events/ScriptEventDispatcher.hx index d5d18f9d7..7010b3531 100644 --- a/source/funkin/modding/events/ScriptEventDispatcher.hx +++ b/source/funkin/modding/events/ScriptEventDispatcher.hx @@ -204,6 +204,51 @@ class ScriptEventDispatcher return; } + if (Std.isOfType(target, IFreeplayScriptedClass)) + { + var t:IFreeplayScriptedClass = cast target; + switch (event.type) + { + case CAPSULE_SELECTED: + t.onCapsuleSelected(cast event); + return; + case DIFFICULTY_SWITCH: + t.onDifficultySwitch(cast event); + return; + case SONG_SELECTED: + t.onSongSelected(cast event); + return; + case FREEPLAY_INTRO: + t.onFreeplayIntroDone(cast event); + return; + case FREEPLAY_OUTRO: + t.onFreeplayOutro(cast event); + return; + case FREEPLAY_CLOSE: + t.onFreeplayClose(cast event); + return; + default: // Continue; + } + } + + if (Std.isOfType(target, ICharacterSelectScriptedClass)) + { + var t:ICharacterSelectScriptedClass = cast target; + switch (event.type) + { + case CHARACTER_SELECTED: + t.onCharacterSelect(cast event); + return; + case CHARACTER_DESELECTED: + t.onCharacterDeselect(cast event); + return; + case CHARACTER_CONFIRMED: + t.onCharacterConfirm(cast event); + return; + default: // Continue; + } + } + // If you get a crash on this line, that means ERIC FUCKED UP! // throw 'No function called for event type: ${event.type}'; } diff --git a/source/funkin/modding/events/ScriptEventType.hx b/source/funkin/modding/events/ScriptEventType.hx index 52abeb64e..61428d525 100644 --- a/source/funkin/modding/events/ScriptEventType.hx +++ b/source/funkin/modding/events/ScriptEventType.hx @@ -254,6 +254,69 @@ enum abstract ScriptEventType(String) from String to String */ var FOCUS_LOST = 'FOCUS_LOST'; + /** + * Called when a freeplay capsule is selected. + * + * This event is not cancelable. + */ + var CAPSULE_SELECTED = 'CAPSULE_SELECTED'; + + /** + * Called when the current difficulty in Freeplay is changed. + * + * This event is not cancelable. + */ + var DIFFICULTY_SWITCH = 'DIFFICULTY_SWITCH'; + + /** + * Called when a song is selected in Freeplay. + * + * This event is not cancelable. + */ + var SONG_SELECTED = 'SONG_SELECTED'; + + /** + * Called when the intro for Freeplay finishes. + * + * This event is not cancelable. + */ + var FREEPLAY_INTRO = 'FREEPLAY_INTRO'; + + /** + * Called when the outro for Freeplay starts. + * + * This event is not cancelable. + */ + var FREEPLAY_OUTRO = 'FREEPLAY_OUTRO'; + + /** + * Called when Freeplay is closed. + * + * This event is not cancelable. + */ + var FREEPLAY_CLOSE = 'FREEPLAY_CLOSE'; + + /** + * Called when a character is selected, but not confirmed, in Character Select. + * + * This event is not cancelable. + */ + var CHARACTER_SELECTED = 'CHARACTER_SELECTED'; + + /** + * Called when a character is deselected in Character Select. + * + * This event is not cancelable. + */ + var CHARACTER_DESELECTED = 'CHARACTER_DESELECTED'; + + /** + * Called when a character is confirmed in Character Select. + * + * This event is not cancelable. + */ + var CHARACTER_CONFIRMED = 'CHARACTER_CONFIRMED'; + /** * Called when the game starts a conversation. * diff --git a/source/funkin/modding/module/Module.hx b/source/funkin/modding/module/Module.hx index d7e5a35d2..27993e893 100644 --- a/source/funkin/modding/module/Module.hx +++ b/source/funkin/modding/module/Module.hx @@ -2,6 +2,8 @@ package funkin.modding.module; import funkin.modding.IScriptedClass.IPlayStateScriptedClass; import funkin.modding.IScriptedClass.IStateChangingScriptedClass; +import funkin.modding.IScriptedClass.IFreeplayScriptedClass; +import funkin.modding.IScriptedClass.ICharacterSelectScriptedClass; import funkin.modding.events.ScriptEvent; /** @@ -21,7 +23,8 @@ typedef ModuleParams = * You may have the module active at all times, or only when another script enables it. */ @:nullSafety -class Module implements IPlayStateScriptedClass implements IStateChangingScriptedClass +class Module implements IPlayStateScriptedClass implements IStateChangingScriptedClass implements IFreeplayScriptedClass + implements ICharacterSelectScriptedClass { /** * Whether the module is currently active. @@ -74,7 +77,7 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte } } - public function toString() + public function toString():String { return 'Module(' + this.moduleId + ')'; } @@ -234,8 +237,54 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte * Called when the song has been restarted. */ public function onSongRetry(event:SongRetryEvent) {} + /** * Called when any state is created. */ public function onStateCreate(event:ScriptEvent) {} + + /** + * Called when a capsule is selected. + */ + public function onCapsuleSelected(event:CapsuleScriptEvent):Void {} + + /** + * Called when the current difficulty is changed. + */ + public function onDifficultySwitch(event:CapsuleScriptEvent):Void {} + + /** + * Called when a song is selected. + */ + public function onSongSelected(event:CapsuleScriptEvent):Void {} + + /** + * Called when the intro for Freeplay finishes. + */ + public function onFreeplayIntroDone(event:FreeplayScriptEvent):Void {} + + /** + * Called when the Freeplay outro begins. + */ + public function onFreeplayOutro(event:FreeplayScriptEvent):Void {} + + /** + * Called when Freeplay is closed. + */ + public function onFreeplayClose(event:FreeplayScriptEvent):Void {} + + /** + * Called when a character is selected. + */ + public function onCharacterSelect(event:CharacterSelectScriptEvent):Void {} + + /** + * Called when the user presses BACK after confirming a character. + */ + public function onCharacterDeselect(event:CharacterSelectScriptEvent):Void {} + + /** + * Called when a character has been confirmed. + */ + public function onCharacterConfirm(event:CharacterSelectScriptEvent):Void {} } diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 7a181a4ae..63f95a611 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1200,7 +1200,10 @@ class PlayState extends MusicBeatSubState Events.logFailSong(currentSong.id, currentVariation); #end - dispatchEvent(new ScriptEvent(GAME_OVER)); + var event:ScriptEvent = new ScriptEvent(GAME_OVER); + dispatchEvent(event); + + if (event.eventCanceled) return; // Disable updates, preventing animations in the background from playing. persistentUpdate = false; diff --git a/source/funkin/ui/charSelect/CharSelectSubState.hx b/source/funkin/ui/charSelect/CharSelectSubState.hx index 48682d7f0..d1ce0e781 100644 --- a/source/funkin/ui/charSelect/CharSelectSubState.hx +++ b/source/funkin/ui/charSelect/CharSelectSubState.hx @@ -932,6 +932,8 @@ class CharSelectSubState extends MusicBeatSubState cursorConfirmed.visible = false; grpCursors.visible = true; + dispatchEvent(new CharacterSelectScriptEvent(CHARACTER_DESELECTED, curChar)); + FlxTween.globalManager.cancelTweensOf(FlxG.sound.music); FlxTween.tween(FlxG.sound.music, {pitch: 1.0, volume: 1.0}, 1, {ease: FlxEase.quartInOut}); playerChill.playAnimation("deselect"); @@ -966,6 +968,8 @@ class CharSelectSubState extends MusicBeatSubState FlxG.sound.play(Paths.sound('CS_confirm')); + dispatchEvent(new CharacterSelectScriptEvent(CHARACTER_CONFIRMED, curChar)); + FlxTween.tween(FlxG.sound.music, {pitch: 0.1}, 1, {ease: FlxEase.quadInOut}); FlxTween.tween(FlxG.sound.music, {volume: 0.0}, 1.5, {ease: FlxEase.quadInOut}); playerChill.playAnimation("select"); @@ -1227,6 +1231,8 @@ class CharSelectSubState extends MusicBeatSubState else staticSound.stop(); + dispatchEvent(new CharacterSelectScriptEvent(CHARACTER_SELECTED, value)); + nametag.switchChar(value); gfChill.visible = false; playerChill.visible = false; diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 49b92e05c..c2488a324 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -712,6 +712,8 @@ class FreeplayState extends MusicBeatSubState var onDJIntroDone:Void->Void = () -> { controls.active = true; + dispatchEvent(new FreeplayScriptEvent(FREEPLAY_INTRO)); + // when boyfriend hits dat shiii albumRoll.playIntro(); @@ -863,6 +865,7 @@ class FreeplayState extends MusicBeatSubState { super.dispatchEvent(event); if (backingCard != null) ScriptEventDispatcher.callEvent(backingCard, event); + if (dj != null) ScriptEventDispatcher.callEvent(dj, event); } /** @@ -999,6 +1002,8 @@ class FreeplayState extends MusicBeatSubState rememberSelection(); changeSelection(); refreshCapsuleDisplays(); + + dispatchEvent(new CapsuleScriptEvent(DIFFICULTY_SWITCH, currentCapsule, currentDifficulty, currentVariation)); } /** @@ -2135,6 +2140,8 @@ class FreeplayState extends MusicBeatSubState FlxTimer.globalManager.clear(); dj?.onIntroDone.removeAll(); + dispatchEvent(new FreeplayScriptEvent(FREEPLAY_OUTRO)); + FunkinSound.playOnce(Paths.sound('cancelMenu')); var longestTimer:Float = 0; @@ -2195,6 +2202,7 @@ class FreeplayState extends MusicBeatSubState persist: true }); FlxG.sound.music.fadeIn(4.0, 0.0, 1.0); + dispatchEvent(new FreeplayScriptEvent(FREEPLAY_CLOSE)); close(); } else @@ -2651,6 +2659,8 @@ class FreeplayState extends MusicBeatSubState letterSort.inputEnabled = false; #end + dispatchEvent(new CapsuleScriptEvent(SONG_SELECTED, currentCapsule, currentDifficulty, currentVariation)); + PlayStatePlaylist.isStoryMode = false; var targetVariation:Null = currentVariation; @@ -2877,6 +2887,8 @@ class FreeplayState extends MusicBeatSubState // Small vibrations every selection change. if (change != 0) HapticUtil.vibrate(0, 0.01, 0.5); + + dispatchEvent(new CapsuleScriptEvent(CAPSULE_SELECTED, currentCapsule, currentDifficulty, currentVariation)); } public function playCurSongPreview(?daSongCapsule:SongMenuItem):Void diff --git a/source/funkin/ui/freeplay/backcards/BackingCard.hx b/source/funkin/ui/freeplay/backcards/BackingCard.hx index a005e315e..35faac9ba 100644 --- a/source/funkin/ui/freeplay/backcards/BackingCard.hx +++ b/source/funkin/ui/freeplay/backcards/BackingCard.hx @@ -10,9 +10,11 @@ import funkin.graphics.adobeanimate.FlxAtlasSprite; import funkin.graphics.FunkinSprite; import funkin.ui.freeplay.charselect.PlayableCharacter; import openfl.display.BlendMode; +import funkin.modding.events.ScriptEvent; import flixel.group.FlxSpriteGroup; import funkin.modding.IScriptedClass.IBPMSyncedScriptedClass; import funkin.modding.IScriptedClass.IStateChangingScriptedClass; +import funkin.modding.IScriptedClass.IFreeplayScriptedClass; import funkin.modding.events.ScriptEvent; import funkin.ui.FullScreenScaleMode; import funkin.util.BitmapUtil; @@ -20,7 +22,7 @@ import funkin.util.BitmapUtil; /** * A class for the backing cards so they dont have to be part of freeplayState...... */ -class BackingCard extends FlxSpriteGroup implements IBPMSyncedScriptedClass implements IStateChangingScriptedClass +class BackingCard extends FlxSpriteGroup implements IBPMSyncedScriptedClass implements IStateChangingScriptedClass implements IFreeplayScriptedClass { public var backingTextYeah:FlxAtlasSprite; public var orangeBackShit:FunkinSprite; @@ -275,6 +277,36 @@ class BackingCard extends FlxSpriteGroup implements IBPMSyncedScriptedClass impl public function onFocusGained(event:FocusScriptEvent):Void {} + /** + * Called when a capsule is selected. + */ + public function onCapsuleSelected(event:CapsuleScriptEvent):Void {} + + /** + * Called when the current difficulty is changed. + */ + public function onDifficultySwitch(event:CapsuleScriptEvent):Void {} + + /** + * Called when a song is selected. + */ + public function onSongSelected(event:CapsuleScriptEvent):Void {} + + /** + * Called when the intro for Freeplay finishes. + */ + public function onFreeplayIntroDone(event:FreeplayScriptEvent):Void {} + + /** + * Called when the Freeplay outro begins. + */ + public function onFreeplayOutro(event:FreeplayScriptEvent):Void {} + + /** + * Called when Freeplay is closed. + */ + public function onFreeplayClose(event:FreeplayScriptEvent):Void {} + public function centerObjectOnCard(object:flixel.FlxObject) { if (pinkBack != null) object.x = (x + ((pinkBack.width - object.width) / 2)) * 0.74; diff --git a/source/funkin/ui/freeplay/dj/BaseFreeplayDJ.hx b/source/funkin/ui/freeplay/dj/BaseFreeplayDJ.hx index b740cff53..25f84a302 100644 --- a/source/funkin/ui/freeplay/dj/BaseFreeplayDJ.hx +++ b/source/funkin/ui/freeplay/dj/BaseFreeplayDJ.hx @@ -9,6 +9,8 @@ import funkin.audio.FunkinSound; import funkin.data.freeplay.player.PlayerRegistry; import funkin.data.freeplay.player.PlayerData.PlayerFreeplayDJData; import funkin.util.assets.FlxAnimationUtil; +import funkin.modding.events.ScriptEvent; +import funkin.modding.IScriptedClass.IFreeplayScriptedClass; enum FreeplayDJState { @@ -62,7 +64,7 @@ enum FreeplayDJState } @:nullSafety -class BaseFreeplayDJ extends FlxAtlasSprite +class BaseFreeplayDJ extends FlxAtlasSprite implements IFreeplayScriptedClass { public var IDLE_EGG_PERIOD:Float = 60.0; public var IDLE_CARTOON_PERIOD:Float = 120.0; @@ -238,6 +240,64 @@ class BaseFreeplayDJ extends FlxAtlasSprite else offset.set(0, 0); } + + public function onScriptEvent(event:ScriptEvent) {} + + public function onCreate(event:ScriptEvent) {} + + public function onDestroy(event:ScriptEvent):Void {} + + public function onUpdate(event:UpdateScriptEvent):Void {} + + public function onStepHit(event:SongTimeScriptEvent):Void {} + + public function onBeatHit(event:SongTimeScriptEvent):Void {} + + public function onStateChangeBegin(event:StateChangeScriptEvent):Void {} + + public function onStateChangeEnd(event:StateChangeScriptEvent):Void {} + + public function onSubStateOpenBegin(event:SubStateScriptEvent):Void {} + + public function onSubStateOpenEnd(event:SubStateScriptEvent):Void {} + + public function onSubStateCloseBegin(event:SubStateScriptEvent):Void {} + + public function onSubStateCloseEnd(event:SubStateScriptEvent):Void {} + + public function onFocusLost(event:FocusScriptEvent):Void {} + + public function onFocusGained(event:FocusScriptEvent):Void {} + + /** + * Called when a capsule is selected. + */ + public function onCapsuleSelected(event:CapsuleScriptEvent):Void {} + + /** + * Called when the current difficulty is changed. + */ + public function onDifficultySwitch(event:CapsuleScriptEvent):Void {} + + /** + * Called when a song is selected. + */ + public function onSongSelected(event:CapsuleScriptEvent):Void {} + + /** + * Called when the intro for Freeplay finishes. + */ + public function onFreeplayIntroDone(event:FreeplayScriptEvent):Void {} + + /** + * Called when the Freeplay outro begins. + */ + public function onFreeplayOutro(event:FreeplayScriptEvent):Void {} + + /** + * Called when Freeplay is closed. + */ + public function onFreeplayClose(event:FreeplayScriptEvent):Void {} } // Class for all non-atalas DJ's