diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx index c99db1d0f..0843e0500 100644 --- a/source/funkin/modding/events/ScriptEvent.hx +++ b/source/funkin/modding/events/ScriptEvent.hx @@ -22,7 +22,7 @@ class ScriptEvent * * This event is not cancelable. */ - public static inline final CREATE:ScriptEventType = "CREATE"; + public static inline final CREATE:ScriptEventType = 'CREATE'; /** * Called when the relevant object is destroyed. @@ -30,7 +30,7 @@ class ScriptEvent * * This event is not cancelable. */ - public static inline final DESTROY:ScriptEventType = "DESTROY"; + public static inline final DESTROY:ScriptEventType = 'DESTROY'; /** * Called when the relevent object is added to the game state. @@ -46,35 +46,35 @@ class ScriptEvent * * This event is not cancelable. */ - public static inline final UPDATE:ScriptEventType = "UPDATE"; + public static inline final UPDATE:ScriptEventType = 'UPDATE'; /** * Called when the player moves to pause the game. * * This event IS cancelable! Canceling the event will prevent the game from pausing. */ - public static inline final PAUSE:ScriptEventType = "PAUSE"; + public static inline final PAUSE:ScriptEventType = 'PAUSE'; /** * Called when the player moves to unpause the game while paused. * * This event IS cancelable! Canceling the event will prevent the game from resuming. */ - public static inline final RESUME:ScriptEventType = "RESUME"; + public static inline final RESUME:ScriptEventType = 'RESUME'; /** * Called once per step in the song. This happens 4 times per measure. * * This event is not cancelable. */ - public static inline final SONG_BEAT_HIT:ScriptEventType = "BEAT_HIT"; + public static inline final SONG_BEAT_HIT:ScriptEventType = 'BEAT_HIT'; /** * Called once per step in the song. This happens 16 times per measure. * * This event is not cancelable. */ - public static inline final SONG_STEP_HIT:ScriptEventType = "STEP_HIT"; + public static inline final SONG_STEP_HIT:ScriptEventType = 'STEP_HIT'; /** * Called when a character hits a note. @@ -83,7 +83,7 @@ class ScriptEvent * This event IS cancelable! Canceling this event prevents the note from being hit, * and will likely result in a miss later. */ - public static inline final NOTE_HIT:ScriptEventType = "NOTE_HIT"; + public static inline final NOTE_HIT:ScriptEventType = 'NOTE_HIT'; /** * Called when a character misses a note. @@ -92,7 +92,7 @@ class ScriptEvent * This event IS cancelable! Canceling this event prevents the note from being considered missed, * avoiding a combo break and lost health. */ - public static inline final NOTE_MISS:ScriptEventType = "NOTE_MISS"; + public static inline final NOTE_MISS:ScriptEventType = 'NOTE_MISS'; /** * Called when a character presses a note when there was none there, causing them to lose health. @@ -101,7 +101,7 @@ class ScriptEvent * This event IS cancelable! Canceling this event prevents the note from being considered missed, * avoiding lost health/score and preventing the miss animation. */ - public static inline final NOTE_GHOST_MISS:ScriptEventType = "NOTE_GHOST_MISS"; + public static inline final NOTE_GHOST_MISS:ScriptEventType = 'NOTE_GHOST_MISS'; /** * Called when a song event is reached in the chart. @@ -109,21 +109,21 @@ class ScriptEvent * This event IS cancelable! Cancelling this event prevents the event from being triggered, * thus blocking its normal functionality. */ - public static inline final SONG_EVENT:ScriptEventType = "SONG_EVENT"; + public static inline final SONG_EVENT:ScriptEventType = 'SONG_EVENT'; /** * Called when the song starts. This occurs as the countdown ends and the instrumental and vocals begin. * * This event is not cancelable. */ - public static inline final SONG_START:ScriptEventType = "SONG_START"; + public static inline final SONG_START:ScriptEventType = 'SONG_START'; /** * Called when the song ends. This happens as the instrumental and vocals end. * * This event is not cancelable. */ - public static inline final SONG_END:ScriptEventType = "SONG_END"; + public static inline final SONG_END:ScriptEventType = 'SONG_END'; /** * Called when the countdown begins. This occurs before the song starts. @@ -132,7 +132,7 @@ class ScriptEvent * - The song will not start until you call Countdown.performCountdown() later. * - Note that calling performCountdown() will trigger this event again, so be sure to add logic to ignore it. */ - public static inline final COUNTDOWN_START:ScriptEventType = "COUNTDOWN_START"; + public static inline final COUNTDOWN_START:ScriptEventType = 'COUNTDOWN_START'; /** * Called when a step of the countdown happens. @@ -141,21 +141,21 @@ class ScriptEvent * This event IS cancelable! Canceling this event will pause the countdown. * - The countdown will not resume until you call PlayState.resumeCountdown(). */ - public static inline final COUNTDOWN_STEP:ScriptEventType = "COUNTDOWN_STEP"; + public static inline final COUNTDOWN_STEP:ScriptEventType = 'COUNTDOWN_STEP'; /** * Called when the countdown is done but just before the song starts. * * This event is not cancelable. */ - public static inline final COUNTDOWN_END:ScriptEventType = "COUNTDOWN_END"; + public static inline final COUNTDOWN_END:ScriptEventType = 'COUNTDOWN_END'; /** * Called before the game over screen triggers and the death animation plays. * * This event is not cancelable. */ - public static inline final GAME_OVER:ScriptEventType = "GAME_OVER"; + public static inline final GAME_OVER:ScriptEventType = 'GAME_OVER'; /** * Called after the player presses a key to restart the game. @@ -163,21 +163,21 @@ class ScriptEvent * * This event IS cancelable! Canceling this event will prevent the game from restarting. */ - public static inline final SONG_RETRY:ScriptEventType = "SONG_RETRY"; + public static inline final SONG_RETRY:ScriptEventType = 'SONG_RETRY'; /** * Called when the player pushes down any key on the keyboard. * * This event is not cancelable. */ - public static inline final KEY_DOWN:ScriptEventType = "KEY_DOWN"; + public static inline final KEY_DOWN:ScriptEventType = 'KEY_DOWN'; /** * Called when the player releases a key on the keyboard. * * This event is not cancelable. */ - public static inline final KEY_UP:ScriptEventType = "KEY_UP"; + public static inline final KEY_UP:ScriptEventType = 'KEY_UP'; /** * Called when the game has finished loading the notes from JSON. @@ -185,49 +185,49 @@ class ScriptEvent * * This event is not cancelable. */ - public static inline final SONG_LOADED:ScriptEventType = "SONG_LOADED"; + public static inline final SONG_LOADED:ScriptEventType = 'SONG_LOADED'; /** * Called when the game is about to switch the current FlxState. * * This event is not cancelable. */ - public static inline final STATE_CHANGE_BEGIN:ScriptEventType = "STATE_CHANGE_BEGIN"; + public static inline final STATE_CHANGE_BEGIN:ScriptEventType = 'STATE_CHANGE_BEGIN'; /** * Called when the game has finished switching the current FlxState. * * This event is not cancelable. */ - public static inline final STATE_CHANGE_END:ScriptEventType = "STATE_CHANGE_END"; + public static inline final STATE_CHANGE_END:ScriptEventType = 'STATE_CHANGE_END'; /** * Called when the game is about to open a new FlxSubState. * * This event is not cancelable. */ - public static inline final SUBSTATE_OPEN_BEGIN:ScriptEventType = "SUBSTATE_OPEN_BEGIN"; + public static inline final SUBSTATE_OPEN_BEGIN:ScriptEventType = 'SUBSTATE_OPEN_BEGIN'; /** * Called when the game has finished opening a new FlxSubState. * * This event is not cancelable. */ - public static inline final SUBSTATE_OPEN_END:ScriptEventType = "SUBSTATE_OPEN_END"; + public static inline final SUBSTATE_OPEN_END:ScriptEventType = 'SUBSTATE_OPEN_END'; /** * Called when the game is about to close the current FlxSubState. * * This event is not cancelable. */ - public static inline final SUBSTATE_CLOSE_BEGIN:ScriptEventType = "SUBSTATE_CLOSE_BEGIN"; + public static inline final SUBSTATE_CLOSE_BEGIN:ScriptEventType = 'SUBSTATE_CLOSE_BEGIN'; /** * Called when the game has finished closing the current FlxSubState. * * This event is not cancelable. */ - public static inline final SUBSTATE_CLOSE_END:ScriptEventType = "SUBSTATE_CLOSE_END"; + public static inline final SUBSTATE_CLOSE_END:ScriptEventType = 'SUBSTATE_CLOSE_END'; /** * Called when the game is exiting the current FlxState. @@ -276,9 +276,12 @@ class ScriptEvent } } + /** + * Cancel this event. + * This is an alias for cancelEvent() but I make this typo all the time. + */ public function cancel():Void { - // This typo happens enough that I just added this. cancelEvent(); } @@ -316,11 +319,17 @@ class NoteScriptEvent extends ScriptEvent */ public var comboCount(default, null):Int; + /** + * Whether to play the record scratch sound (if this eventn type is `NOTE_MISS`). + */ + public var playSound(default, default):Bool; + public function new(type:ScriptEventType, note:Note, comboCount:Int = 0, cancelable:Bool = false):Void { super(type, cancelable); this.note = note; this.comboCount = comboCount; + this.playSound = true; } public override function toString():String @@ -468,7 +477,7 @@ class CountdownScriptEvent extends ScriptEvent */ public var step(default, null):CountdownStep; - public function new(type:ScriptEventType, step:CountdownStep, cancelable = true):Void + public function new(type:ScriptEventType, step:CountdownStep, cancelable:Bool = true):Void { super(type, cancelable); this.step = step; diff --git a/source/funkin/play/event/FocusCameraSongEvent.hx b/source/funkin/play/event/FocusCameraSongEvent.hx index 29d2e80e0..f46bccec9 100644 --- a/source/funkin/play/event/FocusCameraSongEvent.hx +++ b/source/funkin/play/event/FocusCameraSongEvent.hx @@ -1,7 +1,9 @@ package funkin.play.event; -import funkin.play.event.SongEvent; import funkin.play.song.SongData; +import funkin.play.event.SongEvent; +import funkin.play.event.SongEventData.SongEventFieldType; +import funkin.play.event.SongEventData.SongEventSchema; /** * This class represents a handler for a type of song event. diff --git a/source/funkin/play/event/PlayAnimationSongEvent.hx b/source/funkin/play/event/PlayAnimationSongEvent.hx index 73937e61a..a187ca285 100644 --- a/source/funkin/play/event/PlayAnimationSongEvent.hx +++ b/source/funkin/play/event/PlayAnimationSongEvent.hx @@ -3,6 +3,8 @@ package funkin.play.event; import flixel.FlxSprite; import funkin.play.character.BaseCharacter; import funkin.play.event.SongEvent; +import funkin.play.event.SongEventData.SongEventFieldType; +import funkin.play.event.SongEventData.SongEventSchema; import funkin.play.song.SongData; class PlayAnimationSongEvent extends SongEvent diff --git a/source/funkin/play/event/SetCameraBopSongEvent.hx b/source/funkin/play/event/SetCameraBopSongEvent.hx new file mode 100644 index 000000000..6f8a0645d --- /dev/null +++ b/source/funkin/play/event/SetCameraBopSongEvent.hx @@ -0,0 +1,90 @@ +package funkin.play.event; + +import funkin.util.Constants; +import flixel.tweens.FlxTween; +import flixel.FlxCamera; +import flixel.tweens.FlxEase; +import funkin.play.event.SongEvent; +import funkin.play.song.SongData; +import funkin.play.event.SongEventData; +import funkin.play.event.SongEventData.SongEventFieldType; + +/** + * This class represents a handler for configuring camera bop intensity and rate. + * + * Example: Bop the camera twice as hard, once per beat (rather than once every four beats). + * ``` + * { + * 'e': 'SetCameraBop', + * 'v': { + * 'intensity': 2.0, + * 'rate': 1, + * } + * } + * ``` + * + * Example: Reset the camera bop to default values. + * ``` + * { + * 'e': 'SetCameraBop', + * 'v': {} + * } + * ``` + */ +class SetCameraBopSongEvent extends SongEvent +{ + public function new() + { + super('SetCameraBop'); + } + + public override function handleEvent(data:SongEventData):Void + { + // Does nothing if there is no PlayState camera or stage. + if (PlayState.instance == null) return; + + var rate:Null = data.getInt('rate'); + if (rate == null) rate = Constants.DEFAULT_ZOOM_RATE; + var intensity:Null = data.getFloat('intensity'); + if (intensity == null) intensity = 1.0; + + PlayState.instance.cameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY * intensity; + PlayState.instance.hudCameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY * intensity * 2.0; + PlayState.instance.cameraZoomRate = rate; + trace('Set camera zoom rate to ${PlayState.instance.cameraZoomRate}'); + } + + public override function getTitle():String + { + return 'Set Camera Bop'; + } + + /** + * ``` + * { + * 'intensity': FLOAT, // Zoom amount + * 'rate': INT, // Zoom rate (beats/zoom) + * } + * ``` + * @return SongEventSchema + */ + public override function getEventSchema():SongEventSchema + { + return [ + { + name: 'intensity', + title: 'Intensity', + defaultValue: 1.0, + step: 0.1, + type: SongEventFieldType.FLOAT + }, + { + name: 'rate', + title: 'Rate (beats/zoom)', + defaultValue: 4, + step: 1, + type: SongEventFieldType.INTEGER, + } + ]; + } +} diff --git a/source/funkin/play/event/SongEvent.hx b/source/funkin/play/event/SongEvent.hx index 4071f9a34..098a84e12 100644 --- a/source/funkin/play/event/SongEvent.hx +++ b/source/funkin/play/event/SongEvent.hx @@ -1,7 +1,7 @@ package funkin.play.event; -import funkin.util.macro.ClassMacro; import funkin.play.song.SongData.SongEventData; +import funkin.play.event.SongEventData.SongEventSchema; /** * This class represents a handler for a type of song event. @@ -52,233 +52,3 @@ class SongEvent return 'SongEvent(${this.id})'; } } - -/** - * This class statically handles the parsing of internal and scripted song event handlers. - */ -class SongEventParser -{ - /** - * Every built-in event class must be added to this list. - * Thankfully, with the power of `SongEventMacro`, this is done automatically. - */ - static final BUILTIN_EVENTS:List> = ClassMacro.listSubclassesOf(SongEvent); - - /** - * Map of internal handlers for song events. - * These may be either `ScriptedSongEvents` or built-in classes extending `SongEvent`. - */ - static final eventCache:Map = new Map(); - - public static function loadEventCache():Void - { - clearEventCache(); - - // - // BASE GAME EVENTS - // - registerBaseEvents(); - registerScriptedEvents(); - } - - static function registerBaseEvents() - { - trace('Instantiating ${BUILTIN_EVENTS.length} built-in song events...'); - for (eventCls in BUILTIN_EVENTS) - { - var eventClsName:String = Type.getClassName(eventCls); - if (eventClsName == 'funkin.play.event.SongEvent' || eventClsName == 'funkin.play.event.ScriptedSongEvent') continue; - - var event:SongEvent = Type.createInstance(eventCls, ["UNKNOWN"]); - - if (event != null) - { - trace(' Loaded built-in song event: (${event.id})'); - eventCache.set(event.id, event); - } - else - { - trace(' Failed to load built-in song event: ${Type.getClassName(eventCls)}'); - } - } - } - - static function registerScriptedEvents() - { - var scriptedEventClassNames:Array = ScriptedSongEvent.listScriptClasses(); - if (scriptedEventClassNames == null || scriptedEventClassNames.length == 0) return; - - trace('Instantiating ${scriptedEventClassNames.length} scripted song events...'); - for (eventCls in scriptedEventClassNames) - { - var event:SongEvent = ScriptedSongEvent.init(eventCls, "UKNOWN"); - - if (event != null) - { - trace(' Loaded scripted song event: ${event.id}'); - eventCache.set(event.id, event); - } - else - { - trace(' Failed to instantiate scripted song event class: ${eventCls}'); - } - } - } - - public static function listEventIds():Array - { - return eventCache.keys().array(); - } - - public static function listEvents():Array - { - return eventCache.values(); - } - - public static function getEvent(id:String):SongEvent - { - return eventCache.get(id); - } - - public static function getEventSchema(id:String):SongEventSchema - { - var event:SongEvent = getEvent(id); - if (event == null) return null; - - return event.getEventSchema(); - } - - static function clearEventCache() - { - eventCache.clear(); - } - - public static function handleEvent(data:SongEventData):Void - { - var eventType:String = data.event; - var eventHandler:SongEvent = eventCache.get(eventType); - - if (eventHandler != null) - { - eventHandler.handleEvent(data); - } - else - { - trace('WARNING: No event handler for event with id: ${eventType}'); - } - - data.activated = true; - } - - public static inline function handleEvents(events:Array):Void - { - for (event in events) - { - handleEvent(event); - } - } - - /** - * Given a list of song events and the current timestamp, - * return a list of events that should be handled. - */ - public static function queryEvents(events:Array, currentTime:Float):Array - { - return events.filter(function(event:SongEventData):Bool - { - // If the event is already activated, don't activate it again. - if (event.activated) return false; - - // If the event is in the future, don't activate it. - if (event.time > currentTime) return false; - - return true; - }); - } - - /** - * Reset activation of all the provided events. - */ - public static function resetEvents(events:Array):Void - { - for (event in events) - { - event.activated = false; - // TODO: Add an onReset() method to SongEvent? - } - } -} - -enum abstract SongEventFieldType(String) from String to String -{ - /** - * The STRING type will display as a text field. - */ - var STRING = "string"; - - /** - * The INTEGER type will display as a text field that only accepts numbers. - */ - var INTEGER = "integer"; - - /** - * The FLOAT type will display as a text field that only accepts numbers. - */ - var FLOAT = "float"; - - /** - * The BOOL type will display as a checkbox. - */ - var BOOL = "bool"; - - /** - * The ENUM type will display as a dropdown. - * Make sure to specify the `keys` field in the schema. - */ - var ENUM = "enum"; -} - -typedef SongEventSchemaField = -{ - /** - * The name of the property as it should be saved in the event data. - */ - name:String, - - /** - * The title of the field to display in the UI. - */ - title:String, - - /** - * The type of the field. - */ - type:SongEventFieldType, - - /** - * Used for ENUM values. - * The key is the display name and the value is the actual value. - */ - ?keys:Map, - /** - * Used for INTEGER and FLOAT values. - * The minimum value that can be entered. - */ - ?min:Float, - /** - * Used for INTEGER and FLOAT values. - * The maximum value that can be entered. - */ - ?max:Float, - /** - * Used for INTEGER and FLOAT values. - * The step value that will be used when incrementing/decrementing the value. - */ - ?step:Float, - /** - * An optional default value for the field. - */ - ?defaultValue:Dynamic, -} - -typedef SongEventSchema = Array; diff --git a/source/funkin/play/event/SongEventData.hx b/source/funkin/play/event/SongEventData.hx new file mode 100644 index 000000000..8c157b52a --- /dev/null +++ b/source/funkin/play/event/SongEventData.hx @@ -0,0 +1,235 @@ +package funkin.play.event; + +import funkin.play.event.SongEventData.SongEventSchema; +import funkin.play.song.SongData.SongEventData; +import funkin.util.macro.ClassMacro; +import funkin.play.event.ScriptedSongEvent; + +/** + * This class statically handles the parsing of internal and scripted song event handlers. + */ +class SongEventParser +{ + /** + * Every built-in event class must be added to this list. + * Thankfully, with the power of `SongEventMacro`, this is done automatically. + */ + static final BUILTIN_EVENTS:List> = ClassMacro.listSubclassesOf(SongEvent); + + /** + * Map of internal handlers for song events. + * These may be either `ScriptedSongEvents` or built-in classes extending `SongEvent`. + */ + static final eventCache:Map = new Map(); + + public static function loadEventCache():Void + { + clearEventCache(); + + // + // BASE GAME EVENTS + // + registerBaseEvents(); + registerScriptedEvents(); + } + + static function registerBaseEvents() + { + trace('Instantiating ${BUILTIN_EVENTS.length} built-in song events...'); + for (eventCls in BUILTIN_EVENTS) + { + var eventClsName:String = Type.getClassName(eventCls); + if (eventClsName == 'funkin.play.event.SongEvent' || eventClsName == 'funkin.play.event.ScriptedSongEvent') continue; + + var event:SongEvent = Type.createInstance(eventCls, ["UNKNOWN"]); + + if (event != null) + { + trace(' Loaded built-in song event: (${event.id})'); + eventCache.set(event.id, event); + } + else + { + trace(' Failed to load built-in song event: ${Type.getClassName(eventCls)}'); + } + } + } + + static function registerScriptedEvents() + { + var scriptedEventClassNames:Array = ScriptedSongEvent.listScriptClasses(); + if (scriptedEventClassNames == null || scriptedEventClassNames.length == 0) return; + + trace('Instantiating ${scriptedEventClassNames.length} scripted song events...'); + for (eventCls in scriptedEventClassNames) + { + var event:SongEvent = ScriptedSongEvent.init(eventCls, "UKNOWN"); + + if (event != null) + { + trace(' Loaded scripted song event: ${event.id}'); + eventCache.set(event.id, event); + } + else + { + trace(' Failed to instantiate scripted song event class: ${eventCls}'); + } + } + } + + public static function listEventIds():Array + { + return eventCache.keys().array(); + } + + public static function listEvents():Array + { + return eventCache.values(); + } + + public static function getEvent(id:String):SongEvent + { + return eventCache.get(id); + } + + public static function getEventSchema(id:String):SongEventSchema + { + var event:SongEvent = getEvent(id); + if (event == null) return null; + + return event.getEventSchema(); + } + + static function clearEventCache() + { + eventCache.clear(); + } + + public static function handleEvent(data:SongEventData):Void + { + var eventType:String = data.event; + var eventHandler:SongEvent = eventCache.get(eventType); + + if (eventHandler != null) + { + eventHandler.handleEvent(data); + } + else + { + trace('WARNING: No event handler for event with id: ${eventType}'); + } + + data.activated = true; + } + + public static inline function handleEvents(events:Array):Void + { + for (event in events) + { + handleEvent(event); + } + } + + /** + * Given a list of song events and the current timestamp, + * return a list of events that should be handled. + */ + public static function queryEvents(events:Array, currentTime:Float):Array + { + return events.filter(function(event:SongEventData):Bool { + // If the event is already activated, don't activate it again. + if (event.activated) return false; + + // If the event is in the future, don't activate it. + if (event.time > currentTime) return false; + + return true; + }); + } + + /** + * Reset activation of all the provided events. + */ + public static function resetEvents(events:Array):Void + { + for (event in events) + { + event.activated = false; + // TODO: Add an onReset() method to SongEvent? + } + } +} + +enum abstract SongEventFieldType(String) from String to String +{ + /** + * The STRING type will display as a text field. + */ + var STRING = "string"; + + /** + * The INTEGER type will display as a text field that only accepts numbers. + */ + var INTEGER = "integer"; + + /** + * The FLOAT type will display as a text field that only accepts numbers. + */ + var FLOAT = "float"; + + /** + * The BOOL type will display as a checkbox. + */ + var BOOL = "bool"; + + /** + * The ENUM type will display as a dropdown. + * Make sure to specify the `keys` field in the schema. + */ + var ENUM = "enum"; +} + +typedef SongEventSchemaField = +{ + /** + * The name of the property as it should be saved in the event data. + */ + name:String, + + /** + * The title of the field to display in the UI. + */ + title:String, + + /** + * The type of the field. + */ + type:SongEventFieldType, + + /** + * Used for ENUM values. + * The key is the display name and the value is the actual value. + */ + ?keys:Map, + /** + * Used for INTEGER and FLOAT values. + * The minimum value that can be entered. + */ + ?min:Float, + /** + * Used for INTEGER and FLOAT values. + * The maximum value that can be entered. + */ + ?max:Float, + /** + * Used for INTEGER and FLOAT values. + * The step value that will be used when incrementing/decrementing the value. + */ + ?step:Float, + /** + * An optional default value for the field. + */ + ?defaultValue:Dynamic, +} + +typedef SongEventSchema = Array; diff --git a/source/funkin/play/event/ZoomCameraSongEvent.hx b/source/funkin/play/event/ZoomCameraSongEvent.hx new file mode 100644 index 000000000..bceeb251a --- /dev/null +++ b/source/funkin/play/event/ZoomCameraSongEvent.hx @@ -0,0 +1,148 @@ +package funkin.play.event; + +import flixel.tweens.FlxTween; +import flixel.FlxCamera; +import flixel.tweens.FlxEase; +import funkin.play.event.SongEvent; +import funkin.play.song.SongData; +import funkin.play.event.SongEventData; +import funkin.play.event.SongEventData.SongEventFieldType; + +/** + * This class represents a handler for camera zoom events. + * + * Example: Zoom to 1.3x: + * ``` + * { + * 'e': 'ZoomCamera', + * 'v': 1.3 + * } + * ``` + * + * Example: Zoom to 1.3x + * ``` + * { + * 'e': 'FocusCamera', + * 'v': { + * 'char': 2, + * 'y': -10, + * } + * } + * ``` + * + * Example: Focus on (100, 100): + * ``` + * { + * 'e': 'FocusCamera', + * 'v': { + * 'char': -1, + * 'x': 100, + * 'y': 100, + * } + * } + * ``` + */ +class ZoomCameraSongEvent extends SongEvent +{ + public function new() + { + super('ZoomCamera'); + } + + public override function handleEvent(data:SongEventData):Void + { + // Does nothing if there is no PlayState camera or stage. + if (PlayState.instance == null) return; + + var zoom:Null = data.getFloat('zoom'); + if (zoom == null) zoom = 1.0; + var duration:Null = data.getFloat('duration'); + if (duration == null) duration = 4.0; + + var ease:Null = data.getString('ease'); + if (ease == null) ease = 'linear'; + + // If it's a string, check the value. + switch (ease) + { + case 'INSTANT': + // Set the zoom. Use defaultCameraZoom to prevent breaking camera bops. + PlayState.instance.defaultCameraZoom = zoom * FlxCamera.defaultZoom; + default: + var easeFunction:NullFloat> = Reflect.field(FlxEase, ease); + if (easeFunction == null) + { + trace('Invalid ease function: $ease'); + return; + } + + FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.stepLengthMs * duration / 1000), + {ease: easeFunction}); + } + } + + public override function getTitle():String + { + return 'Zoom Camera'; + } + + /** + * ``` + * { + * 'zoom': FLOAT, // Target zoom level. + * 'duration': FLOAT, // Optional duration in steps + * 'ease': ENUM, // Optional easing function + * } + * @return SongEventSchema + */ + public override function getEventSchema():SongEventSchema + { + return [ + { + name: 'zoom', + title: 'Zoom Level', + defaultValue: 1.0, + step: 0.1, + type: SongEventFieldType.FLOAT + }, + { + name: 'duration', + title: 'Duration (in steps)', + defaultValue: 4.0, + step: 0.5, + type: SongEventFieldType.FLOAT, + }, + { + name: 'ease', + title: 'Easing Type', + defaultValue: 'linear', + type: SongEventFieldType.ENUM, + keys: [ + 'Linear' => 'linear', + 'Instant' => 'INSTANT', + 'Quad In' => 'quadIn', + 'Quad Out' => 'quadOut', + 'Quad In/Out' => 'quadInOut', + 'Cube In' => 'cubeIn', + 'Cube Out' => 'cubeOut', + 'Cube In/Out' => 'cubeInOut', + 'Quart In' => 'quartIn', + 'Quart Out' => 'quartOut', + 'Quart In/Out' => 'quartInOut', + 'Quint In' => 'quintIn', + 'Quint Out' => 'quintOut', + 'Quint In/Out' => 'quintInOut', + 'Smooth Step In' => 'smoothStepIn', + 'Smooth Step Out' => 'smoothStepOut', + 'Smooth Step In/Out' => 'smoothStepInOut', + 'Sine In' => 'sineIn', + 'Sine Out' => 'sineOut', + 'Sine In/Out' => 'sineInOut', + 'Elastic In' => 'elasticIn', + 'Elastic Out' => 'elasticOut', + 'Elastic In/Out' => 'elasticInOut', + ] + } + ]; + } +}