package funkin.play.event; import flixel.FlxSprite; import funkin.play.PlayState; import funkin.play.character.BaseCharacter; import funkin.play.song.SongData.RawSongEventData; import haxe.DynamicAccess; typedef RawSongEvent = { > RawSongEventData, /** * Whether the event has been activated or not. */ var a:Bool; } @:forward abstract SongEvent(RawSongEvent) { public function new(time:Float, event:String, value:Dynamic = null) { this = { t: time, e: event, v: value, a: false }; } public var time(get, set):Float; public function get_time():Float { return this.t; } public function set_time(value:Float):Float { return this.t = value; } public var event(get, set):String; public function get_event():String { return this.e; } public function set_event(value:String):String { return this.e = value; } public var value(get, set):Dynamic; public function get_value():Dynamic { return this.v; } public function set_value(value:Dynamic):Dynamic { return this.v = value; } public inline function getBool():Bool { return cast this.v; } public inline function getInt():Int { return cast this.v; } public inline function getFloat():Float { return cast this.v; } public inline function getString():String { return cast this.v; } public inline function getArray():Array { return cast this.v; } public inline function getMap():DynamicAccess { return cast this.v; } public inline function getBoolArray():Array { return cast this.v; } } typedef SongEventCallback = SongEvent->Void; class SongEventHandler { private static final eventCallbacks:Map = new Map(); public static function registerCallback(event:String, callback:SongEventCallback):Void { eventCallbacks.set(event, callback); } public static function unregisterCallback(event:String):Void { eventCallbacks.remove(event); } public static function clearCallbacks():Void { eventCallbacks.clear(); } /** * Register each of the event callbacks provided by the base game. */ public static function registerBaseEventCallbacks():Void { // TODO: Add a system for mods to easily add their own event callbacks. // Should be easy as creating character or stage scripts. registerCallback('FocusCamera', VanillaEventCallbacks.focusCamera); registerCallback('PlayAnimation', VanillaEventCallbacks.playAnimation); } /** * Given a list of song events and the current timestamp, * return a list of events that should be activated. */ public static function queryEvents(events:Array, currentTime:Float):Array { return events.filter(function(event:SongEvent):Bool { // If the event is already activated, don't activate it again. if (event.a) return false; // If the event is in the future, don't activate it. if (event.time > currentTime) return false; return true; }); } public static function activateEvents(events:Array):Void { for (event in events) { activateEvent(event); } } public static function activateEvent(event:SongEvent):Void { if (event.a) { trace('Event already activated: ' + event); return; } // Prevent the event from being activated again. event.a = true; // Perform the action. if (eventCallbacks.exists(event.event)) { eventCallbacks.get(event.event)(event); } } public static function resetEvents(events:Array):Void { for (event in events) { resetEvent(event); } } public static function resetEvent(event:SongEvent):Void { // TODO: Add a system for mods to easily add their reset callbacks. event.a = false; } } class VanillaEventCallbacks { /** * Event Name: "FocusCamera" * Event Value: Int * 0: Focus on the player. * 1: Focus on the opponent. * 2: Focus on the girlfriend. */ public static function focusCamera(event:SongEvent):Void { // Does nothing if there is no PlayState camera or stage. if (PlayState.instance == null || PlayState.instance.currentStage == null) return; switch (event.getInt()) { case 0: // Boyfriend // Focus the camera on the player. trace('[EVENT] Focusing camera on player.'); PlayState.instance.cameraFollowPoint.setPosition(PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.x, PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.y); case 1: // Dad // Focus the camera on the dad. trace('[EVENT] Focusing camera on dad.'); PlayState.instance.cameraFollowPoint.setPosition(PlayState.instance.currentStage.getDad().cameraFocusPoint.x, PlayState.instance.currentStage.getDad().cameraFocusPoint.y); case 2: // Girlfriend // Focus the camera on the girlfriend. trace('[EVENT] Focusing camera on girlfriend.'); PlayState.instance.cameraFollowPoint.setPosition(PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.x, PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.y); default: trace('[EVENT] Unknown camera focus: ' + event.value); } } /** * Event Name: "playAnimation" * Event Value: Object * { * target: String, // "player", "dad", "girlfriend", or * animation: String, * force: Bool // optional * } */ public static function playAnimation(event:SongEvent):Void { // Does nothing if there is no PlayState camera or stage. if (PlayState.instance == null || PlayState.instance.currentStage == null) return; var data:Dynamic = event.value; var targetName:String = Reflect.field(data, 'target'); var anim:String = Reflect.field(data, 'anim'); var force:Null = Reflect.field(data, 'force'); if (force == null) force = false; var target:FlxSprite = null; switch (targetName) { case 'boyfriend': trace('[EVENT] Playing animation $anim on boyfriend.'); target = PlayState.instance.currentStage.getBoyfriend(); case 'bf': trace('[EVENT] Playing animation $anim on boyfriend.'); target = PlayState.instance.currentStage.getBoyfriend(); case 'player': trace('[EVENT] Playing animation $anim on boyfriend.'); target = PlayState.instance.currentStage.getBoyfriend(); case 'dad': trace('[EVENT] Playing animation $anim on dad.'); target = PlayState.instance.currentStage.getDad(); case 'opponent': trace('[EVENT] Playing animation $anim on dad.'); target = PlayState.instance.currentStage.getDad(); case 'girlfriend': trace('[EVENT] Playing animation $anim on girlfriend.'); target = PlayState.instance.currentStage.getGirlfriend(); case 'gf': trace('[EVENT] Playing animation $anim on girlfriend.'); target = PlayState.instance.currentStage.getGirlfriend(); default: target = PlayState.instance.currentStage.getNamedProp(targetName); if (target == null) trace('[EVENT] Unknown animation target: $targetName'); else trace('[EVENT] Fetched animation target $targetName from stage.'); } if (target != null) { if (Std.isOfType(target, BaseCharacter)) { var targetChar:BaseCharacter = cast target; targetChar.playAnimation(anim, force, force); } else { target.animation.play(anim, force); } } } }