diff --git a/.gitignore b/.gitignore index 9f24f450e..8a4d131df 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,4 @@ export/ .vscode/ APIStuff.hx .DS_STORE - -example_mods/enaSkin/ - -example_mods/tricky/ +RECOVER_*.fla \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 4afcd170e..9a277f85c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,6 +12,21 @@ "name": "Haxe Eval", "type": "haxe-eval", "request": "launch" - } + }, + { + "name": "HTML5 Debug", + "type": "chrome", + "request": "launch", + "url": "http://127.0.0.1:3001", + "sourceMaps": true, + "webRoot": "${workspaceFolder}", + "preLaunchTask": "debug: html" + }, + { + "name": "Mac (Debug)", + "type": "hxcpp", + "request": "launch", + "program": "${workspaceRoot}/export/debug/macos/bin/Funkin.app/Contents/MacOS/Funkin", + "preLaunchTask": "debug: mac" ] } diff --git a/Project.xml b/Project.xml index 1382f5f71..89402c2eb 100644 --- a/Project.xml +++ b/Project.xml @@ -96,7 +96,13 @@ - + @@ -122,7 +128,7 @@ - + @@ -191,7 +197,7 @@ - + diff --git a/example_mods/introMod/_polymod_meta.json b/example_mods/introMod/_polymod_meta.json index ccd7405d8..4c7fd742e 100644 --- a/example_mods/introMod/_polymod_meta.json +++ b/example_mods/introMod/_polymod_meta.json @@ -1,7 +1,9 @@ { "title": "Intro Mod", "description": "An introductory mod.", - "author": "MasterEric", + "contributors": [{ + "name": "MasterEric" + }], "api_version": "0.1.0", "mod_version": "1.0.0", "license": "Apache-2.0" diff --git a/example_mods/testing123/_polymod_meta.json b/example_mods/testing123/_polymod_meta.json index 0c134dd9f..1a7766820 100644 --- a/example_mods/testing123/_polymod_meta.json +++ b/example_mods/testing123/_polymod_meta.json @@ -1,7 +1,9 @@ { "title": "Testing123", "description": "Newgrounds? More like OLDGROUNDS lol.", - "author": "MasterEric", + "contributors": [{ + "name": "MasterEric" + }], "api_version": "0.1.0", "mod_version": "1.0.0", "license": "Apache-2.0" diff --git a/source/Main.hx b/source/Main.hx index fb2a01a69..191ebef53 100644 --- a/source/Main.hx +++ b/source/Main.hx @@ -1,9 +1,9 @@ package; -import funkin.InitState; -import funkin.MemoryCounter; import flixel.FlxGame; import flixel.FlxState; +import funkin.InitState; +import funkin.MemoryCounter; import openfl.Lib; import openfl.display.FPS; import openfl.display.Sprite; @@ -37,17 +37,9 @@ class Main extends Sprite { super(); - // TODO: Ideally this should change to utilize a user interface. - // 1. Call PolymodHandler.getAllMods(). This gives you an array of ModMetadata items, - // each of which contains information about the mod including an icon. - // 2. Provide an interface to enable, disable, and reorder enabled mods. - // A design similar to that of Minecraft resource packs would be intuitive. - // 3. The interface should save (to the save file) and output an ordered array of mod IDs. - // 4. Replace the call to PolymodHandler.loadAllMods() with a call to PolymodHandler.loadModsById(ids:Array). + // TODO: Replace this with loadEnabledMods(). funkin.modding.PolymodHandler.loadAllMods(); - funkin.i18n.FireTongueHandler.init(); - if (stage != null) { init(); diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx index 765686740..331dd46f7 100644 --- a/source/funkin/Conductor.hx +++ b/source/funkin/Conductor.hx @@ -15,9 +15,31 @@ typedef BPMChangeEvent = class Conductor { + /** + * Beats per minute of the song. + */ public static var bpm:Float = 100; - public static var crochet:Float = ((60 / bpm) * 1000); // beats in milliseconds - public static var stepCrochet:Float = crochet / 4; // steps in milliseconds + + /** + * Duration of a beat in millisecond. + */ + public static var crochet(get, null):Float; + + static function get_crochet():Float + { + return ((60 / bpm) * 1000); + } + + /** + * Duration of a step in milliseconds. + */ + public static var stepCrochet(get, null):Float; + + static function get_stepCrochet():Float + { + return crochet / 4; + } + public static var songPosition:Float; public static var lastSongPos:Float; public static var offset:Float = 0; @@ -52,12 +74,4 @@ class Conductor } trace("new BPM map BUDDY " + bpmChangeMap); } - - public static function changeBPM(newBpm:Float) - { - bpm = newBpm; - - crochet = ((60 / bpm) * 1000); - stepCrochet = crochet / 4; - } } diff --git a/source/funkin/GameOverSubstate.hx b/source/funkin/GameOverSubstate.hx index 3c397ee78..40cf9c0ca 100644 --- a/source/funkin/GameOverSubstate.hx +++ b/source/funkin/GameOverSubstate.hx @@ -1,14 +1,11 @@ package funkin; import flixel.FlxObject; -import flixel.FlxSubState; -import flixel.math.FlxPoint; import flixel.system.FlxSound; import flixel.util.FlxColor; import flixel.util.FlxTimer; -import haxe.display.Display; -import funkin.ui.PreferencesMenu; import funkin.play.PlayState; +import funkin.ui.PreferencesMenu; class GameOverSubstate extends MusicBeatSubstate { @@ -57,7 +54,7 @@ class GameOverSubstate extends MusicBeatSubstate add(camFollow); FlxG.sound.play(Paths.sound('fnf_loss_sfx' + stageSuffix)); - // Conductor.changeBPM(100); + // Conductor.bpm = 100; switch (PlayState.currentSong.player1) { diff --git a/source/funkin/LatencyState.hx b/source/funkin/LatencyState.hx index 4e0ca07af..0239bea53 100644 --- a/source/funkin/LatencyState.hx +++ b/source/funkin/LatencyState.hx @@ -31,7 +31,7 @@ class LatencyState extends FlxState strumLine = new FlxSprite(FlxG.width / 2, 100).makeGraphic(FlxG.width, 5); add(strumLine); - Conductor.changeBPM(120); + Conductor.bpm = 120; super.create(); } diff --git a/source/funkin/LoadingState.hx b/source/funkin/LoadingState.hx index b163b49d0..ad649bb03 100644 --- a/source/funkin/LoadingState.hx +++ b/source/funkin/LoadingState.hx @@ -2,9 +2,9 @@ package funkin; import flixel.FlxSprite; import flixel.FlxState; -import flixel.graphics.frames.FlxAtlasFrames; import flixel.math.FlxMath; import flixel.util.FlxTimer; +import funkin.play.PlayState; import haxe.io.Path; import lime.app.Future; import lime.app.Promise; @@ -12,7 +12,6 @@ import lime.utils.AssetLibrary; import lime.utils.AssetManifest; import lime.utils.Assets as LimeAssets; import openfl.utils.Assets; -import funkin.play.PlayState; class LoadingState extends MusicBeatState { @@ -21,7 +20,6 @@ class LoadingState extends MusicBeatState var target:FlxState; var stopMusic = false; var callbacks:MultiCallback; - var danceLeft = false; var loadBar:FlxSprite; @@ -117,17 +115,15 @@ class LoadingState extends MusicBeatState } } - override function beatHit() + override function beatHit():Bool { - super.beatHit(); + // super.beatHit() returns false if a module cancelled the event. + if (!super.beatHit()) + return false; - // logo.animation.play('bump'); danceLeft = !danceLeft; - /* - if (danceLeft) - gfDance.animation.play('danceRight'); - else - gfDance.animation.play('danceLeft'); */ + + return true; } var targetShit:Float = 0; diff --git a/source/funkin/MusicBeatState.hx b/source/funkin/MusicBeatState.hx index 2dce0272b..0fca6fb84 100644 --- a/source/funkin/MusicBeatState.hx +++ b/source/funkin/MusicBeatState.hx @@ -1,13 +1,21 @@ package funkin; -import flixel.util.FlxColor; +import flixel.FlxState; +import flixel.FlxSubState; +import flixel.addons.ui.FlxUIState; import flixel.text.FlxText; +import flixel.util.FlxColor; +import flixel.util.FlxSort; +import funkin.Conductor.BPMChangeEvent; +import funkin.modding.PolymodHandler; import funkin.modding.events.ScriptEvent; import funkin.modding.module.ModuleHandler; -import funkin.modding.events.ScriptEvent.UpdateScriptEvent; -import funkin.Conductor.BPMChangeEvent; -import flixel.addons.ui.FlxUIState; +import funkin.util.SortUtil; +/** + * MusicBeatState actually represents the core utility FlxState of the game. + * It includes functionality for event handling, as well as maintaining BPM-based update events. + */ class MusicBeatState extends FlxUIState { private var curStep:Int = 0; @@ -21,13 +29,23 @@ class MusicBeatState extends FlxUIState public var leftWatermarkText:FlxText = null; public var rightWatermarkText:FlxText = null; + public function new() + { + super(); + + initCallbacks(); + } + + function initCallbacks() + { + subStateOpened.add(onOpenSubstateComplete); + subStateClosed.add(onCloseSubstateComplete); + } + override function create() { super.create(); - if (transIn != null) - trace('reg ' + transIn.region); - createWatermarkText(); } @@ -35,6 +53,10 @@ class MusicBeatState extends FlxUIState { super.update(elapsed); + // This can now be used in EVERY STATE YAY! + if (FlxG.keys.justPressed.F5) + debug_refreshModules(); + // everyStep(); var oldStep:Int = curStep; @@ -44,6 +66,8 @@ class MusicBeatState extends FlxUIState if (oldStep != curStep && curStep >= 0) stepHit(); + FlxG.watch.addQuick("songPos", Conductor.songPosition); + dispatchEvent(new UpdateScriptEvent(elapsed)); } @@ -71,6 +95,14 @@ class MusicBeatState extends FlxUIState ModuleHandler.callEvent(event); } + function debug_refreshModules() + { + PolymodHandler.forceReloadAssets(); + + // Restart the current state, so old data is cleared. + FlxG.resetState(); + } + private function updateBeat():Void { curBeat = Math.floor(curStep / 4); @@ -92,15 +124,97 @@ class MusicBeatState extends FlxUIState curStep = lastChange.stepTime + Math.floor((Conductor.songPosition - lastChange.songTime) / Conductor.stepCrochet); } - public function stepHit():Void + public function stepHit():Bool { + var event = new SongTimeScriptEvent(ScriptEvent.SONG_STEP_HIT, curBeat, curStep); + + dispatchEvent(event); + + if (event.eventCanceled) + { + return false; + } + if (curStep % 4 == 0) beatHit(); + + return true; } - public function beatHit():Void + public function beatHit():Bool { + var event = new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, curBeat, curStep); + + dispatchEvent(event); + + if (event.eventCanceled) + { + return false; + } + lastBeatHitTime = Conductor.songPosition; - // do literally nothing dumbass + + return true; + } + + /** + * Refreshes the state, by redoing the render order of all sprites. + * It does this based on the `zIndex` of each prop. + */ + public function refresh() + { + sort(SortUtil.byZIndex, FlxSort.ASCENDING); + } + + override function switchTo(nextState:FlxState):Bool + { + var event = new StateChangeScriptEvent(ScriptEvent.STATE_CHANGE_BEGIN, nextState, true); + + dispatchEvent(event); + + if (event.eventCanceled) + { + return false; + } + + return super.switchTo(nextState); + } + + public override function openSubState(targetSubstate:FlxSubState):Void + { + var event = new SubStateScriptEvent(ScriptEvent.SUBSTATE_OPEN_BEGIN, targetSubstate, true); + + dispatchEvent(event); + + if (event.eventCanceled) + { + return; + } + + super.openSubState(targetSubstate); + } + + function onOpenSubstateComplete(targetState:FlxSubState):Void + { + dispatchEvent(new SubStateScriptEvent(ScriptEvent.SUBSTATE_OPEN_END, targetState, true)); + } + + public override function closeSubState():Void + { + var event = new SubStateScriptEvent(ScriptEvent.SUBSTATE_CLOSE_BEGIN, this.subState, true); + + dispatchEvent(event); + + if (event.eventCanceled) + { + return; + } + + super.closeSubState(); + } + + function onCloseSubstateComplete(targetState:FlxSubState):Void + { + dispatchEvent(new SubStateScriptEvent(ScriptEvent.SUBSTATE_CLOSE_END, targetState, true)); } } diff --git a/source/funkin/MusicBeatSubstate.hx b/source/funkin/MusicBeatSubstate.hx index 8374848e4..db20bce34 100644 --- a/source/funkin/MusicBeatSubstate.hx +++ b/source/funkin/MusicBeatSubstate.hx @@ -1,8 +1,13 @@ package funkin; -import funkin.Conductor.BPMChangeEvent; import flixel.FlxSubState; +import funkin.Conductor.BPMChangeEvent; +import funkin.modding.events.ScriptEvent; +import funkin.modding.module.ModuleHandler; +/** + * MusicBeatSubstate reincorporates the functionality of MusicBeatState into an FlxSubState. + */ class MusicBeatSubstate extends FlxSubState { public function new() @@ -53,6 +58,11 @@ class MusicBeatSubstate extends FlxSubState beatHit(); } + function dispatchEvent(event:ScriptEvent) + { + ModuleHandler.callEvent(event); + } + public function beatHit():Void { // do literally nothing dumbass diff --git a/source/funkin/TitleState.hx b/source/funkin/TitleState.hx index e5ce1d21c..6dfd5c8b7 100644 --- a/source/funkin/TitleState.hx +++ b/source/funkin/TitleState.hx @@ -4,13 +4,7 @@ import flixel.FlxObject; import flixel.FlxSprite; import flixel.FlxState; import flixel.group.FlxGroup; -import flixel.input.android.FlxAndroidKey; -import flixel.input.android.FlxAndroidKeys; import flixel.input.gamepad.FlxGamepad; -import flixel.input.gamepad.id.SwitchJoyconLeftID; -import flixel.math.FlxPoint; -import flixel.math.FlxRect; -import flixel.text.FlxText; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.util.FlxColor; @@ -19,29 +13,20 @@ import funkin.audiovis.SpectogramSprite; import funkin.shaderslmfao.BuildingShaders; import funkin.shaderslmfao.ColorSwap; import funkin.shaderslmfao.TitleOutline; -import funkin.ui.PreferencesMenu; -import lime.app.Application; -import lime.graphics.Image; -import lime.media.AudioContext; -import lime.ui.Window; +import funkin.ui.AtlasText; +import funkin.util.Constants; import openfl.Assets; import openfl.display.Sprite; import openfl.events.AsyncErrorEvent; -import openfl.events.Event; import openfl.events.MouseEvent; import openfl.events.NetStatusEvent; import openfl.media.Video; -import openfl.net.NetConnection; import openfl.net.NetStream; using StringTools; #if desktop -import sys.FileSystem; -import sys.io.File; -import sys.thread.Thread; #end - class TitleState extends MusicBeatState { public static var initialized:Bool = false; @@ -73,33 +58,33 @@ class TitleState extends MusicBeatState super.create(); /* - #elseif web + #elseif web - if (!initialized) - { + if (!initialized) + { - video = new Video(); - FlxG.stage.addChild(video); + video = new Video(); + FlxG.stage.addChild(video); - var netConnection = new NetConnection(); - netConnection.connect(null); + var netConnection = new NetConnection(); + netConnection.connect(null); - netStream = new NetStream(netConnection); - netStream.client = {onMetaData: client_onMetaData}; - netStream.addEventListener(AsyncErrorEvent.ASYNC_ERROR, netStream_onAsyncError); - netConnection.addEventListener(NetStatusEvent.NET_STATUS, netConnection_onNetStatus); - // netStream.addEventListener(NetStatusEvent.NET_STATUS) // netStream.play(Paths.file('music/kickstarterTrailer.mp4')); + netStream = new NetStream(netConnection); + netStream.client = {onMetaData: client_onMetaData}; + netStream.addEventListener(AsyncErrorEvent.ASYNC_ERROR, netStream_onAsyncError); + netConnection.addEventListener(NetStatusEvent.NET_STATUS, netConnection_onNetStatus); + // netStream.addEventListener(NetStatusEvent.NET_STATUS) // netStream.play(Paths.file('music/kickstarterTrailer.mp4')); - overlay = new Sprite(); - overlay.graphics.beginFill(0, 0.5); - overlay.graphics.drawRect(0, 0, 1280, 720); - overlay.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown); + overlay = new Sprite(); + overlay.graphics.beginFill(0, 0.5); + overlay.graphics.drawRect(0, 0, 1280, 720); + overlay.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown); - overlay.buttonMode = true; - // FlxG.stage.addChild(overlay); + overlay.buttonMode = true; + // FlxG.stage.addChild(overlay); - } + } */ // netConnection.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown); @@ -158,9 +143,9 @@ class TitleState extends MusicBeatState { FlxG.sound.playMusic(Paths.music('freakyMenu'), 0); FlxG.sound.music.fadeIn(4, 0, 0.7); + Conductor.bpm = Constants.FREAKY_MENU_BPM; } - Conductor.changeBPM(102); persistentUpdate = true; var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK); @@ -189,8 +174,6 @@ class TitleState extends MusicBeatState gfDance.antialiasing = true; add(gfDance); - trace('MACRO TEST: ${gfDance.zIndex}'); - // alphaShader.shader.funnyShit.input = gfDance.pixels; // old shit logoBl.shader = alphaShader.shader; @@ -220,6 +203,7 @@ class TitleState extends MusicBeatState blackScreen = bg.clone(); credGroup.add(blackScreen); + credGroup.add(textGroup); // var atlasBullShit:FlxSprite = new FlxSprite(); // atlasBullShit.frames = CoolUtil.fromAnimate(Paths.image('money'), Paths.file('images/money.json')); @@ -290,13 +274,13 @@ class TitleState extends MusicBeatState #end /* if (FlxG.onMobile) + { + if (gfDance != null) { - if (gfDance != null) - { - gfDance.x = (FlxG.width / 2) + (FlxG.accelerometer.x * (FlxG.width / 2)); - // gfDance.y = (FlxG.height / 2) + (FlxG.accelerometer.y * (FlxG.height / 2)); - } + gfDance.x = (FlxG.width / 2) + (FlxG.accelerometer.x * (FlxG.width / 2)); + // gfDance.y = (FlxG.height / 2) + (FlxG.accelerometer.y * (FlxG.height / 2)); } + } */ if (FlxG.keys.justPressed.I) { @@ -313,37 +297,37 @@ class TitleState extends MusicBeatState } /* - FlxG.watch.addQuick('cur display', FlxG.stage.window.display.id); - if (FlxG.keys.justPressed.Y) + FlxG.watch.addQuick('cur display', FlxG.stage.window.display.id); + if (FlxG.keys.justPressed.Y) + { + // trace(FlxG.stage.window.display.name); + + if (FlxG.gamepads.firstActive != null) { - // trace(FlxG.stage.window.display.name); - - if (FlxG.gamepads.firstActive != null) - { - trace(FlxG.gamepads.firstActive.model); - FlxG.gamepads.firstActive.id - } - else - trace('gamepad null'); - - // FlxG.stage.window.title = Std.string(FlxG.random.int(0, 20000)); - // FlxG.stage.window.setIcon(Image.fromFile('assets/images/icon16.png')); - // FlxG.stage.window.readPixels; - - if (FlxG.stage.window.width == Std.int(FlxG.stage.window.display.bounds.width)) - { - FlxG.stage.window.width = 1280; - FlxG.stage.window.height = 720; - FlxG.stage.window.y = 30; - } - else - { - FlxG.stage.window.width = Std.int(FlxG.stage.window.display.bounds.width); - FlxG.stage.window.height = Std.int(FlxG.stage.window.display.bounds.height); - FlxG.stage.window.x = Std.int(FlxG.stage.window.display.bounds.x); - FlxG.stage.window.y = Std.int(FlxG.stage.window.display.bounds.y); - } + trace(FlxG.gamepads.firstActive.model); + FlxG.gamepads.firstActive.id } + else + trace('gamepad null'); + + // FlxG.stage.window.title = Std.string(FlxG.random.int(0, 20000)); + // FlxG.stage.window.setIcon(Image.fromFile('assets/images/icon16.png')); + // FlxG.stage.window.readPixels; + + if (FlxG.stage.window.width == Std.int(FlxG.stage.window.display.bounds.width)) + { + FlxG.stage.window.width = 1280; + FlxG.stage.window.height = 720; + FlxG.stage.window.y = 30; + } + else + { + FlxG.stage.window.width = Std.int(FlxG.stage.window.display.bounds.width); + FlxG.stage.window.height = Std.int(FlxG.stage.window.display.bounds.height); + FlxG.stage.window.x = Std.int(FlxG.stage.window.display.bounds.x); + FlxG.stage.window.y = Std.int(FlxG.stage.window.display.bounds.y); + } + } */ #if debug @@ -382,13 +366,6 @@ class TitleState extends MusicBeatState pressedEnter = true; #end } - - // a faster intro thing lol! - if (pressedEnter && transitioning && skippedIntro) - { - FlxG.switchState(new MainMenuState()); - } - if (pressedEnter && !transitioning && skippedIntro) { if (FlxG.sound.music != null) @@ -434,22 +411,23 @@ class TitleState extends MusicBeatState Assets.cache.clear(Paths.image('logoBumpin')); Assets.cache.clear(Paths.image('titleEnter')); // ngSpr?? + FlxG.switchState(targetState); }); // FlxG.sound.play(Paths.music('titleShoot'), 0.7); } if (pressedEnter && !skippedIntro && initialized) skipIntro(); /* - #if web - if (!initialized && controls.ACCEPT) - { - // netStream.dispose(); - // FlxG.stage.removeChild(video); + #if web + if (!initialized && controls.ACCEPT) + { + // netStream.dispose(); + // FlxG.stage.removeChild(video); - startIntro(); - skipIntro(); - } - #end + startIntro(); + skipIntro(); + } + #end */ if (controls.UI_LEFT) @@ -503,39 +481,47 @@ class TitleState extends MusicBeatState var spec:SpectogramSprite = new SpectogramSprite(FlxG.sound.music); add(spec); - Conductor.changeBPM(190); + Conductor.bpm = 190; FlxG.camera.flash(FlxColor.WHITE, 1); FlxG.sound.play(Paths.sound('confirmMenu'), 0.7); } function createCoolText(textArray:Array) { + if (credGroup == null || textGroup == null) + return; + for (i in 0...textArray.length) { - var money:Alphabet = new Alphabet(0, 0, textArray[i], true, false); + var money:AtlasText = new AtlasText(0, 0, textArray[i], AtlasFont.BOLD); money.screenCenter(X); money.y += (i * 60) + 200; - credGroup.add(money); + // credGroup.add(money); textGroup.add(money); } } function addMoreText(text:String) { + if (credGroup == null || textGroup == null) + return; + lime.ui.Haptic.vibrate(100, 100); - var coolText:Alphabet = new Alphabet(0, 0, text, true, false); + var coolText:AtlasText = new AtlasText(0, 0, text, AtlasFont.BOLD); coolText.screenCenter(X); coolText.y += (textGroup.length * 60) + 200; - credGroup.add(coolText); textGroup.add(coolText); } function deleteCoolText() { + if (credGroup == null || textGroup == null) + return; + while (textGroup.members.length > 0) { - credGroup.remove(textGroup.members[0], true); + // credGroup.remove(textGroup.members[0], true); textGroup.remove(textGroup.members[0], true); } } @@ -543,9 +529,11 @@ class TitleState extends MusicBeatState var isRainbow:Bool = false; var skippedIntro:Bool = false; - override function beatHit() + override function beatHit():Bool { - super.beatHit(); + // super.beatHit() returns false if a module cancelled the event. + if (!super.beatHit()) + return false; if (!skippedIntro) { @@ -605,6 +593,8 @@ class TitleState extends MusicBeatState else gfDance.animation.play('danceLeft'); } + + return true; } function skipIntro():Void diff --git a/source/funkin/charting/ChartingState.hx b/source/funkin/charting/ChartingState.hx index e8aea9da1..eb4b13663 100644 --- a/source/funkin/charting/ChartingState.hx +++ b/source/funkin/charting/ChartingState.hx @@ -1,12 +1,5 @@ package funkin.charting; -import funkin.Conductor.BPMChangeEvent; -import funkin.Note.NoteData; -import funkin.Section.SwagSection; -import funkin.SongLoad.SwagSong; -import funkin.audiovis.ABotVis; -import funkin.audiovis.PolygonSpectogram; -import funkin.audiovis.SpectogramSprite; import flixel.FlxSprite; import flixel.addons.display.FlxGridOverlay; import flixel.addons.transition.FlxTransitionableState; @@ -23,14 +16,21 @@ import flixel.system.FlxSound; import flixel.text.FlxText; import flixel.ui.FlxButton; import flixel.util.FlxColor; +import funkin.Conductor.BPMChangeEvent; +import funkin.Note.NoteData; +import funkin.Section.SwagSection; +import funkin.SongLoad.SwagSong; +import funkin.audiovis.ABotVis; +import funkin.audiovis.PolygonSpectogram; +import funkin.audiovis.SpectogramSprite; +import funkin.play.PlayState; +import funkin.rendering.MeshRender; import haxe.Json; import lime.media.AudioBuffer; import lime.utils.Assets; import openfl.events.Event; import openfl.events.IOErrorEvent; import openfl.net.FileReference; -import funkin.rendering.MeshRender; -import funkin.play.PlayState; using Lambda; using StringTools; @@ -144,7 +144,7 @@ class ChartingState extends MusicBeatState updateGrid(); loadSong(_song.song); - Conductor.changeBPM(_song.bpm); + Conductor.bpm = _song.bpm; Conductor.mapBPMChanges(_song); bpmTxt = new FlxText(1000, 50, 0, "", 16); @@ -545,7 +545,7 @@ class ChartingState extends MusicBeatState { tempBpm = nums.value; Conductor.mapBPMChanges(_song); - Conductor.changeBPM(nums.value); + Conductor.bpm = nums.value; } else if (wname == 'note_susLength') { @@ -975,9 +975,9 @@ class ChartingState extends MusicBeatState _song.bpm = tempBpm; /* if (FlxG.keys.justPressed.UP) - Conductor.changeBPM(Conductor.bpm + 1); + Conductor.bpm = Conductor.bpm + 1; if (FlxG.keys.justPressed.DOWN) - Conductor.changeBPM(Conductor.bpm - 1); */ + Conductor.bpm = Conductor.bpm - 1; */ var shiftThing:Int = 1; if (FlxG.keys.pressed.SHIFT) @@ -1213,7 +1213,7 @@ class ChartingState extends MusicBeatState if (SongLoad.getSong()[curSection].changeBPM && SongLoad.getSong()[curSection].bpm > 0) { - Conductor.changeBPM(SongLoad.getSong()[curSection].bpm); + Conductor.bpm = SongLoad.getSong()[curSection].bpm; FlxG.log.add('CHANGED BPM!'); } else @@ -1223,7 +1223,7 @@ class ChartingState extends MusicBeatState for (i in 0...curSection) if (SongLoad.getSong()[i].changeBPM) daBPM = SongLoad.getSong()[i].bpm; - Conductor.changeBPM(daBPM); + Conductor.bpm = daBPM; } /* // PORT BULLSHIT, INCASE THERE'S NO SUSTAIN DATA FOR A NOTE diff --git a/source/funkin/i18n/FireTongueHandler.hx b/source/funkin/i18n/FireTongueHandler.hx deleted file mode 100644 index fdb13e41c..000000000 --- a/source/funkin/i18n/FireTongueHandler.hx +++ /dev/null @@ -1,114 +0,0 @@ -package funkin.i18n; - -import firetongue.FireTongue; - -class FireTongueHandler -{ - static final DEFAULT_LOCALE = 'en-US'; - // static final DEFAULT_LOCALE = 'pt-BR'; - static final LOCALE_DIR = 'assets/locales/'; - - static var tongue:FireTongue; - - /** - * Initialize the FireTongue instance. - * This will automatically start with the default locale for you. - */ - public static function init():Void - { - tongue = new FireTongue(OPENFL, // Haxe framework being used. - // This should really have been a parameterized object... - null, // Function to check if a file exists. Specify null to use the one from the framework. - null, // Function to retrieve the text of a file. Specify null to use the one from the framework. - null, // Function to get a list of files in a directory. Specify null to use the one from the framework. - firetongue.FireTongue.Case.Upper); - - // TODO: Make this use the language from the user's preferences. - setLanguage(DEFAULT_LOCALE); - - trace('[FIRETONGUE] Initialized. Available locales: ${tongue.locales.join(', ')}'); - } - - /** - * Switch the language used by FireTongue. - * @param locale The name of the locale to use, such as `en-US`. - */ - public static function setLanguage(locale:String):Void - { - tongue.initialize({ - locale: locale, // The locale to load. - - finishedCallback: onFinishLoad, // Function run when the locale is loaded. - directory: LOCALE_DIR, // Folder (relative to assets/) to load data from. - replaceMissing: false, // If true, missing flags fallback to the default locale. - checkMissing: true, // If true, check for and store the list of missing flags for this locale. - }); - } - - /** - * Function called when FireTongue finishes loading a language. - */ - static function onFinishLoad() - { - if (tongue == null) - return; - - trace('[FIRETONGUE] Finished loading locale: ${tongue.locale}'); - if (tongue.missingFlags != null) - { - if (tongue.missingFlags.get(tongue.locale) != null && tongue.missingFlags.get(tongue.locale).length != 0) - { - trace('[FIRETONGUE] Missing flags: ${tongue.missingFlags.get(tongue.locale).join(', ')}'); - } - else - { - trace('[FIRETONGUE] No missing flags for this locale. (Note: Another locale has missing flags.)'); - } - } - else - { - trace('[FIRETONGUE] No missing flags.'); - } - - trace('[FIRETONGUE] HELLO_WORLD = ${t("HELLO_WORLD")}'); - } - - /** - * Retrieve a localized string based on the given key. - * - * Example: - * import i18n.FiretongueHandler.t; - * trace(t('HELLO')); // Prints "Hello!" - * - * @param key The key to use to retrieve the localized string. - * @param context The data file to load the key from. - * @return The localized string. - */ - public static function t(key:String, context:String = 'data'):String - { - // The localization strings can be stored all in one file, - // or split into several contexts. - return tongue.get(key, context); - } - - /** - * Retrieve a localized string while replacing specific values. - * In this way, you can use the same invocation call to properly localize - * a variety of different languages with distinct grammar. - * - * Example: - * import i18n.FiretongueHandler.f; - * trace(f('COLLECT_X_APPLES', 'data', [''], ['10']); // Prints "Collect 10 apples!" - * - * @param key The key to use to retrieve the localized string. - * @param context The data file to load the key from. - * @param flags The flags to replace in the string. - * @param values The values to replace those flags with. - * @return The localized string. - */ - public static function f(key:String, context:String = 'data', flags:Array = null, values:Array = null):String - { - var str = t(key, context); - return firetongue.Replace.flags(str, flags, values); - } -} diff --git a/source/funkin/i18n/README.md b/source/funkin/i18n/README.md deleted file mode 100644 index d55a8f41c..000000000 --- a/source/funkin/i18n/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# i18n - -This package contains functions used for internationalization (i18n). diff --git a/source/funkin/modding/IScriptedClass.hx b/source/funkin/modding/IScriptedClass.hx index 9691c1417..4261032a2 100644 --- a/source/funkin/modding/IScriptedClass.hx +++ b/source/funkin/modding/IScriptedClass.hx @@ -23,6 +23,20 @@ interface IStateChangingScriptedClass extends IScriptedClass { 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; +} + +/** + * Defines a set of callbacks available to scripted classes which represent notes. + */ +interface INoteScriptedClass extends IScriptedClass +{ + public function onNoteHit(event:NoteScriptEvent):Void; + public function onNoteMiss(event:NoteScriptEvent):Void; } /** @@ -40,18 +54,18 @@ interface IStateChangingScriptedClass extends IScriptedClass */ interface IPlayStateScriptedClass extends IScriptedClass { - public function onPause(event:ScriptEvent):Void; + public function onPause(event:PauseScriptEvent):Void; public function onResume(event:ScriptEvent):Void; public function onSongLoaded(eent:SongLoadScriptEvent):Void; public function onSongStart(event:ScriptEvent):Void; public function onSongEnd(event:ScriptEvent):Void; - public function onSongReset(event:ScriptEvent):Void; public function onGameOver(event:ScriptEvent):Void; - public function onGameRetry(event:ScriptEvent):Void; + public function onSongRetry(event:ScriptEvent):Void; public function onNoteHit(event:NoteScriptEvent):Void; public function onNoteMiss(event:NoteScriptEvent):Void; + public function onNoteGhostMiss(event:GhostMissNoteScriptEvent):Void; public function onStepHit(event:SongTimeScriptEvent):Void; public function onBeatHit(event:SongTimeScriptEvent):Void; diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 1f6d149cb..407f3ec85 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -1,10 +1,9 @@ package funkin.modding; -import polymod.Polymod.ModMetadata; +import funkin.modding.module.ModuleHandler; +import funkin.play.stage.StageData; import polymod.Polymod; -import polymod.backends.OpenFLBackend; import polymod.backends.PolymodAssets.PolymodAssetType; -import polymod.format.ParseRules.LinesParseFormat; import polymod.format.ParseRules.TextFileFormat; class PolymodHandler @@ -31,6 +30,15 @@ class PolymodHandler loadModsById(getAllModIds()); } + /** + * Loads the game with configured mods enabled with Polymod. + */ + public static function loadEnabledMods() + { + trace("Initializing Polymod (using configured mods)..."); + loadModsById(getEnabledModIds()); + } + /** * Loads the game without any mods enabled with Polymod. */ @@ -146,8 +154,8 @@ class PolymodHandler { return { assetLibraryPaths: [ - "songs" => "songs", "shared" => "", "tutorial" => "tutorial", "scripts" => "scripts", "week1" => "week1", "week2" => "week2", - "week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "week8" => "week8", + "songs" => "songs", "shared" => "", "tutorial" => "tutorial", "scripts" => "scripts", "week1" => "week1", "week2" => "week2", + "week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "week8" => "week8", ] } } @@ -165,4 +173,61 @@ class PolymodHandler var modIds = [for (i in getAllMods()) i.id]; return modIds; } + + public static function setEnabledMods(newModList:Array):Void + { + FlxG.save.data.enabledMods = newModList; + // Make sure to COMMIT the changes. + FlxG.save.flush(); + } + + /** + * Returns the list of enabled mods. + * @return Array + */ + public static function getEnabledModIds():Array + { + if (FlxG.save.data.enabledMods == null) + { + // NOTE: If the value is null, the enabled mod list is unconfigured. + // Currently, we default to disabling newly installed mods. + // If we want to auto-enable new mods, but otherwise leave the configured list in place, + // we will need some custom logic. + FlxG.save.data.enabledMods = []; + } + return FlxG.save.data.enabledMods; + } + + public static function getEnabledMods():Array + { + var modIds = getEnabledModIds(); + var modMetadata = getAllMods(); + var enabledMods = []; + for (item in modMetadata) + { + if (modIds.indexOf(item.id) != -1) + { + enabledMods.push(item); + } + } + return enabledMods; + } + + public static function forceReloadAssets() + { + // Forcibly clear scripts so that scripts can be edited. + ModuleHandler.clearModuleCache(); + polymod.hscript.PolymodScriptClass.clearScriptClasses(); + + // Forcibly reload Polymod so it finds any new files. + loadEnabledMods(); + + // Reload scripted classes so stages and modules will update. + polymod.hscript.PolymodScriptClass.registerAllScriptClasses(); + + // Reload the stages in cache. + // TODO: Currently this causes lag since you're reading a lot of files, how to fix? + StageDataParser.loadStageCache(); + ModuleHandler.loadModuleCache(); + } } diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx index ce4c08743..ef8fc3a06 100644 --- a/source/funkin/modding/events/ScriptEvent.hx +++ b/source/funkin/modding/events/ScriptEvent.hx @@ -1,5 +1,8 @@ package funkin.modding.events; +import flixel.FlxState; +import flixel.FlxSubState; +import funkin.Note.NoteDir; import funkin.play.Countdown.CountdownStep; import openfl.events.EventType; import openfl.events.KeyboardEvent; @@ -83,6 +86,15 @@ class ScriptEvent */ 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. + * Important information such as direction pressed, etc. are all provided. + * + * 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"; + /** * Called when the song starts. This occurs as the countdown ends and the instrumental and vocals begin. * @@ -97,13 +109,6 @@ class ScriptEvent */ public static inline final SONG_END:ScriptEventType = "SONG_END"; - /** - * Called when the song is reset. This can happen from the pause menu or the game over screen. - * - * This event is not cancelable. - */ - public static inline final SONG_RESET:ScriptEventType = "SONG_RESET"; - /** * Called when the countdown begins. This occurs before the song starts. * @@ -130,18 +135,19 @@ class ScriptEvent public static inline final COUNTDOWN_END:ScriptEventType = "COUNTDOWN_END"; /** - * Called when 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. */ public static inline final GAME_OVER:ScriptEventType = "GAME_OVER"; /** - * Called when the player presses a key to restart the game after the death animation. + * Called when the player presses a key to restart the game. + * This can happen from the pause menu or the game over screen. * * This event IS cancelable! Canceling this event will prevent the game from restarting. */ - public static inline final GAME_RETRY:ScriptEventType = "GAME_RETRY"; + public static inline final SONG_RETRY:ScriptEventType = "SONG_RETRY"; /** * Called when the player pushes down any key on the keyboard. @@ -166,11 +172,46 @@ class ScriptEvent public static inline final SONG_LOADED:ScriptEventType = "SONG_LOADED"; /** - * Called when the game is entering the current FlxState. + * Called when the game is about to switch the current FlxState. * * This event is not cancelable. */ - public static inline final STATE_ENTER:ScriptEventType = "STATE_ENTER"; + 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"; + + /** + * 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"; + + /** + * 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"; + + /** + * 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"; + + /** + * 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"; /** * Called when the game is exiting the current FlxState. @@ -179,7 +220,7 @@ class ScriptEvent */ /** * If true, the behavior associated with this event can be prevented. - * For example, cancelling COUNTDOWN_BEGIN should prevent the countdown from starting, + * For example, cancelling COUNTDOWN_START should prevent the countdown from starting, * until another script restarts it, or cancelling NOTE_HIT should cause the note to be missed. */ public var cancelable(default, null):Bool; @@ -209,7 +250,7 @@ class ScriptEvent /** * Call this function on a cancelable event to cancel the associated behavior. - * For example, cancelling COUNTDOWN_BEGIN will prevent the countdown from starting. + * For example, cancelling COUNTDOWN_START will prevent the countdown from starting. */ public function cancelEvent():Void { @@ -265,6 +306,59 @@ class NoteScriptEvent extends ScriptEvent } } +/** + * An event that is fired when you press a key with no note present. + */ +class GhostMissNoteScriptEvent extends ScriptEvent +{ + /** + * The direction that was mistakenly pressed. + */ + public var dir(default, null):NoteDir; + + /** + * Whether there was a note within judgement range when this ghost note was pressed. + */ + public var hasPossibleNotes(default, null):Bool; + + /** + * How much health should be lost when this ghost note is pressed. + * Remember that max health is 2.00. + */ + public var healthChange(default, default):Float; + + /** + * How much score should be lost when this ghost note is pressed. + */ + public var scoreChange(default, default):Int; + + /** + * Whether to play the record scratch sound. + */ + public var playSound(default, default):Bool; + + /** + * Whether to play the miss animation on the player. + */ + public var playAnim(default, default):Bool; + + public function new(dir:NoteDir, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Int):Void + { + super(ScriptEvent.NOTE_GHOST_MISS, true); + this.dir = dir; + this.hasPossibleNotes = hasPossibleNotes; + this.healthChange = healthChange; + this.scoreChange = scoreChange; + this.playSound = true; + this.playAnim = true; + } + + public override function toString():String + { + return 'GhostMissNoteScriptEvent(dir=' + dir + ', hasPossibleNotes=' + hasPossibleNotes + ')'; + } +} + /** * An event that is fired during the update loop. */ @@ -306,7 +400,7 @@ class SongTimeScriptEvent extends ScriptEvent public function new(type:ScriptEventType, beat:Int, step:Int):Void { - super(type, false); + super(type, true); this.beat = beat; this.step = step; } @@ -403,13 +497,58 @@ class SongLoadScriptEvent extends ScriptEvent */ class StateChangeScriptEvent extends ScriptEvent { - public function new(type:ScriptEventType):Void + /** + * The state the game is moving into. + */ + public var targetState(default, null):FlxState; + + public function new(type:ScriptEventType, targetState:FlxState, cancelable:Bool = false):Void { - super(type, false); + super(type, cancelable); + this.targetState = targetState; } public override function toString():String { - return 'StateChangeScriptEvent(type=' + type + ')'; + return 'StateChangeScriptEvent(type=' + type + ', targetState=' + targetState + ')'; + } +} + +/** + * An event that is fired when moving out of or into an FlxSubState. + */ +class SubStateScriptEvent extends ScriptEvent +{ + /** + * The state the game is moving into. + */ + public var targetState(default, null):FlxSubState; + + public function new(type:ScriptEventType, targetState:FlxSubState, cancelable:Bool = false):Void + { + super(type, cancelable); + this.targetState = targetState; + } + + public override function toString():String + { + return 'SubStateScriptEvent(type=' + type + ', targetState=' + targetState + ')'; + } +} + +/** + * An event which is called when the player attempts to pause the game. + */ +class PauseScriptEvent extends ScriptEvent +{ + /** + * Whether to use the Gitaroo Man pause. + */ + public var gitaroo(default, default):Bool; + + public function new(gitaroo:Bool):Void + { + super(ScriptEvent.PAUSE, true); + this.gitaroo = gitaroo; } } diff --git a/source/funkin/modding/events/ScriptEventDispatcher.hx b/source/funkin/modding/events/ScriptEventDispatcher.hx index ecb97a846..474d1a18c 100644 --- a/source/funkin/modding/events/ScriptEventDispatcher.hx +++ b/source/funkin/modding/events/ScriptEventDispatcher.hx @@ -1,7 +1,7 @@ package funkin.modding.events; -import funkin.modding.IScriptedClass; import funkin.modding.IScriptedClass.IPlayStateScriptedClass; +import funkin.modding.IScriptedClass; /** * Utility functions to assist with handling scripted classes. @@ -35,18 +35,6 @@ class ScriptEventDispatcher return; } - if (Std.isOfType(target, IStateChangingScriptedClass)) - { - var t = cast(target, IStateChangingScriptedClass); - var t = cast(target, IPlayStateScriptedClass); - switch (event.type) - { - case ScriptEvent.NOTE_HIT: - t.onNoteHit(cast event); - return; - } - } - if (Std.isOfType(target, IPlayStateScriptedClass)) { var t = cast(target, IPlayStateScriptedClass); @@ -58,6 +46,9 @@ class ScriptEventDispatcher case ScriptEvent.NOTE_MISS: t.onNoteMiss(cast event); return; + case ScriptEvent.NOTE_GHOST_MISS: + t.onNoteGhostMiss(cast event); + return; case ScriptEvent.SONG_BEAT_HIT: t.onBeatHit(cast event); return; @@ -70,11 +61,14 @@ class ScriptEventDispatcher case ScriptEvent.SONG_END: t.onSongEnd(event); return; - case ScriptEvent.SONG_RESET: - t.onSongReset(event); + case ScriptEvent.SONG_RETRY: + t.onSongRetry(event); + return; + case ScriptEvent.GAME_OVER: + t.onGameOver(event); return; case ScriptEvent.PAUSE: - t.onPause(event); + t.onPause(cast event); return; case ScriptEvent.RESUME: t.onResume(event); @@ -94,7 +88,38 @@ class ScriptEventDispatcher } } - throw "No helper for event type: " + event.type; + if (Std.isOfType(target, IStateChangingScriptedClass)) + { + var t = cast(target, IStateChangingScriptedClass); + switch (event.type) + { + case ScriptEvent.STATE_CHANGE_BEGIN: + t.onStateChangeBegin(cast event); + return; + case ScriptEvent.STATE_CHANGE_END: + t.onStateChangeEnd(cast event); + return; + case ScriptEvent.SUBSTATE_OPEN_BEGIN: + t.onSubstateOpenBegin(cast event); + return; + case ScriptEvent.SUBSTATE_OPEN_END: + t.onSubstateOpenEnd(cast event); + return; + case ScriptEvent.SUBSTATE_CLOSE_BEGIN: + t.onSubstateCloseBegin(cast event); + return; + case ScriptEvent.SUBSTATE_CLOSE_END: + t.onSubstateCloseEnd(cast event); + return; + } + } + else + { + // Prevent "NO HELPER error." + return; + } + + throw "No function called for event type: " + event.type; } public static function callEventOnAllTargets(targets:Iterator, event:ScriptEvent):Void diff --git a/source/funkin/modding/module/Module.hx b/source/funkin/modding/module/Module.hx index 3f499c5e1..d337ad719 100644 --- a/source/funkin/modding/module/Module.hx +++ b/source/funkin/modding/module/Module.hx @@ -1,13 +1,8 @@ package funkin.modding.module; -import funkin.modding.events.ScriptEvent; -import funkin.modding.events.ScriptEvent.UpdateScriptEvent; -import funkin.modding.events.ScriptEvent.KeyboardInputScriptEvent; -import funkin.modding.events.ScriptEvent.NoteScriptEvent; -import funkin.modding.events.ScriptEvent.SongTimeScriptEvent; -import funkin.modding.events.ScriptEvent.CountdownScriptEvent; import funkin.modding.IScriptedClass.IPlayStateScriptedClass; import funkin.modding.IScriptedClass.IStateChangingScriptedClass; +import funkin.modding.events.ScriptEvent; /** * A module is a scripted class which receives all events without requiring a specific context. @@ -18,7 +13,7 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte /** * Whether the module is currently active. */ - public var active(default, set):Bool = false; + public var active(default, set):Bool = true; function set_active(value:Bool):Bool { @@ -48,14 +43,11 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte * Called when the module is initialized. * It may not be safe to reference other modules here since they may not be loaded yet. * - * @param startActive Whether to start with the module active. - * If false, the module will be inactive and must be enabled by another script, - * such as a stage or another module. + * NOTE: To make the module start inactive, call `this.active = false` in the constructor. */ - public function new(moduleId:String, active:Bool = true, priority:Int = 1000):Void + public function new(moduleId:String, priority:Int = 1000):Void { this.moduleId = moduleId; - this.active = active; this.priority = priority; } @@ -82,7 +74,7 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte public function onUpdate(event:UpdateScriptEvent) {} - public function onPause(event:ScriptEvent) {} + public function onPause(event:PauseScriptEvent) {} public function onResume(event:ScriptEvent) {} @@ -90,16 +82,14 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte public function onSongEnd(event:ScriptEvent) {} - public function onSongReset(event:ScriptEvent) {} - public function onGameOver(event:ScriptEvent) {} - public function onGameRetry(event:ScriptEvent) {} - public function onNoteHit(event:NoteScriptEvent) {} public function onNoteMiss(event:NoteScriptEvent) {} + public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {} + public function onStepHit(event:SongTimeScriptEvent) {} public function onBeatHit(event:SongTimeScriptEvent) {} @@ -110,9 +100,19 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte public function onCountdownEnd(event:CountdownScriptEvent) {} - public function onSongLoaded(eent:SongLoadScriptEvent) {} + public function onSongLoaded(event:SongLoadScriptEvent) {} public function onStateChangeBegin(event:StateChangeScriptEvent) {} public function onStateChangeEnd(event:StateChangeScriptEvent) {} + + public function onSubstateOpenBegin(event:SubStateScriptEvent) {} + + public function onSubstateOpenEnd(event:SubStateScriptEvent) {} + + public function onSubstateCloseBegin(event:SubStateScriptEvent) {} + + public function onSubstateCloseEnd(event:SubStateScriptEvent) {} + + public function onSongRetry(event:ScriptEvent) {} } diff --git a/source/funkin/play/AnimationData.hx b/source/funkin/play/AnimationData.hx new file mode 100644 index 000000000..597495345 --- /dev/null +++ b/source/funkin/play/AnimationData.hx @@ -0,0 +1,63 @@ +package funkin.play; + +typedef AnimationData = +{ + /** + * The name for the animation. + * This should match the animation name queried by the game; + * for example, characters need animations with names `idle`, `singDOWN`, `singUPmiss`, etc. + */ + var name:String; + + /** + * The prefix for the frames of the animation as defined by the XML file. + * This will may or may not differ from the `name` of the animation, + * depending on how your animator organized their FLA or whatever. + */ + var prefix:String; + + /** + * Optionally specify an asset path to use for this specific animation. + * ONLY for use by MultiSparrow characters. + * @default The assetPath of the parent sprite + */ + var assetPath:Null; + + /** + * Offset the character's position by this amount when playing this animation. + * @default [0, 0] + */ + var offsets:Null>; + + /** + * Whether the animation should loop when it finishes. + * @default false + */ + var looped:Null; + + /** + * Whether the animation's sprites should be flipped horizontally. + * @default false + */ + var flipX:Null; + + /** + * Whether the animation's sprites should be flipped vertically. + * @default false + */ + var flipY:Null; + + /** + * The frame rate of the animation. + * @default 24 + */ + var frameRate:Null; + + /** + * If you want this animation to use only certain frames of an animation with a given prefix, + * select them here. + * @example [0, 1, 2, 3] (use only the first four frames) + * @default [] (all frames) + */ + var frameIndices:Null>; +} diff --git a/source/funkin/play/PicoFight.hx b/source/funkin/play/PicoFight.hx index 1ef8a1fa2..f37bcb08f 100644 --- a/source/funkin/play/PicoFight.hx +++ b/source/funkin/play/PicoFight.hx @@ -1,14 +1,13 @@ package funkin.play; -import funkin.Note.NoteData; -import funkin.audiovis.PolygonSpectogram; import flixel.FlxObject; import flixel.FlxSprite; import flixel.addons.effects.FlxTrail; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.math.FlxMath; import flixel.util.FlxColor; -import flixel.util.FlxTimer; +import funkin.Note.NoteData; +import funkin.audiovis.PolygonSpectogram; class PicoFight extends MusicBeatState { @@ -37,7 +36,7 @@ class PicoFight extends MusicBeatState FlxG.sound.playMusic(Paths.inst("blazin")); SongLoad.loadFromJson('blazin', "blazin"); - Conductor.changeBPM(SongLoad.songData.bpm); + Conductor.bpm = SongLoad.songData.bpm; for (dumbassSection in SongLoad.songData.noteMap['hard']) { @@ -184,13 +183,15 @@ class PicoFight extends MusicBeatState super.update(elapsed); } - override function stepHit() + override function stepHit():Bool { - super.stepHit(); + return super.stepHit(); } - override function beatHit() + override function beatHit():Bool { + if (!super.beatHit()) + return false; funnyWave.thickness = 10; funnyWave.waveAmplitude = 300; funnyWave.realtimeVisLenght = 0.1; @@ -198,7 +199,6 @@ class PicoFight extends MusicBeatState picoHealth += 1; makeNotes(); - // trace(picoHealth); - super.beatHit(); + return true; } } diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 61ede4246..4cb5f1397 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1,13 +1,12 @@ package funkin.play; -import funkin.play.Strumline.StrumlineArrow; -import flixel.addons.effects.FlxTrail; -import flixel.addons.transition.FlxTransitionableState; import flixel.FlxCamera; import flixel.FlxObject; import flixel.FlxSprite; import flixel.FlxState; import flixel.FlxSubState; +import flixel.addons.effects.FlxTrail; +import flixel.addons.transition.FlxTransitionableState; import flixel.group.FlxGroup; import flixel.math.FlxMath; import flixel.math.FlxPoint; @@ -19,19 +18,17 @@ import flixel.ui.FlxBar; import flixel.util.FlxColor; import flixel.util.FlxSort; import flixel.util.FlxTimer; -import funkin.charting.ChartingState; -import funkin.modding.events.ScriptEvent; -import funkin.modding.events.ScriptEvent.SongTimeScriptEvent; -import funkin.modding.events.ScriptEvent.UpdateScriptEvent; -import funkin.modding.events.ScriptEventDispatcher; -import funkin.modding.IHook; -import funkin.modding.module.ModuleHandler; import funkin.Note; -import funkin.play.stage.Stage; -import funkin.play.stage.StageData; -import funkin.play.Strumline.StrumlineStyle; import funkin.Section.SwagSection; import funkin.SongLoad.SwagSong; +import funkin.charting.ChartingState; +import funkin.modding.IHook; +import funkin.modding.events.ScriptEvent; +import funkin.modding.events.ScriptEventDispatcher; +import funkin.play.Strumline.StrumlineArrow; +import funkin.play.Strumline.StrumlineStyle; +import funkin.play.stage.Stage; +import funkin.play.stage.StageData; import funkin.ui.PopUpStuff; import funkin.ui.PreferencesMenu; import funkin.util.Constants; @@ -287,7 +284,7 @@ class PlayState extends MusicBeatState implements IHook currentSong = SongLoad.loadFromJson('tutorial'); Conductor.mapBPMChanges(currentSong); - Conductor.changeBPM(currentSong.bpm); + Conductor.bpm = currentSong.bpm; switch (currentSong.song.toLowerCase()) { @@ -592,7 +589,7 @@ class PlayState extends MusicBeatState implements IHook * * Call this by pressing F5 on a debug build. */ - function debug_refreshStages() + override function debug_refreshModules() { // Remove the current stage. If the stage gets deleted while it's still in use, // it'll probably crash the game or something. @@ -604,18 +601,7 @@ class PlayState extends MusicBeatState implements IHook currentStage = null; } - ModuleHandler.clearModuleCache(); - - // Forcibly reload scripts so that scripted stages can be edited. - polymod.hscript.PolymodScriptClass.clearScriptClasses(); - polymod.hscript.PolymodScriptClass.registerAllScriptClasses(); - - // Reload the stages in cache. This might cause a lag spike but who cares this is a debug utility. - StageDataParser.loadStageCache(); - ModuleHandler.loadModuleCache(); - - // Reload the level. This should use new data from the assets folder. - LoadingState.loadAndSwitchState(new PlayState()); + super.debug_refreshModules(); } /** @@ -783,7 +769,7 @@ class PlayState extends MusicBeatState implements IHook { // FlxG.log.add(ChartParser.parse()); - Conductor.changeBPM(currentSong.bpm); + Conductor.bpm = currentSong.bpm; currentSong.song = currentSong.song; @@ -849,7 +835,9 @@ class PlayState extends MusicBeatState implements IHook oldNote = null; var swagNote:Note = new Note(daStrumTime, daNoteData, oldNote); - swagNote.data = songNotes; + // swagNote.data = songNotes; + swagNote.data.sustainLength = songNotes.sustainLength; + swagNote.data.altNote = songNotes.altNote; swagNote.scrollFactor.set(0, 0); var susLength:Float = swagNote.data.sustainLength; @@ -938,6 +926,8 @@ class PlayState extends MusicBeatState implements IHook if (needsReset) { + dispatchEvent(new ScriptEvent(ScriptEvent.SONG_RETRY)); + resetCamera(); persistentUpdate = true; @@ -948,11 +938,10 @@ class PlayState extends MusicBeatState implements IHook FlxG.sound.music.pause(); vocals.pause(); - var event:ScriptEvent = new ScriptEvent(ScriptEvent.SONG_RESET, false); - FlxG.sound.music.time = 0; regenNoteData(); // loads the note data from start health = 1; + songScore = 0; Countdown.performCountdown(currentStageId.startsWith('school')); needsReset = false; @@ -1004,28 +993,35 @@ class PlayState extends MusicBeatState implements IHook if ((controls.PAUSE || androidPause) && isInCountdown && mayPauseGame) { - persistentUpdate = false; - persistentDraw = true; + var event = new PauseScriptEvent(FlxG.random.bool(1 / 1000)); - // There is a 1/1000 change to use a special pause menu. - // This prevents the player from resuming, but that's the point. - // It's a reference to Gitaroo Man, which doesn't let you pause the game. - if (FlxG.random.bool(1 / 1000)) - { - FlxG.switchState(new GitarooPause()); - } - else - { - var boyfriendPos = currentStage.getBoyfriend().getScreenPosition(); - var pauseSubState = new PauseSubState(boyfriendPos.x, boyfriendPos.y); - openSubState(pauseSubState); - pauseSubState.camera = camHUD; - boyfriendPos.put(); - } + dispatchEvent(event); - #if discord_rpc - DiscordClient.changePresence(detailsPausedText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC); - #end + if (!event.eventCanceled) + { + persistentUpdate = false; + persistentDraw = true; + + // There is a 1/1000 change to use a special pause menu. + // This prevents the player from resuming, but that's the point. + // It's a reference to Gitaroo Man, which doesn't let you pause the game. + if (event.gitaroo) + { + FlxG.switchState(new GitarooPause()); + } + else + { + var boyfriendPos = currentStage.getBoyfriend().getScreenPosition(); + var pauseSubState = new PauseSubState(boyfriendPos.x, boyfriendPos.y); + openSubState(pauseSubState); + pauseSubState.camera = camHUD; + boyfriendPos.put(); + } + + #if discord_rpc + DiscordClient.changePresence(detailsPausedText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC); + #end + } } if (FlxG.keys.justPressed.SEVEN) @@ -1040,9 +1036,6 @@ class PlayState extends MusicBeatState implements IHook if (FlxG.keys.justPressed.EIGHT) FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState()); - if (FlxG.keys.justPressed.F5) - debug_refreshStages(); - if (FlxG.keys.justPressed.NINE) iconP1.swapOldIcon(); @@ -1656,7 +1649,7 @@ class PlayState extends MusicBeatState implements IHook } } - if (PlayState.instance.currentStage == null) + if (PlayState.instance == null || PlayState.instance.currentStage == null) return; if (PlayState.instance.currentStage.getBoyfriend().holdTimer > Conductor.stepCrochet * 4 * 0.001 && !holdArray.contains(true)) { @@ -1724,9 +1717,12 @@ class PlayState extends MusicBeatState implements IHook } } - override function stepHit() + override function stepHit():Bool { - super.stepHit(); + // super.stepHit() returns false if a module cancelled the event. + if (!super.stepHit()) + return false; + if (Math.abs(FlxG.sound.music.time - (Conductor.songPosition - Conductor.offset)) > 20 || (currentSong.needsVoices && Math.abs(vocals.time - (Conductor.songPosition - Conductor.offset)) > 20)) { @@ -1734,11 +1730,15 @@ class PlayState extends MusicBeatState implements IHook } dispatchEvent(new SongTimeScriptEvent(ScriptEvent.SONG_STEP_HIT, curBeat, curStep)); + + return true; } - override function beatHit() + override function beatHit():Bool { - super.beatHit(); + // super.beatHit() returns false if a module cancelled the event. + if (!super.beatHit()) + return false; if (generatedMusic) { @@ -1749,7 +1749,7 @@ class PlayState extends MusicBeatState implements IHook { if (SongLoad.getSong()[Math.floor(curStep / 16)].changeBPM) { - Conductor.changeBPM(SongLoad.getSong()[Math.floor(curStep / 16)].bpm); + Conductor.bpm = SongLoad.getSong()[Math.floor(curStep / 16)].bpm; FlxG.log.add('CHANGED BPM!'); } } @@ -1780,8 +1780,7 @@ class PlayState extends MusicBeatState implements IHook // Make the characters dance on the beat danceOnBeat(); - // Call any relevant event handlers. - dispatchEvent(new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, curBeat, curStep)); + return true; } /** @@ -1897,13 +1896,6 @@ class PlayState extends MusicBeatState implements IHook Countdown.pauseCountdown(); } - var event:ScriptEvent = new ScriptEvent(ScriptEvent.PAUSE, true); - - dispatchEvent(event); - - if (event.eventCanceled) - return; - super.openSubState(subState); } @@ -1915,6 +1907,13 @@ class PlayState extends MusicBeatState implements IHook { if (isGamePaused) { + var event:ScriptEvent = new ScriptEvent(ScriptEvent.RESUME, true); + + dispatchEvent(event); + + if (event.eventCanceled) + return; + if (FlxG.sound.music != null && !startingSong) resyncVocals(); @@ -1930,13 +1929,6 @@ class PlayState extends MusicBeatState implements IHook #end } - var event:ScriptEvent = new ScriptEvent(ScriptEvent.RESUME, true); - - dispatchEvent(event); - - if (event.eventCanceled) - return; - super.closeSubState(); } @@ -1957,12 +1949,16 @@ class PlayState extends MusicBeatState implements IHook override function dispatchEvent(event:ScriptEvent):Void { + // ORDER: Module, Stage, Character, Song, Note + // Modules should get the first chance to cancel the event. + + // super.dispatchEvent(event) dispatches event to module scripts. + super.dispatchEvent(event); + + // Dispatch event to stage script. ScriptEventDispatcher.callEvent(currentStage, event); // TODO: Dispatch event to song script - // TODO: Dispatch events to character scripts - - super.dispatchEvent(event); } /** @@ -2014,16 +2010,6 @@ class PlayState extends MusicBeatState implements IHook instance = null; } - /** - * Refreshes the state, by redoing the render order of all elements. - * It does this based on the `zIndex` of each element. - */ - public function refresh() - { - sort(SortUtil.byZIndex, FlxSort.ASCENDING); - trace('Stage sorted by z-index'); - } - /** * This function is called whenever Flixel switches switching to a new FlxState. */ diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx index ca1d26ce0..1c8fdce5d 100644 --- a/source/funkin/play/stage/Bopper.hx +++ b/source/funkin/play/stage/Bopper.hx @@ -1,12 +1,8 @@ package funkin.play.stage; -import funkin.modding.events.ScriptEvent; -import funkin.modding.events.ScriptEvent.UpdateScriptEvent; -import funkin.modding.events.ScriptEvent.NoteScriptEvent; -import funkin.modding.events.ScriptEvent.SongTimeScriptEvent; -import funkin.modding.events.ScriptEvent.CountdownScriptEvent; -import funkin.modding.IScriptedClass.IPlayStateScriptedClass; import flixel.FlxSprite; +import funkin.modding.IScriptedClass.IPlayStateScriptedClass; +import funkin.modding.events.ScriptEvent; /** * A Bopper is a stage prop which plays a dance animation. @@ -16,6 +12,7 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass { /** * The bopper plays the dance animation once every `danceEvery` beats. + * Set to 0 to disable idle animation. */ public var danceEvery:Int = 1; @@ -29,16 +26,14 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass public var shouldAlternate:Null = null; /** - * Set this value to define an additional horizontal offset to this sprite's position. + * Offset the character's sprite by this much when playing each animation. */ - public var xOffset:Float = 0; - - override function set_x(value:Float):Float - { - this.x = value + this.xOffset; - return value; - } + public var animationOffsets:Map> = new Map>(); + /** + * Add a suffix to the `idle` animation (or `danceLeft` and `danceRight` animations) + * that this bopper will play. + */ public var idleSuffix(default, set):String = ""; function set_idleSuffix(value:String):String @@ -49,14 +44,26 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass } /** - * Set this value to define an additional vertical offset to this sprite's position. + * The offset of the character relative to the position specified by the stage. */ - public var yOffset:Float = 0; + public var globalOffsets(default, null):Array = [0, 0]; - override function set_y(value:Float):Float + private var animOffsets(default, set):Array = [0, 0]; + + function set_animOffsets(value:Array) { - this.y = value + this.yOffset; - return value; + if (animOffsets == null) + animOffsets = [0, 0]; + if (animOffsets == value) + return value; + + var xDiff = animOffsets[0] - value[0]; + var yDiff = animOffsets[1] - value[1]; + + this.x += xDiff; + this.y += yDiff; + + return animOffsets = value; } /** @@ -73,7 +80,7 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass function update_shouldAlternate():Void { - if (this.animation.getByName('danceLeft') != null) + if (hasAnimation('danceLeft')) { this.shouldAlternate = true; } @@ -84,16 +91,16 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass */ public function onBeatHit(event:SongTimeScriptEvent):Void { - if (event.beat % danceEvery == 0) + if (danceEvery > 0 && event.beat % danceEvery == 0) { - dance(); + dance(true); } } /** * Called every `danceEvery` beats of the song. */ - function dance():Void + public function dance(force:Bool = false):Void { if (this.animation == null) { @@ -109,20 +116,113 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass { if (hasDanced) { - this.animation.play('danceRight$idleSuffix'); + playAnimation('danceRight$idleSuffix', true); } else { - this.animation.play('danceLeft$idleSuffix'); + playAnimation('danceLeft$idleSuffix', true); } hasDanced = !hasDanced; } else { - this.animation.play('idle$idleSuffix'); + playAnimation('idle$idleSuffix', true); } } + public function hasAnimation(id:String):Bool + { + if (this.animation == null) + return false; + + return this.animation.getByName(id) != null; + } + + /** + * Ensure that a given animation exists before playing it. + * Will gracefully check for name, then name with stripped suffixes, then 'idle', then fail to play. + * @param name + */ + function correctAnimationName(name:String) + { + // If the animation exists, we're good. + if (hasAnimation(name)) + return name; + + trace('[BOPPER] Animation "$name" does not exist!'); + + // Attempt to strip a `-alt` suffix, if it exists. + if (name.lastIndexOf('-') != -1) + { + var correctName = name.substring(0, name.lastIndexOf('-')); + trace('[BOPPER] Attempting to fallback to "$correctName"'); + return correctAnimationName(correctName); + } + else + { + if (name != 'idle') + { + trace('[BOPPER] Attempting to fallback to "idle"'); + return correctAnimationName('idle'); + } + else + { + trace('[BOPPER] Failing animation playback.'); + return null; + } + } + } + + /** + * @param name The name of the animation to play. + * @param restart Whether to restart the animation if it is already playing. + */ + public function playAnimation(name:String, restart:Bool = false):Void + { + var correctName = correctAnimationName(name); + if (correctName == null) + return; + + this.animation.play(correctName, restart, false, 0); + + applyAnimationOffsets(correctName); + } + + function applyAnimationOffsets(name:String) + { + var offsets = animationOffsets.get(name); + if (offsets != null) + { + this.animOffsets = [offsets[0] + globalOffsets[0], offsets[1] + globalOffsets[1]]; + } + else + { + this.animOffsets = globalOffsets; + } + } + + public function isAnimationFinished():Bool + { + return this.animation.finished; + } + + public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void + { + animationOffsets.set(name, [xOffset, yOffset]); + } + + /** + * Returns the name of the animation that is currently playing. + * If no animation is playing (usually this means the character is BROKEN!), + * returns an empty string to prevent NPEs. + */ + public function getCurrentAnimation():String + { + if (this.animation == null || this.animation.curAnim == null) + return ""; + return this.animation.curAnim.name; + } + public function onScriptEvent(event:ScriptEvent) {} public function onCreate(event:ScriptEvent) {} @@ -131,7 +231,7 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass public function onUpdate(event:UpdateScriptEvent) {} - public function onPause(event:ScriptEvent) {} + public function onPause(event:PauseScriptEvent) {} public function onResume(event:ScriptEvent) {} @@ -139,16 +239,14 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass public function onSongEnd(event:ScriptEvent) {} - public function onSongReset(event:ScriptEvent) {} - public function onGameOver(event:ScriptEvent) {} - public function onGameRetry(event:ScriptEvent) {} - public function onNoteHit(event:NoteScriptEvent) {} public function onNoteMiss(event:NoteScriptEvent) {} + public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {} + public function onStepHit(event:SongTimeScriptEvent) {} public function onCountdownStart(event:CountdownScriptEvent) {} @@ -158,4 +256,6 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass public function onCountdownEnd(event:CountdownScriptEvent) {} public function onSongLoaded(eent:SongLoadScriptEvent) {} + + public function onSongRetry(event:ScriptEvent) {} } diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index a17f340d4..8d77e374a 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -1,18 +1,16 @@ package funkin.play.stage; -import funkin.modding.events.ScriptEventDispatcher; -import funkin.modding.events.ScriptEvent; -import funkin.modding.events.ScriptEvent.CountdownScriptEvent; -import funkin.modding.events.ScriptEvent.KeyboardInputScriptEvent; -import funkin.modding.IScriptedClass; import flixel.FlxSprite; import flixel.group.FlxSpriteGroup; -import flixel.math.FlxPoint; import flixel.util.FlxSort; import funkin.modding.IHook; +import funkin.modding.IScriptedClass; +import funkin.modding.events.ScriptEvent; +import funkin.modding.events.ScriptEventDispatcher; import funkin.play.character.Character.CharacterType; import funkin.play.stage.StageData.StageDataParser; import funkin.util.SortUtil; +import funkin.util.assets.FlxAnimationUtil; /** * A Stage is a group of objects rendered in the PlayState. @@ -143,19 +141,19 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte for (propAnim in dataProp.animations) { propSprite.animation.add(propAnim.name, propAnim.frameIndices); + + if (Std.isOfType(propSprite, Bopper)) + { + cast(propSprite, Bopper).setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]); + } } default: // "sparrow" - for (propAnim in dataProp.animations) + FlxAnimationUtil.addAtlasAnimations(propSprite, dataProp.animations); + if (Std.isOfType(propSprite, Bopper)) { - if (propAnim.frameIndices.length == 0) + for (propAnim in dataProp.animations) { - propSprite.animation.addByPrefix(propAnim.name, propAnim.prefix, propAnim.frameRate, propAnim.loop, propAnim.flipX, - propAnim.flipY); - } - else - { - propSprite.animation.addByIndices(propAnim.name, propAnim.prefix, propAnim.frameIndices, "", propAnim.frameRate, propAnim.loop, - propAnim.flipX, propAnim.flipY); + cast(propSprite, Bopper).setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]); } } } @@ -377,7 +375,7 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte public function onScriptEvent(event:ScriptEvent) {} - public function onPause(event:ScriptEvent) {} + public function onPause(event:PauseScriptEvent) {} public function onResume(event:ScriptEvent) {} @@ -385,29 +383,23 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte public function onSongEnd(event:ScriptEvent) {} - /** - * Resets the stage and its props. - */ - public function onSongReset(event:ScriptEvent) {} - public function onGameOver(event:ScriptEvent) {} - public function onGameRetry(event:ScriptEvent) {} - public function onCountdownStart(event:CountdownScriptEvent) {} public function onCountdownStep(event:CountdownScriptEvent) {} public function onCountdownEnd(event:CountdownScriptEvent) {} - /** - * A function that should get called every frame. - */ public function onUpdate(event:UpdateScriptEvent) {} public function onNoteHit(event:NoteScriptEvent) {} public function onNoteMiss(event:NoteScriptEvent) {} + public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {} + public function onSongLoaded(eent:SongLoadScriptEvent) {} + + public function onSongRetry(event:ScriptEvent) {} } diff --git a/source/funkin/play/stage/StageData.hx b/source/funkin/play/stage/StageData.hx index 2e9058a87..dc5538bac 100644 --- a/source/funkin/play/stage/StageData.hx +++ b/source/funkin/play/stage/StageData.hx @@ -1,9 +1,10 @@ package funkin.play.stage; -import openfl.Assets; +import flixel.util.typeLimit.OneOfTwo; +import funkin.util.VersionUtil; import funkin.util.assets.DataAssets; import haxe.Json; -import flixel.util.typeLimit.OneOfTwo; +import openfl.Assets; using StringTools; @@ -17,7 +18,12 @@ class StageDataParser * Handle breaking changes by incrementing this value * and adding migration to the `migrateStageData()` function. */ - public static final STAGE_DATA_VERSION:String = "1.0"; + public static final STAGE_DATA_VERSION:String = "1.0.0"; + + /** + * The current version rule check for the stage data format. + */ + public static final STAGE_DATA_VERSION_RULE:String = "1.0.x"; static final stageCache:Map = new Map(); @@ -163,20 +169,21 @@ class StageDataParser } } - static final DEFAULT_NAME:String = "Untitled Stage"; - static final DEFAULT_CAMERAZOOM:Float = 1.0; - static final DEFAULT_ZINDEX:Int = 0; - static final DEFAULT_DANCEEVERY:Int = 0; - static final DEFAULT_SCALE:Float = 1.0; - static final DEFAULT_ISPIXEL:Bool = false; - static final DEFAULT_POSITION:Array = [0, 0]; - static final DEFAULT_SCROLL:Array = [0, 0]; - static final DEFAULT_FRAMEINDICES:Array = []; static final DEFAULT_ANIMTYPE:String = "sparrow"; + static final DEFAULT_CAMERAZOOM:Float = 1.0; + static final DEFAULT_DANCEEVERY:Int = 0; + static final DEFAULT_ISPIXEL:Bool = false; + static final DEFAULT_NAME:String = "Untitled Stage"; + static final DEFAULT_OFFSETS:Array = [0, 0]; + static final DEFAULT_POSITION:Array = [0, 0]; + static final DEFAULT_SCALE:Float = 1.0; + static final DEFAULT_SCROLL:Array = [0, 0]; + static final DEFAULT_ZINDEX:Int = 0; static final DEFAULT_CHARACTER_DATA:StageDataCharacter = { zIndex: DEFAULT_ZINDEX, position: DEFAULT_POSITION, + cameraOffsets: DEFAULT_OFFSETS, } /** @@ -194,12 +201,18 @@ class StageDataParser return null; } - if (input.version != STAGE_DATA_VERSION) + if (input.version == null) { trace('[STAGEDATA] ERROR: Could not load stage data for "$id": missing version'); return null; } + if (!VersionUtil.validateVersion(input.version, STAGE_DATA_VERSION_RULE)) + { + trace('[STAGEDATA] ERROR: Could not load stage data for "$id": bad version (got ${input.version}, expected ${STAGE_DATA_VERSION_RULE})'); + return null; + } + if (input.name == null) { trace('[STAGEDATA] WARN: Stage data for "$id" missing name'); @@ -211,10 +224,9 @@ class StageDataParser input.cameraZoom = DEFAULT_CAMERAZOOM; } - if (input.props == null || input.props.length == 0) + if (input.props == null) { - trace('[STAGEDATA] ERROR: Could not load stage data for "$id": missing props'); - return null; + input.props = []; } for (inputProp in input.props) @@ -296,14 +308,14 @@ class StageDataParser inputAnimation.frameRate = 24; } - if (inputAnimation.frameIndices == null) + if (inputAnimation.offsets == null) { - inputAnimation.frameIndices = DEFAULT_FRAMEINDICES; + inputAnimation.offsets = DEFAULT_OFFSETS; } - if (inputAnimation.loop == null) + if (inputAnimation.looped == null) { - inputAnimation.loop = true; + inputAnimation.looped = true; } if (inputAnimation.flipX == null) @@ -347,6 +359,10 @@ class StageDataParser { inputCharacter.position = [0, 0]; } + if (inputCharacter.cameraOffsets == null || inputCharacter.cameraOffsets.length != 2) + { + inputCharacter.cameraOffsets = [0, 0]; + } } // All good! @@ -356,8 +372,12 @@ class StageDataParser typedef StageData = { - // Uses semantic versioning. + /** + * The sematic version number of the stage data JSON format. + * Supports fancy comparisons like NPM does it's neat. + */ var version:String; + var name:String; var cameraZoom:Null; var props:Array; @@ -432,7 +452,7 @@ typedef StageDataProp = * An optional array of animations which the prop can play. * @default Prop has no animations. */ - var animations:Array; + var animations:Array; /** * If animations are used, this is the name of the animation to play first. @@ -448,52 +468,6 @@ typedef StageDataProp = var animType:String; }; -typedef StageDataPropAnimation = -{ - /** - * The name of the animation. - */ - var name:String; - - /** - * The common beginning of image names in atlas for this animation's frames. - * For example, if the frames are named "test0001.png", "test0002.png", etc., use "test". - */ - var prefix:String; - - /** - * If you want this animation to use only certain frames of an animation with a given prefix, - * select them here. - * @example [0, 1, 2, 3] (use only the first four frames) - * @default [] (all frames) - */ - var frameIndices:Array; - - /** - * The speed of the animation in frames per second. - * @default 24 - */ - var frameRate:Null; - - /** - * Whether the animation should loop. - * @default false - */ - var loop:Null; - - /** - * Whether to flip the sprite horizontally while animating. - * @default false - */ - var flipX:Null; - - /** - * Whether to flip the sprite vertically while animating. - * @default false - */ - var flipY:Null; -}; - typedef StageDataCharacter = { /** @@ -505,5 +479,12 @@ typedef StageDataCharacter = /** * The position to render the character at. - */ position:Array + */ + position:Array, + + /** + * The camera offsets to apply when focusing on the character on this stage. + * @default [0, 0] + */ + cameraOffsets:Array, }; diff --git a/source/funkin/ui/AtlasText.hx b/source/funkin/ui/AtlasText.hx index 6df5f9e22..b07aca240 100644 --- a/source/funkin/ui/AtlasText.hx +++ b/source/funkin/ui/AtlasText.hx @@ -1,21 +1,13 @@ package funkin.ui; import flixel.FlxSprite; -import flixel.group.FlxSpriteGroup; import flixel.graphics.frames.FlxAtlasFrames; +import flixel.group.FlxSpriteGroup; import flixel.util.FlxStringUtil; -@:forward -abstract BoldText(AtlasText) from AtlasText to AtlasText -{ - inline public function new(x = 0.0, y = 0.0, text:String) - { - this = new AtlasText(x, y, text, Bold); - } -} - /** - * Alphabet.hx has a ton of bugs and does a bunch of stuff I don't need, fuck that class + * AtlasText is an improved version of Alphabet and FlxBitmapText. + * It supports animations on the letters, and is less buggy than Alphabet. */ class AtlasText extends FlxTypedSpriteGroup { @@ -41,7 +33,7 @@ class AtlasText extends FlxTypedSpriteGroup inline function get_maxHeight() return font.maxHeight; - public function new(x = 0.0, y = 0.0, text:String, fontName:AtlasFont = Default) + public function new(x = 0.0, y = 0.0, text:String, fontName:AtlasFont = AtlasFont.DEFAULT) { if (!fonts.exists(fontName)) fonts[fontName] = new AtlasFontData(fontName); @@ -246,7 +238,14 @@ private class AtlasFontData public function new(name:AtlasFont) { - atlas = Paths.getSparrowAtlas("fonts/" + name.getName().toLowerCase()); + var fontName:String = name; + atlas = Paths.getSparrowAtlas('fonts/${fontName.toLowerCase()}'); + if (atlas == null) + { + FlxG.log.warn('Could not find font atlas for font "${fontName}".'); + return; + } + atlas.parent.destroyOnNoUse = false; atlas.parent.persist = true; @@ -276,8 +275,8 @@ enum Case Lower; } -enum AtlasFont +enum abstract AtlasFont(String) from String to String { - Default; - Bold; + var DEFAULT = "default"; + var BOLD = "bold"; } diff --git a/source/funkin/ui/ControlsMenu.hx b/source/funkin/ui/ControlsMenu.hx index fb4144b5b..e5e3e44b0 100644 --- a/source/funkin/ui/ControlsMenu.hx +++ b/source/funkin/ui/ControlsMenu.hx @@ -1,6 +1,5 @@ package funkin.ui; -import funkin.Controls; import flixel.FlxCamera; import flixel.FlxObject; import flixel.FlxSprite; @@ -8,6 +7,7 @@ import flixel.group.FlxGroup; import flixel.input.actions.FlxActionInput; import flixel.input.gamepad.FlxGamepadInputID; import flixel.input.keyboard.FlxKey; +import funkin.Controls; import funkin.ui.AtlasText; import funkin.ui.MenuList; import funkin.ui.TextMenuList; @@ -66,11 +66,11 @@ class ControlsMenu extends funkin.ui.OptionsState.Page var item; - item = deviceList.createItem("Keyboard", Bold, selectDevice.bind(Keys)); + item = deviceList.createItem("Keyboard", AtlasFont.BOLD, selectDevice.bind(Keys)); item.x = FlxG.width / 2 - item.width - 30; item.y = (devicesBg.height - item.height) / 2; - item = deviceList.createItem("Gamepad", Bold, selectDevice.bind(Gamepad(FlxG.gamepads.firstActive.id))); + item = deviceList.createItem("Gamepad", AtlasFont.BOLD, selectDevice.bind(Gamepad(FlxG.gamepads.firstActive.id))); item.x = FlxG.width / 2 + 30; item.y = (devicesBg.height - item.height) / 2; } @@ -87,20 +87,20 @@ class ControlsMenu extends funkin.ui.OptionsState.Page if (currentHeader != "UI_" && name.indexOf("UI_") == 0) { currentHeader = "UI_"; - headers.add(new BoldText(0, y, "UI")).screenCenter(X); + headers.add(new AtlasText(0, y, "UI", AtlasFont.BOLD)).screenCenter(X); y += spacer; } else if (currentHeader != "NOTE_" && name.indexOf("NOTE_") == 0) { currentHeader = "NOTE_"; - headers.add(new BoldText(0, y, "NOTES")).screenCenter(X); + headers.add(new AtlasText(0, y, "NOTES", AtlasFont.BOLD)).screenCenter(X); y += spacer; } if (currentHeader != null && name.indexOf(currentHeader) == 0) name = name.substr(currentHeader.length); - var label = labels.add(new BoldText(150, y, name)); + var label = labels.add(new AtlasText(150, y, name, AtlasFont.BOLD)); label.alpha = 0.6; for (i in 0...COLUMNS) createItem(label.x + 400 + i * 300, y, control, i); @@ -317,7 +317,7 @@ class InputItem extends TextMenuItem this.index = index; this.input = getInput(); - super(x, y, getLabel(input), Default, callback); + super(x, y, getLabel(input), DEFAULT, callback); } public function updateDevice(device:Device) diff --git a/source/funkin/ui/OptionsState.hx b/source/funkin/ui/OptionsState.hx index 1b6d03e93..3a74ef3f8 100644 --- a/source/funkin/ui/OptionsState.hx +++ b/source/funkin/ui/OptionsState.hx @@ -5,10 +5,9 @@ import flixel.FlxSubState; import flixel.addons.transition.FlxTransitionableState; import flixel.group.FlxGroup; import flixel.util.FlxSignal; -import funkin.i18n.FireTongueHandler.t; +import funkin.util.Constants; +import funkin.util.WindowUtil; -// typedef OptionsState = OptionsMenu_old; -// class OptionsState_new extends MusicBeatState class OptionsState extends MusicBeatState { var pages = new Map(); @@ -31,17 +30,12 @@ class OptionsState extends MusicBeatState var options = addPage(Options, new OptionsMenu(false)); var preferences = addPage(Preferences, new PreferencesMenu()); var controls = addPage(Controls, new ControlsMenu()); - // var colors = addPage(Colors, new ColorsMenu()); - - var mods = addPage(Mods, new ModMenu()); if (options.hasMultipleOptions()) { options.onExit.add(exitToMainMenu); controls.onExit.add(switchPage.bind(Options)); - // colors.onExit.add(switchPage.bind(Options)); preferences.onExit.add(switchPage.bind(Options)); - mods.onExit.add(switchPage.bind(Options)); } else { @@ -67,12 +61,18 @@ class OptionsState extends MusicBeatState function setPage(name:PageName) { if (pages.exists(currentName)) + { currentPage.exists = false; + currentPage.visible = false; + } currentName = name; if (pages.exists(currentName)) + { currentPage.exists = true; + currentPage.visible = true; + } } override function finishTransIn() @@ -91,7 +91,7 @@ class OptionsState extends MusicBeatState function exitToMainMenu() { currentPage.enabled = false; - // Todo animate? + // TODO: Animate this transition? FlxG.switchState(new MainMenuState()); } } @@ -172,30 +172,29 @@ class OptionsMenu extends Page super(); add(items = new TextMenuList()); - createItem(t("PREFERENCES"), function() switchPage(Preferences)); - createItem(t("CONTROLS"), function() switchPage(Controls)); - // createItem(t("COLORS"), function() switchPage(Colors)); - createItem(t("MODS"), function() switchPage(Mods)); + createItem("PREFERENCES", function() switchPage(Preferences)); + createItem("CONTROLS", function() switchPage(Controls)); + // createItem("COLORS", function() switchPage(Colors)); #if CAN_OPEN_LINKS if (showDonate) { var hasPopupBlocker = #if web true #else false #end; - createItem(t("DONATE"), selectDonate, hasPopupBlocker); + createItem("DONATE", selectDonate, hasPopupBlocker); } #end #if newgrounds if (NGio.isLoggedIn) - createItem(t("LOGOUT"), selectLogout); + createItem("LOGOUT", selectLogout); else - createItem(t("LOGIN"), selectLogin); + createItem("LOGIN", selectLogin); #end - createItem(t("EXIT"), exit); + createItem("EXIT", exit); } function createItem(name:String, callback:Void->Void, fireInstantly = false) { - var item = items.createItem(0, 100 + items.length * 100, name, Bold, callback); + var item = items.createItem(0, 100 + items.length * 100, name, BOLD, callback); item.fireInstantly = fireInstantly; item.screenCenter(X); return item; @@ -219,11 +218,7 @@ class OptionsMenu extends Page #if CAN_OPEN_LINKS function selectDonate() { - #if linux - Sys.command('/usr/bin/xdg-open', ["https://ninja-muffin24.itch.io/funkin", "&"]); - #else - FlxG.openURL('https://ninja-muffin24.itch.io/funkin'); - #end + WindowUtil.openURL(Constants.URL_ITCH); } #end diff --git a/source/funkin/ui/PreferencesMenu.hx b/source/funkin/ui/PreferencesMenu.hx index 6fcf7b57a..94158bcce 100644 --- a/source/funkin/ui/PreferencesMenu.hx +++ b/source/funkin/ui/PreferencesMenu.hx @@ -4,8 +4,8 @@ import flixel.FlxCamera; import flixel.FlxObject; import flixel.FlxSprite; import funkin.ui.AtlasText.AtlasFont; -import funkin.ui.TextMenuList.TextMenuItem; import funkin.ui.OptionsState.Page; +import funkin.ui.TextMenuList.TextMenuItem; class PreferencesMenu extends Page { @@ -84,7 +84,7 @@ class PreferencesMenu extends Page private function createPrefItem(prefName:String, prefString:String, prefValue:Dynamic):Void { - items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.Bold, function() + items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() { preferenceCheck(prefString, prefValue); @@ -157,16 +157,17 @@ class PreferencesMenu extends Page }); } - private static function preferenceCheck(prefString:String, prefValue:Dynamic):Void + private static function preferenceCheck(prefString:String, defaultValue:Dynamic):Void { if (preferences.get(prefString) == null) { - preferences.set(prefString, prefValue); - trace('set preference!'); + // Set the value to default. + preferences.set(prefString, defaultValue); + trace('Set preference to default: ${prefString} = ${defaultValue}'); } else { - trace('found preference: ' + preferences.get(prefString)); + trace('Found preference: ${prefString} = ${preferences.get(prefString)}'); } } } diff --git a/source/funkin/ui/Prompt.hx b/source/funkin/ui/Prompt.hx index 1679d3886..76d85744b 100644 --- a/source/funkin/ui/Prompt.hx +++ b/source/funkin/ui/Prompt.hx @@ -1,12 +1,12 @@ package funkin.ui; import flixel.FlxSprite; -import flixel.graphics.frames.FlxAtlasFrames; -import flixel.text.FlxText; -import flixel.util.FlxColor; -import funkin.ui.AtlasText; +import funkin.ui.AtlasText.AtlasFont; import funkin.ui.MenuList; +/** + * Opens a yes/no dialog box as a substate over the current state. + */ class Prompt extends flixel.FlxSubState { inline static var MARGIN = 100; @@ -26,7 +26,7 @@ class Prompt extends flixel.FlxSubState buttons = new TextMenuList(Horizontal); - field = new BoldText(text); + field = new AtlasText(text, AtlasFont.BOLD); field.scrollFactor.set(0, 0); } diff --git a/source/funkin/ui/TextMenuList.hx b/source/funkin/ui/TextMenuList.hx index ae3ed7bff..fe1a13a7c 100644 --- a/source/funkin/ui/TextMenuList.hx +++ b/source/funkin/ui/TextMenuList.hx @@ -10,7 +10,7 @@ class TextMenuList extends MenuTypedList super(navControls, wrapMode); } - public function createItem(x = 0.0, y = 0.0, name:String, font:AtlasFont = Bold, callback, fireInstantly = false) + public function createItem(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, callback, fireInstantly = false) { var item = new TextMenuItem(x, y, name, font, callback); item.fireInstantly = fireInstantly; @@ -20,7 +20,7 @@ class TextMenuList extends MenuTypedList class TextMenuItem extends TextTypedMenuItem { - public function new(x = 0.0, y = 0.0, name:String, font:AtlasFont = Bold, callback) + public function new(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, callback) { super(x, y, new AtlasText(0, 0, name, font), name, callback); setEmptyBackground(); diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index fb121e144..a93578c20 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -1,7 +1,7 @@ package funkin.util; -import lime.app.Application; import flixel.util.FlxColor; +import lime.app.Application; class Constants { @@ -18,6 +18,8 @@ class Constants public static final VERSION_SUFFIX = ' PROTOTYPE'; public static var VERSION(get, null):String; + public static final FREAKY_MENU_BPM = 102; + #if debug public static final GIT_HASH = funkin.util.macro.GitCommit.getGitCommitHash(); @@ -31,4 +33,7 @@ class Constants return 'v${Application.current.meta.get('version')}' + VERSION_SUFFIX; } #end + + public static final URL_KICKSTARTER:String = "https://www.kickstarter.com/projects/funkin/friday-night-funkin-the-full-ass-game/"; + public static final URL_ITCH:String = "https://ninja-muffin24.itch.io/funkin"; } diff --git a/source/funkin/util/VersionUtil.hx b/source/funkin/util/VersionUtil.hx new file mode 100644 index 000000000..57e1f80ed --- /dev/null +++ b/source/funkin/util/VersionUtil.hx @@ -0,0 +1,31 @@ +package funkin.util; + +import thx.semver.Version; +import thx.semver.VersionRule; + +/** + * Remember, increment the patch version (1.0.x) if you make a bugfix, + * increment the minor version (1.x.0) if you make a new feature (but previous content is still compatible), + * and increment the major version (x.0.0) if you make a breaking change (e.g. new API or reorganized file format). + */ +class VersionUtil +{ + /** + * Checks that a given verison number satisisfies a given version rule. + * Version rule can be complex, e.g. "1.0.x" or ">=1.0.0,<1.1.0", or anything NPM supports. + */ + public static function validateVersion(version:String, versionRule:String):Bool + { + try + { + var v:Version = version; // Perform a cast. + var vr:VersionRule = versionRule; // Perform a cast. + return v.satisfies(vr); + } + catch (e) + { + trace('[VERSIONUTIL] Invalid semantic version: ${version}'); + return false; + } + } +} diff --git a/source/funkin/util/WindowUtil.hx b/source/funkin/util/WindowUtil.hx new file mode 100644 index 000000000..fe27ed252 --- /dev/null +++ b/source/funkin/util/WindowUtil.hx @@ -0,0 +1,18 @@ +package funkin.util; + +class WindowUtil +{ + public static function openURL(targetUrl:String) + { + #if CAN_OPEN_LINKS + #if linux + // Sys.command('/usr/bin/xdg-open', [, "&"]); + Sys.command('/usr/bin/xdg-open', [targetUrl, "&"]); + #else + FlxG.openURL(targetUrl); + #end + #else + trace('Cannot open') + #end + } +} diff --git a/source/funkin/util/assets/FlxAnimationUtil.hx b/source/funkin/util/assets/FlxAnimationUtil.hx new file mode 100644 index 000000000..d40dfa73c --- /dev/null +++ b/source/funkin/util/assets/FlxAnimationUtil.hx @@ -0,0 +1,42 @@ +package funkin.util.assets; + +import flixel.FlxSprite; +import funkin.play.AnimationData; + +class FlxAnimationUtil +{ + /** + * Properly adds an animation to a sprite based on JSON data. + */ + public static function addAtlasAnimation(target:FlxSprite, anim:AnimationData) + { + var frameRate = anim.frameRate == null ? 24 : anim.frameRate; + var looped = anim.looped == null ? false : anim.looped; + var flipX = anim.flipX == null ? false : anim.flipX; + var flipY = anim.flipY == null ? false : anim.flipY; + + if (anim.frameIndices != null && anim.frameIndices.length > 0) + { + // trace('addByIndices(${anim.name}, ${anim.prefix}, ${anim.frameIndices}, ${frameRate}, ${looped}, ${flipX}, ${flipY})'); + target.animation.addByIndices(anim.name, anim.prefix, anim.frameIndices, "", frameRate, looped, flipX, flipY); + // trace('RESULT:${target.animation.getAnimationList()}'); + } + else + { + // trace('addByPrefix(${anim.name}, ${anim.prefix}, ${frameRate}, ${looped}, ${flipX}, ${flipY})'); + target.animation.addByPrefix(anim.name, anim.prefix, frameRate, looped, flipX, flipY); + // trace('RESULT:${target.animation.getAnimationList()}'); + } + } + + /** + * Properly adds multiple animations to a sprite based on JSON data. + */ + public static function addAtlasAnimations(target:FlxSprite, animations:Array) + { + for (anim in animations) + { + addAtlasAnimation(target, anim); + } + } +}