Internal rework to song events

This commit is contained in:
EliteMasterEric 2023-06-02 14:35:28 -04:00
parent 57caeef802
commit 4851ff5c27
7 changed files with 517 additions and 261 deletions

View file

@ -22,7 +22,7 @@ class ScriptEvent
* *
* This event is not cancelable. * 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. * Called when the relevant object is destroyed.
@ -30,7 +30,7 @@ class ScriptEvent
* *
* This event is not cancelable. * 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. * Called when the relevent object is added to the game state.
@ -46,35 +46,35 @@ class ScriptEvent
* *
* This event is not cancelable. * 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. * Called when the player moves to pause the game.
* *
* This event IS cancelable! Canceling the event will prevent the game from pausing. * 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. * Called when the player moves to unpause the game while paused.
* *
* This event IS cancelable! Canceling the event will prevent the game from resuming. * 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. * Called once per step in the song. This happens 4 times per measure.
* *
* This event is not cancelable. * 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. * Called once per step in the song. This happens 16 times per measure.
* *
* This event is not cancelable. * 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. * 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, * This event IS cancelable! Canceling this event prevents the note from being hit,
* and will likely result in a miss later. * 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. * 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, * This event IS cancelable! Canceling this event prevents the note from being considered missed,
* avoiding a combo break and lost health. * 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. * 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, * This event IS cancelable! Canceling this event prevents the note from being considered missed,
* avoiding lost health/score and preventing the miss animation. * 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. * 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, * This event IS cancelable! Cancelling this event prevents the event from being triggered,
* thus blocking its normal functionality. * 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. * Called when the song starts. This occurs as the countdown ends and the instrumental and vocals begin.
* *
* This event is not cancelable. * 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. * Called when the song ends. This happens as the instrumental and vocals end.
* *
* This event is not cancelable. * 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. * 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. * - 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. * - 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. * 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. * This event IS cancelable! Canceling this event will pause the countdown.
* - The countdown will not resume until you call PlayState.resumeCountdown(). * - 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. * Called when the countdown is done but just before the song starts.
* *
* This event is not cancelable. * 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. * Called before the game over screen triggers and the death animation plays.
* *
* This event is not cancelable. * 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. * 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. * 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. * Called when the player pushes down any key on the keyboard.
* *
* This event is not cancelable. * 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. * Called when the player releases a key on the keyboard.
* *
* This event is not cancelable. * 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. * Called when the game has finished loading the notes from JSON.
@ -185,49 +185,49 @@ class ScriptEvent
* *
* This event is not cancelable. * 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. * Called when the game is about to switch the current FlxState.
* *
* This event is not cancelable. * 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. * Called when the game has finished switching the current FlxState.
* *
* This event is not cancelable. * 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. * Called when the game is about to open a new FlxSubState.
* *
* This event is not cancelable. * 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. * Called when the game has finished opening a new FlxSubState.
* *
* This event is not cancelable. * 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. * Called when the game is about to close the current FlxSubState.
* *
* This event is not cancelable. * 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. * Called when the game has finished closing the current FlxSubState.
* *
* This event is not cancelable. * 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. * 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 public function cancel():Void
{ {
// This typo happens enough that I just added this.
cancelEvent(); cancelEvent();
} }
@ -316,11 +319,17 @@ class NoteScriptEvent extends ScriptEvent
*/ */
public var comboCount(default, null):Int; 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 public function new(type:ScriptEventType, note:Note, comboCount:Int = 0, cancelable:Bool = false):Void
{ {
super(type, cancelable); super(type, cancelable);
this.note = note; this.note = note;
this.comboCount = comboCount; this.comboCount = comboCount;
this.playSound = true;
} }
public override function toString():String public override function toString():String
@ -468,7 +477,7 @@ class CountdownScriptEvent extends ScriptEvent
*/ */
public var step(default, null):CountdownStep; 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); super(type, cancelable);
this.step = step; this.step = step;

View file

@ -1,7 +1,9 @@
package funkin.play.event; package funkin.play.event;
import funkin.play.event.SongEvent;
import funkin.play.song.SongData; 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. * This class represents a handler for a type of song event.

View file

@ -3,6 +3,8 @@ package funkin.play.event;
import flixel.FlxSprite; import flixel.FlxSprite;
import funkin.play.character.BaseCharacter; import funkin.play.character.BaseCharacter;
import funkin.play.event.SongEvent; import funkin.play.event.SongEvent;
import funkin.play.event.SongEventData.SongEventFieldType;
import funkin.play.event.SongEventData.SongEventSchema;
import funkin.play.song.SongData; import funkin.play.song.SongData;
class PlayAnimationSongEvent extends SongEvent class PlayAnimationSongEvent extends SongEvent

View file

@ -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<Int> = data.getInt('rate');
if (rate == null) rate = Constants.DEFAULT_ZOOM_RATE;
var intensity:Null<Float> = 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,
}
];
}
}

View file

@ -1,7 +1,7 @@
package funkin.play.event; package funkin.play.event;
import funkin.util.macro.ClassMacro;
import funkin.play.song.SongData.SongEventData; import funkin.play.song.SongData.SongEventData;
import funkin.play.event.SongEventData.SongEventSchema;
/** /**
* This class represents a handler for a type of song event. * This class represents a handler for a type of song event.
@ -52,233 +52,3 @@ class SongEvent
return 'SongEvent(${this.id})'; 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<Class<SongEvent>> = 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<String, SongEvent> = new Map<String, SongEvent>();
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<String> = 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<String>
{
return eventCache.keys().array();
}
public static function listEvents():Array<SongEvent>
{
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<SongEventData>):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<SongEventData>, currentTime:Float):Array<SongEventData>
{
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<SongEventData>):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<String, Dynamic>,
/**
* 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<SongEventSchemaField>;

View file

@ -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<Class<SongEvent>> = 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<String, SongEvent> = new Map<String, SongEvent>();
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<String> = 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<String>
{
return eventCache.keys().array();
}
public static function listEvents():Array<SongEvent>
{
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<SongEventData>):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<SongEventData>, currentTime:Float):Array<SongEventData>
{
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<SongEventData>):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<String, Dynamic>,
/**
* 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<SongEventSchemaField>;

View file

@ -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<Float> = data.getFloat('zoom');
if (zoom == null) zoom = 1.0;
var duration:Null<Float> = data.getFloat('duration');
if (duration == null) duration = 4.0;
var ease:Null<String> = 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:Null<Float->Float> = 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',
]
}
];
}
}