From 0a5419d7fc7725f27a6e1802cc34298983f6d455 Mon Sep 17 00:00:00 2001 From: Kolo <67389779+JustKolosaki@users.noreply.github.com> Date: Sun, 29 Sep 2024 19:07:09 +0200 Subject: [PATCH] the stage editor shit --- source/funkin/data/stage/StageData.hx | 30 + source/funkin/play/stage/Stage.hx | 4 + source/funkin/save/Save.hx | 135 ++ source/funkin/ui/debug/DebugMenuSubState.hx | 3 +- .../ui/debug/stageeditor/StageEditorObject.hx | 119 ++ .../ui/debug/stageeditor/StageEditorState.hx | 1486 +++++++++++++++++ .../stageeditor/components/AboutDialog.hx | 6 + .../components/BackupAvailableDialog.hx | 80 + .../components/ExitConfirmDialog.hx | 31 + .../stageeditor/components/FindObjDialog.hx | 105 ++ .../components/LoadFromUrlDialog.hx | 70 + .../stageeditor/components/NewObjDialog.hx | 90 + .../stageeditor/components/UserGuideDialog.hx | 6 + .../stageeditor/components/WelcomeDialog.hx | 146 ++ .../stageeditor/handlers/AssetDataHandler.hx | 208 +++ .../stageeditor/handlers/StageDataHandler.hx | 365 ++++ .../stageeditor/handlers/UndoRedoHandler.hx | 191 +++ source/funkin/ui/debug/stageeditor/import.hx | 7 + .../toolboxes/StageEditorCharacterToolbox.hx | 242 +++ .../toolboxes/StageEditorDefaultToolbox.hx | 44 + .../toolboxes/StageEditorObjectToolbox.hx | 581 +++++++ .../toolboxes/StageEditorStageToolbox.hx | 60 + source/funkin/ui/transition/LoadingState.hx | 12 +- source/funkin/util/FileUtil.hx | 7 + 24 files changed, 4025 insertions(+), 3 deletions(-) create mode 100644 source/funkin/ui/debug/stageeditor/StageEditorObject.hx create mode 100644 source/funkin/ui/debug/stageeditor/StageEditorState.hx create mode 100644 source/funkin/ui/debug/stageeditor/components/AboutDialog.hx create mode 100644 source/funkin/ui/debug/stageeditor/components/BackupAvailableDialog.hx create mode 100644 source/funkin/ui/debug/stageeditor/components/ExitConfirmDialog.hx create mode 100644 source/funkin/ui/debug/stageeditor/components/FindObjDialog.hx create mode 100644 source/funkin/ui/debug/stageeditor/components/LoadFromUrlDialog.hx create mode 100644 source/funkin/ui/debug/stageeditor/components/NewObjDialog.hx create mode 100644 source/funkin/ui/debug/stageeditor/components/UserGuideDialog.hx create mode 100644 source/funkin/ui/debug/stageeditor/components/WelcomeDialog.hx create mode 100644 source/funkin/ui/debug/stageeditor/handlers/AssetDataHandler.hx create mode 100644 source/funkin/ui/debug/stageeditor/handlers/StageDataHandler.hx create mode 100644 source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx create mode 100644 source/funkin/ui/debug/stageeditor/import.hx create mode 100644 source/funkin/ui/debug/stageeditor/toolboxes/StageEditorCharacterToolbox.hx create mode 100644 source/funkin/ui/debug/stageeditor/toolboxes/StageEditorDefaultToolbox.hx create mode 100644 source/funkin/ui/debug/stageeditor/toolboxes/StageEditorObjectToolbox.hx create mode 100644 source/funkin/ui/debug/stageeditor/toolboxes/StageEditorStageToolbox.hx diff --git a/source/funkin/data/stage/StageData.hx b/source/funkin/data/stage/StageData.hx index 1e9172b00..5ac22ca90 100644 --- a/source/funkin/data/stage/StageData.hx +++ b/source/funkin/data/stage/StageData.hx @@ -20,6 +20,10 @@ class StageData @:optional public var cameraZoom:Null; + @:default("shared") + @:optional + public var directory:Null; + public function new() { this.version = StageRegistry.STAGE_DATA_VERSION; @@ -198,6 +202,32 @@ typedef StageDataProp = @:default("sparrow") @:optional var animType:String; + + /** + * The angle of the prop, as a float. + * @default 1.0 + */ + @:optional + @:default(0.0) + var angle:Float; + + /** + * The blend mode of the prop, as a string. + * Just like in photoshop. + * @default Nothing. + */ + @:default("") + @:optional + var blend:String; + + /** + * The color of the prop overlay, as a hex string. + * White overlays, or the ones with the value #FFFFFF, do not appear. + * @default `#FFFFFF` + */ + @:default("#FFFFFF") + @:optional + var color:String; }; typedef StageDataCharacter = diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index 62295a717..88e19b191 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -256,6 +256,10 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements propSprite.scrollFactor.x = dataProp.scroll[0]; propSprite.scrollFactor.y = dataProp.scroll[1]; + propSprite.angle = dataProp.angle; + propSprite.color = FlxColor.fromString(dataProp.color); + @:privateAccess if (!isSolidColor) propSprite.blend = BlendMode.fromString(dataProp.blend); + propSprite.zIndex = dataProp.zIndex; propSprite.flipX = dataProp.flipX; diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 47467cd45..8403a1a77 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -10,6 +10,7 @@ import funkin.save.migrator.SaveDataMigrator; import funkin.save.migrator.SaveDataMigrator; import funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle; import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme; +import funkin.ui.debug.stageeditor.StageEditorState.StageEditorTheme; import funkin.util.SerializerUtil; import thx.semver.Version; import thx.semver.Version; @@ -146,6 +147,14 @@ class Save hitsoundVolumeOpponent: 1.0, themeMusic: true }, + + optionsStageEditor: + { + previousFiles: [], + moveStep: "1px", + angleStep: 5, + theme: StageEditorTheme.Light + } }; } @@ -428,6 +437,91 @@ class Save return data.unlocks.oldChar; } + public var stageEditorPreviousFiles(get, set):Array; + + function get_stageEditorPreviousFiles():Array + { + if (data.optionsStageEditor.previousFiles == null) data.optionsStageEditor.previousFiles = []; + + return data.optionsStageEditor.previousFiles; + } + + function set_stageEditorPreviousFiles(value:Array):Array + { + // Set and apply. + data.optionsStageEditor.previousFiles = value; + flush(); + return data.optionsStageEditor.previousFiles; + } + + public var stageEditorHasBackup(get, set):Bool; + + function get_stageEditorHasBackup():Bool + { + if (data.optionsStageEditor.hasBackup == null) data.optionsStageEditor.hasBackup = false; + + return data.optionsStageEditor.hasBackup; + } + + function set_stageEditorHasBackup(value:Bool):Bool + { + // Set and apply. + data.optionsStageEditor.hasBackup = value; + flush(); + return data.optionsStageEditor.hasBackup; + } + + public var stageEditorMoveStep(get, set):String; + + function get_stageEditorMoveStep():String + { + if (data.optionsStageEditor.moveStep == null) data.optionsStageEditor.moveStep = "1px"; + + return data.optionsStageEditor.moveStep; + } + + function set_stageEditorMoveStep(value:String):String + { + // Set and apply. + data.optionsStageEditor.moveStep = value; + flush(); + return data.optionsStageEditor.moveStep; + } + + public var stageEditorAngleStep(get, set):Float; + + function get_stageEditorAngleStep():Float + { + if (data.optionsStageEditor.angleStep == null) data.optionsStageEditor.angleStep = 5; + + return data.optionsStageEditor.angleStep; + } + + function set_stageEditorAngleStep(value:Float):Float + { + // Set and apply. + data.optionsStageEditor.angleStep = value; + flush(); + return data.optionsStageEditor.angleStep; + } + + public var stageEditorTheme(get, set):StageEditorTheme; + + function get_stageEditorTheme():StageEditorTheme + { + if (data.optionsStageEditor.theme == null) data.optionsStageEditor.theme = StageEditorTheme.Light; + + return data.optionsStageEditor.theme; + } + + function set_stageEditorTheme(value:StageEditorTheme):StageEditorTheme + { + // Set and apply. + data.optionsStageEditor.theme = value; + flush(); + return data.optionsStageEditor.theme; + } + /** * When we've seen a character unlock, add it to the list of characters seen. * @param character @@ -1068,6 +1162,11 @@ typedef RawSaveData = * The user's preferences specific to the Chart Editor. */ var optionsChartEditor:SaveDataChartEditorOptions; + + /** + * The user's preferences specific to the Stage Editor. + */ + var optionsStageEditor:SaveDataStageEditorOptions; }; typedef SaveApiData = @@ -1441,3 +1540,39 @@ typedef SaveDataChartEditorOptions = */ var ?playbackSpeed:Float; }; + +typedef SaveDataStageEditorOptions = +{ + // a lot of these things were copied from savedatacharteditoroptions + + /** + * Whether the Stage Editor created a backup the last time it closed. + * Prompt the user to load it, then set this back to `false`. + * @default `false` + */ + var ?hasBackup:Bool; + + /** + * Previous files opened in the Stage Editor. + * @default `[]` + */ + var ?previousFiles:Array; + + /** + * The Step at which an Object or Character is moved. + * @default `1px` + */ + var ?moveStep:String; + + /** + * The Step at which an Object is rotated. + * @default `5` + */ + var ?angleStep:Float; + + /** + * Theme in the Stage Editor. + * @default `StageEditorTheme.Light` + */ + var ?theme:StageEditorTheme; +}; diff --git a/source/funkin/ui/debug/DebugMenuSubState.hx b/source/funkin/ui/debug/DebugMenuSubState.hx index fc5f3aa37..cc6a2426e 100644 --- a/source/funkin/ui/debug/DebugMenuSubState.hx +++ b/source/funkin/ui/debug/DebugMenuSubState.hx @@ -60,7 +60,7 @@ class DebugMenuSubState extends MusicBeatSubState // createItem("Input Offset Testing", openInputOffsetTesting); createItem("CHARACTER SELECT", openCharSelect, true); createItem("ANIMATION EDITOR", openAnimationEditor); - // createItem("STAGE EDITOR", openStageEditor); + createItem("STAGE EDITOR", openStageEditor); // createItem("TEST STICKERS", testStickers); #if sys createItem("OPEN CRASH LOG FOLDER", openLogFolder); @@ -125,6 +125,7 @@ class DebugMenuSubState extends MusicBeatSubState function openStageEditor() { trace('Stage Editor'); + FlxG.switchState(() -> new funkin.ui.debug.stageeditor.StageEditorState()); } #if sys diff --git a/source/funkin/ui/debug/stageeditor/StageEditorObject.hx b/source/funkin/ui/debug/stageeditor/StageEditorObject.hx new file mode 100644 index 000000000..6fa7db6be --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/StageEditorObject.hx @@ -0,0 +1,119 @@ +package funkin.ui.debug.stageeditor; + +import funkin.data.animation.AnimationData; +import funkin.graphics.FunkinSprite; +import funkin.modding.events.ScriptEvent; + +/** + * Contains all the Logic needed for Stage Editor. Only for Stage Editor, as in the gameplay StageProps and Boppers will be used. + */ +class StageEditorObject extends FunkinSprite +{ + /** + * The internal Name of the Object. + */ + public var name:String = "Unnamed"; + + /** + * What animation to play upon starting. + */ + public var startingAnimation:String = ""; + + public var animDatas:Map = []; + + override public function new() + { + super(); + } + + /** + * Whether the Object is currently being modified in the Stage Editor. + */ + public var isDebugged(default, set):Bool = true; + + function set_isDebugged(value:Bool) + { + this.isDebugged = value; + + if (value == false) // plays upon starting yippee!!! + playAnim(startingAnimation, true); + else + { + if (animation.curAnim != null) + { + animation.stop(); + offset.set(); + updateHitbox(); + } + } + + return value; + } + + public function playAnim(name:String, restart:Bool = false, reversed:Bool = false) + { + if (!animation.getNameList().contains(name)) return; + + animation.play(name, restart, reversed, 0); + + if (animDatas.exists(name)) offset.set(animDatas[name].offsets[0], animDatas[name].offsets[1]); + else + offset.set(); + } + + /** + * On which beat should it dance? + */ + public var danceEvery:Float = 0; + + /** + * Internal, handles danceLeft and danceRight. + */ + var _danced:Bool = true; + + public function dance(restart:Bool = false) + { + if (isDebugged) return; + + var idle = animation.getNameList().contains("idle"); + var dancing = animation.getNameList().contains("danceLeft") && animation.getNameList().contains("danceRight"); + + if (!idle && !dancing) return; + + if (dancing) + { + if (_danced) playAnim("danceRight", restart); + else + playAnim("danceLeft", restart); + + _danced = !_danced; + } + else if (idle) + { + playAnim("idle", restart); + } + } + + public function addAnim(name:String, prefix:String, offsets:Array, indices:Array, frameRate:Int = 24, looped:Bool = true, flipX:Bool = false, + flipY:Bool = false) + { + if (indices.length > 0) animation.addByIndices(name, prefix, indices, "", frameRate, looped, flipX, flipY); + else + animation.addByPrefix(name, prefix, frameRate, looped, flipX, flipY); + + if (animation.getNameList().contains(name)) // sometimes the animation doesnt add + { + animDatas.set(name, + { + name: name, + prefix: prefix, + offsets: offsets, + looped: looped, + frameRate: frameRate, + flipX: flipX, + flipY: flipY, + frameIndices: indices + }); + } + } +} diff --git a/source/funkin/ui/debug/stageeditor/StageEditorState.hx b/source/funkin/ui/debug/stageeditor/StageEditorState.hx new file mode 100644 index 000000000..e6d2e1f8a --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/StageEditorState.hx @@ -0,0 +1,1486 @@ +package funkin.ui.debug.stageeditor; + +import flixel.math.FlxPoint; +import flixel.text.FlxText; +import openfl.display.BitmapData; +import flixel.util.FlxTimer; +import flixel.FlxCamera; +import flixel.addons.display.shapes.FlxShapeCircle; +import flixel.graphics.FlxGraphic; +import flixel.FlxSprite; +import flixel.util.FlxColor; +import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.addons.display.FlxGridOverlay; +import funkin.play.character.BaseCharacter; +import funkin.play.character.BaseCharacter.CharacterType; +import funkin.play.character.CharacterData.CharacterDataParser; +import funkin.save.Save; +import funkin.input.Cursor; +import haxe.ui.backend.flixel.UIState; +import haxe.ui.containers.menus.MenuItem; +import haxe.ui.containers.menus.Menu; +import haxe.ui.containers.menus.MenuSeparator; +import haxe.ui.containers.menus.MenuBar; +import haxe.ui.containers.menus.MenuOptionBox; +import haxe.ui.containers.menus.MenuCheckBox; +import funkin.util.FileUtil; +import funkin.ui.debug.stageeditor.handlers.AssetDataHandler; +import funkin.ui.debug.stageeditor.handlers.AssetDataHandler.StageEditorObjectData; +import funkin.ui.debug.stageeditor.handlers.StageDataHandler; +import funkin.ui.debug.stageeditor.handlers.UndoRedoHandler.UndoAction; +import funkin.ui.debug.stageeditor.toolboxes.*; +import funkin.ui.debug.stageeditor.components.*; +import haxe.ui.containers.dialogs.Dialogs; +import haxe.ui.containers.dialogs.Dialog; +import haxe.ui.containers.dialogs.MessageBox.MessageBoxType; +import haxe.ui.containers.Box; +import haxe.ui.containers.VBox; // vbucks +import haxe.ui.components.Button; +import haxe.ui.containers.windows.WindowList; +import haxe.ui.containers.windows.WindowManager; +import haxe.ui.containers.windows.Window; +import flixel.FlxObject; +import haxe.ui.components.Label; +import funkin.ui.mainmenu.MainMenuState; +import flixel.system.debug.interaction.tools.Pointer.GraphicCursorCross; +import haxe.ui.focus.FocusManager; +import haxe.ui.core.Screen; +import funkin.util.WindowUtil; +import funkin.audio.FunkinSound; +import haxe.ui.notifications.NotificationType; +import haxe.ui.notifications.NotificationManager; +import funkin.util.logging.CrashHandler; + +/** + * Da Stage Editor woo!! + * made by Kolo NEVER FORGET + */ +@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/stage-editor/main-view.xml")) +class StageEditorState extends UIState +{ + // i aint documenting allat + // the uh finals + public static final BACKUPS_PATH:String = "./stagebackups/"; + public static final LIGHT_MODE_COLORS:Array = [0xFFE7E6E6, 0xFFF8F8F8]; + public static final DARK_MODE_COLORS:Array = [0xFF181919, 0xFF202020]; + + public static final DEFAULT_POSITIONS:Map> = [ + CharacterType.BF => [989.5, 885], + CharacterType.GF => [751.5, 787], + CharacterType.DAD => [335, 885] + ]; + + public static final DEFAULT_CAMERA_OFFSETS:Map> = [ + CharacterType.BF => [-100, -100], + CharacterType.GF => [0, 0], + CharacterType.DAD => [150, -100] + ]; + + public static final MAX_Z_INDEX:Int = 10000; + public static final CHARACTER_COLORS:Array = [FlxColor.RED, FlxColor.PURPLE, FlxColor.CYAN]; // FCUK IVE TURNED INTO AN AMERICAN + public static final TIME_BEFORE_ANIM_STOP:Float = 3.0; + + public static var instance:StageEditorState = null; // unused lol + + // the other shit:tm: + var menubar:MenuBar; + + var menubarMenuFile:Menu; + var menubarItemNewStage:MenuItem; // new + var menubarItemOpenStage:MenuItem; // open + var menubarItemOpenRecent:Menu; // open recent submenu + var menubarItemSaveStage:MenuItem; // save + var menubarItemSaveStageAs:MenuItem; // save as + var menubarItemClearAssets:MenuItem; // clear assets + var menubarItemExit:MenuItem; // exit + + var menubarMenuEdit:Menu; + var menubarItemUndo:MenuItem; // undo + var menubarItemRedo:MenuItem; // redo + var menubarItemCopy:MenuItem; // copy + var menubarItemCut:MenuItem; // cut + var menubarItemPaste:MenuItem; // paste + var menubarItemDelete:MenuItem; // delete + var menubarItemNewObj:MenuItem; // new + var menubarItemFindObj:MenuItem; // find + var menubarItemMoveStep:Menu; // move step submenu + + var menubarMenuView:Menu; + var menubarItemThemeLight:MenuOptionBox; // light mode option + var menubarItemThemeDark:MenuOptionBox; // dark mode option + var menubarItemViewChars:MenuCheckBox; // view chars check + var menubarItemViewNameText:MenuCheckBox; // view name text check + var menubarItemViewFloorLines:MenuCheckBox; // view floor lines check + var menubarItemViewPosMarkers:MenuCheckBox; // view pos markers check + var menubarItemViewCamBounds:MenuCheckBox; // view cam bounds check + + var menubarMenuWindow:Menu; + var menubarItemWindowObject:MenuCheckBox; + var menubarItemWindowCharacter:MenuCheckBox; + var menubarItemWindowStage:MenuCheckBox; + + var menubarMenuHelp:Menu; + var menubarItemUserGuide:MenuItem; + var menubarItemGoToBackupsFolder:MenuItem; + var menubarItemAbout:MenuItem; + + var menubarButtonText:Button; // test stage button + var windowList:WindowList; + + var bottomBarModeText:Label; + var bottomBarSelectText:Label; + var bottomBarMoveStepText:Label; + var bottomBarAngleStepText:Label; + + var bg:FlxSprite; + + public var selectedSprite(default, set):StageEditorObject = null; + + function set_selectedSprite(value:StageEditorObject) + { + this.selectedSprite = value; + updateDialog(StageEditorDialogType.OBJECT); + + if (selectedSprite != null) + { + spriteMarker.setGraphicSize(Std.int(selectedSprite.width), Std.int(selectedSprite.height)); + spriteMarker.updateHitbox(); + } + + return selectedSprite; + } + + public var selectedChar(default, set):BaseCharacter = null; + + function set_selectedChar(value:BaseCharacter) + { + this.selectedChar = value; + updateDialog(StageEditorDialogType.CHARACTER); + return selectedChar; + } + + var isCursorOverHaxeUI(get, never):Bool; + + function get_isCursorOverHaxeUI():Bool + { + return Screen.instance.hasSolidComponentUnderPoint(Screen.instance.currentMouseX, Screen.instance.currentMouseY); + } + + public var spriteMarker:FlxSprite; + public var spriteArray:Array = []; + public var camMarker:FlxSprite; + + public var copiedSprite:StageEditorObjectData = null; + + public var stageZoom:Float = 1.0; + public var stageName:String = "Unnamed"; + public var stageFolder:String = "shared"; + + public var autoSaveTimer:FlxTimer = new FlxTimer(); + + public var saved(default, set):Bool = true; + public var currentFile(default, set):String = ""; + + function set_saved(value:Bool) + { + saved = value; + + updateWindowTitle(); + + if (!autoSaveTimer.finished) + { + autoSaveTimer.cancel(); + } + + if (!saved) + { + autoSaveTimer.start(Constants.AUTOSAVE_TIMER_DELAY_SEC, function(tmr:FlxTimer) { + FileUtil.createDirIfNotExists(BACKUPS_PATH); + + var data = this.packShitToZip(); + var path = haxe.io.Path.join([ + BACKUPS_PATH, + 'stage-editor-${funkin.util.DateUtil.generateTimestamp()}.${FileUtil.FILE_EXTENSION_INFO_FNFS.extension}' + ]); + + FileUtil.writeBytesToPath(path, data); + saved = true; + + Save.instance.stageEditorHasBackup = true; + Save.instance.flush(); + + notifyChange("Auto-Save", "A Backup of this Stage has been made."); + }); + } + + return value; + } + + function set_currentFile(value:String) + { + currentFile = value; + + updateWindowTitle(); + + if (currentFile != "") updateRecentFiles(); + + reloadRecentFiles(); + + return value; + } + + public var undoArray:Array = []; + public var redoArray:Array = []; + + public var nameTxt:FlxText; + + public var gf(get, never):BaseCharacter; + public var bf(get, never):BaseCharacter; + public var dad(get, never):BaseCharacter; + + function get_gf() + return charGroups[CharacterType.GF].getFirst(StageDataHandler.checkForCharacter); + + function get_bf() + return charGroups[CharacterType.BF].getFirst(StageDataHandler.checkForCharacter); + + function get_dad() + return charGroups[CharacterType.DAD].getFirst(StageDataHandler.checkForCharacter); + + public var charGroups:Map> = []; + + public var charCamOffsets:Map> = DEFAULT_CAMERA_OFFSETS.copy(); + public var charPos:Map> = DEFAULT_POSITIONS.copy(); + + public var bitmaps:Map = []; // used for optimizing the file size!!! + + var floorLines:Array = []; + var posCircles:Array = []; + var camFields:FlxTypedGroup; + var camHUD:FlxCamera; + var camGame:FlxCamera; + + public var camFollow:FlxObject; + public var moveOffset:Array = []; + public var moveStep:Int = 1; + public var moveMode:String = "assets"; + public var infoSelection:String = "None"; + public var dialogs:Map = []; + + var allowInput(get, never):Bool; + + function get_allowInput() + { + return FocusManager.instance.focus == null; + } + + var testingMode:Bool = false; + + var showChars(default, set):Bool = true; + + function set_showChars(value:Bool) + { + this.showChars = value; + + for (cooldude in getCharacters()) + cooldude.visible = showChars; + + return value; + } + + override public function create() + { + WindowManager.instance.reset(); + instance = this; + FlxG.sound.music.stop(); + WindowUtil.setWindowTitle("Friday Night Funkin\' Stage Editor"); + + AssetDataHandler.init(this); + + camGame = new FlxCamera(); + camHUD = new FlxCamera(); + camHUD.bgColor.alpha = 0; + + FlxG.cameras.reset(camGame); + FlxG.cameras.add(camHUD, false); + FlxG.cameras.setDefaultDrawTarget(camGame, true); + + persistentUpdate = false; + + // FlxG.worldBounds.set(0, 0, FlxG.width, FlxG.height); + + bg = FlxGridOverlay.create(10, 10); + bg.scrollFactor.set(); + add(bg); + + updateBGColors(); + + super.create(); + root.scrollFactor.set(); + root.cameras = [camHUD]; + root.width = FlxG.width; + root.height = FlxG.height; + + menubar.height = 35; + WindowManager.instance.container = root; + Screen.instance.addComponent(root); + + // group shit + assets + var gf = CharacterDataParser.fetchCharacter("gf", true); + gf.characterType = CharacterType.GF; + var dad = CharacterDataParser.fetchCharacter("dad", true); + dad.characterType = CharacterType.DAD; + var bf = CharacterDataParser.fetchCharacter("bf", true); + bf.characterType = CharacterType.BF; + + bf.flipX = !bf.getDataFlipX(); + gf.flipX = gf.getDataFlipX(); + dad.flipX = dad.getDataFlipX(); + + gf.updateHitbox(); + dad.updateHitbox(); + bf.updateHitbox(); + + // only one char !!! + charGroups = [ + CharacterType.BF => new FlxTypedGroup(1), + CharacterType.GF => new FlxTypedGroup(1), + CharacterType.DAD => new FlxTypedGroup(1) + ]; + + // this is the part where the stage generate function comes up + // apparently no, said the future me + // back to the regular program + + gf.x = charPos[CharacterType.GF][0] - gf.characterOrigin.x + gf.globalOffsets[0]; + gf.y = charPos[CharacterType.GF][1] - gf.characterOrigin.y + gf.globalOffsets[1]; + dad.x = charPos[CharacterType.DAD][0] - dad.characterOrigin.x + dad.globalOffsets[0]; + dad.y = charPos[CharacterType.DAD][1] - dad.characterOrigin.y + dad.globalOffsets[1]; + bf.x = charPos[CharacterType.BF][0] - bf.characterOrigin.x + bf.globalOffsets[0]; + bf.y = charPos[CharacterType.BF][1] - bf.characterOrigin.y + bf.globalOffsets[1]; + + selectedChar = bf; + + charGroups[CharacterType.GF].add(gf); + charGroups[CharacterType.DAD].add(dad); + charGroups[CharacterType.BF].add(bf); + + add(charGroups[CharacterType.GF]); + add(charGroups[CharacterType.DAD]); + add(charGroups[CharacterType.BF]); + + // ui + spriteMarker = new FlxSprite().makeGraphic(1, 1, FlxColor.CYAN); + spriteMarker.alpha = 0.3; + spriteMarker.zIndex = MAX_Z_INDEX + CHARACTER_COLORS.length + 3; // PLEASE + add(spriteMarker); + + camFields = new FlxTypedGroup(); + camFields.visible = false; + camFields.zIndex = MAX_Z_INDEX + CHARACTER_COLORS.length + 1; + + for (i in 0...CHARACTER_COLORS.length) + { + var floorLine = new FlxSprite().makeGraphic(FlxG.width * 10, 15, CHARACTER_COLORS[i]); + floorLine.screenCenter(X); + + var pointer = new FlxShapeCircle(0, 0, 30, cast {thickness: 2, color: CHARACTER_COLORS[i]}, CHARACTER_COLORS[i]); + + var field = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, CHARACTER_COLORS[i]); + + pointer.alpha = floorLine.alpha = field.alpha = 0.35; + pointer.ID = floorLine.ID = field.ID = i; + pointer.visible = floorLine.visible = false; + pointer.zIndex = floorLine.zIndex = MAX_Z_INDEX + 1 + i; + + add(floorLine); + add(pointer); + + floorLines.push(floorLine); + posCircles.push(pointer); + + camFields.add(field); + } + + camMarker = new FlxSprite().loadGraphic(FlxGraphic.fromClass(GraphicCursorCross)); + camMarker.setGraphicSize(80, 80); + camMarker.updateHitbox(); + camMarker.zIndex = MAX_Z_INDEX + CHARACTER_COLORS.length + 2; + + updateMarkerPos(); + + add(camFields); + add(camMarker); + + nameTxt = new FlxText(0, 0, 0, "", 24); + nameTxt.setFormat(Paths.font("vcr.ttf"), 24, FlxColor.WHITE, LEFT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK); + nameTxt.cameras = [camHUD]; + add(nameTxt); + + camFollow = new FlxObject(0, 0, 2, 2); + camFollow.screenCenter(); + add(camFollow); + + camGame.follow(camFollow); + + addUI(); + + findObjDialog = new FindObjDialog(this, selectedSprite == null ? "" : selectedSprite.name); + + FlxG.stage.window.onDropFile.add(function(path:String):Void { + if (!allowInput || welcomeDialog != null) return; + + var data = BitmapData.fromFile(path); + + if (data != null) + { + objNameDialog = new NewObjDialog(this, data); + objNameDialog.showDialog(); + + objNameDialog.onDialogClosed = function(_) { + objNameDialog = null; + } + + return; + } + }); + + onMenuItemClick("new stage"); + welcomeDialog.closable = false; + + #if sys + if (Save.instance.stageEditorHasBackup) + { + FileUtil.createDirIfNotExists(BACKUPS_PATH); + + var files = sys.FileSystem.readDirectory(BACKUPS_PATH); + + if (files.length > 0) + { + // ensures that the top most file is a backup + files.sort(funkin.util.SortUtil.alphabetically); + + while (!files[files.length - 1].endsWith(FileUtil.FILE_EXTENSION_INFO_FNFS.extension) + || !files[files.length - 1].startsWith("stage-editor-")) + files.pop(); + } + + if (files.length != 0) new BackupAvailableDialog(this, haxe.io.Path.join([BACKUPS_PATH, files[files.length - 1]])).showDialog(true); + } + #end + + WindowUtil.windowExit.add(windowClose); + CrashHandler.errorSignal.add(autosavePerCrash); + CrashHandler.criticalErrorSignal.add(autosavePerCrash); + + Save.instance.stageEditorHasBackup = false; + + Cursor.show(); + FlxG.sound.playMusic(Paths.music('chartEditorLoop/chartEditorLoop')); + FlxG.sound.music.fadeIn(10, 0, 1); + } + + var curTestChar:Int = 0; + + override public function beatHit() + { + if (testingMode) + { + if (conductorInUse.currentBeat % 2 == 0) + { + for (char in getCharacters()) + char.dance(true); + } + + for (asset in spriteArray) + { + if (asset.danceEvery > 0 && conductorInUse.currentBeat % asset.danceEvery == 0) asset.dance(true); + } + + if (conductorInUse.currentBeat % 8 == 0 && !FlxG.keys.pressed.SHIFT) curTestChar++; + } + + return super.beatHit(); + } + + override public function update(elapsed:Float) + { + updateBGSize(); + conductorInUse.update(); + + super.update(elapsed); + + if (FlxG.mouse.justPressed || FlxG.mouse.justPressedRight) FunkinSound.playOnce(Paths.sound("chartingSounds/ClickDown")); + if (FlxG.mouse.justReleased || FlxG.mouse.justReleasedRight) FunkinSound.playOnce(Paths.sound("chartingSounds/ClickUp")); + + // testmode + menubarMenuFile.disabled = menubarMenuEdit.disabled = bottomBarModeText.disabled = menubarMenuWindow.disabled = testingMode; + menubarButtonText.selected = testingMode; + + if (testingMode) + { + for (char in getCharacters()) + char.alpha = 1; + + spriteMarker.visible = camMarker.visible = false; + findObjDialog.hideDialog(DialogButton.CANCEL); + + // cam + camGame.follow(camFollow, LOCKON, 0.04); + FlxG.camera.zoom = stageZoom; + + if (FlxG.keys.justPressed.TAB && !FlxG.keys.pressed.SHIFT) curTestChar++; + + if (curTestChar >= getCharacters().length) curTestChar = 0; + + bottomBarSelectText.text = Std.string(getCharacters()[curTestChar].characterType); + + var char = getCharacters()[curTestChar]; + camFollow.x = char.cameraFocusPoint.x + charCamOffsets.get(char.characterType)[0]; + camFollow.y = char.cameraFocusPoint.y + charCamOffsets.get(char.characterType)[1]; + + // EXIT + if (FlxG.keys.justPressed.ENTER) // so we dont accidentally get stuck (happened to me once, terrible experience) + onMenuItemClick("test stage"); + + return; + } + + // some misc + nameTxt.text = ""; + bottomBarModeText.text = (moveMode == "assets" ? "Objects" : "Characters"); + + camGame.follow(camFollow); + // camera movement + + if ((FlxG.mouse.wheel > 0 || (FlxG.mouse.wheel < 0 && camGame.zoom > 0.11)) + && !isCursorOverHaxeUI) // include the floating poing error thing + { + camGame.zoom += FlxG.mouse.wheel / 10; + updateBGSize(); + } + + // key shortcuts and inputs + if (allowInput) + { + if (FlxG.keys.pressed.CONTROL) + { + if (FlxG.keys.justPressed.Z) onMenuItemClick("undo"); + if (FlxG.keys.justPressed.Y) onMenuItemClick("redo"); + if (FlxG.keys.justPressed.C) onMenuItemClick("copy object"); + if (FlxG.keys.justPressed.V) onMenuItemClick("paste object"); + if (FlxG.keys.justPressed.X) onMenuItemClick("cut object"); + if (FlxG.keys.justPressed.S) FlxG.keys.pressed.SHIFT ? onMenuItemClick("save stage as") : onMenuItemClick("save stage"); + if (FlxG.keys.justPressed.F) onMenuItemClick("find object"); + if (FlxG.keys.justPressed.O) onMenuItemClick("open stage"); + } + + if (FlxG.keys.justPressed.TAB) onMenuItemClick("switch mode"); + if (FlxG.keys.justPressed.DELETE) onMenuItemClick("delete object"); + if (FlxG.keys.justPressed.ENTER) onMenuItemClick("test stage"); + if (FlxG.keys.justPressed.ESCAPE) onMenuItemClick("exit"); + if (FlxG.keys.justPressed.F1) onMenuItemClick("user guide"); + + if (FlxG.keys.justPressed.T) + { + camFollow.screenCenter(); + FlxG.camera.zoom = 1; + } + + if (FlxG.keys.pressed.W || FlxG.keys.pressed.S || FlxG.keys.pressed.A || FlxG.keys.pressed.D) + { + if (FlxG.keys.pressed.W) camFollow.velocity.y = -90 * (2 / FlxG.camera.zoom); + else if (FlxG.keys.pressed.S) camFollow.velocity.y = 90 * (2 / FlxG.camera.zoom); + else + camFollow.velocity.y = 0; + + if (FlxG.keys.pressed.A) camFollow.velocity.x = -90 * (2 / FlxG.camera.zoom); + else if (FlxG.keys.pressed.D) camFollow.velocity.x = 90 * (2 / FlxG.camera.zoom); + else + camFollow.velocity.x = 0; + } + else + { + camFollow.velocity.set(); + } + } + else + { + camFollow.velocity.set(); + } + + // movement handling + if (FlxG.mouse.justReleased && moveOffset.length > 0) moveOffset = []; + + if (moveMode == "assets") + { + for (spr in spriteArray) + { + spr.active = spr.isOnScreen(); + + if (FlxG.mouse.overlaps(spr)) + { + if (spr.visible && !FlxG.keys.pressed.SHIFT) nameTxt.text = spr.name; + + if (FlxG.mouse.justPressed && allowInput && spr.visible && !FlxG.keys.pressed.SHIFT && !isCursorOverHaxeUI) + { + selectedSprite = spr; + updateDialog(StageEditorDialogType.OBJECT); + } + } + + if (spr == selectedSprite) + { + infoSelection = spr.name; + + if (FlxG.keys.pressed.SHIFT) nameTxt.text = spr.name + " (LOCKED)"; + } + } + + if (FlxG.mouse.pressed && allowInput && selectedSprite != null && FlxG.mouse.overlaps(selectedSprite) && FlxG.mouse.justMoved && !isCursorOverHaxeUI) + { + saved = false; + updateDialog(StageEditorDialogType.OBJECT); + + if (moveOffset.length == 0) + { + this.createAndPushAction(OBJECT_MOVED); + + moveOffset = [ + FlxG.mouse.getWorldPosition().x - selectedSprite.x, + FlxG.mouse.getWorldPosition().y - selectedSprite.y + ]; + } + + var posBros = new FlxPoint(FlxG.mouse.getWorldPosition().x - moveOffset[0], FlxG.mouse.getWorldPosition().y - moveOffset[1]); + selectedSprite.x = (Math.floor(posBros.x) - Math.floor(posBros.x) % moveStep); + selectedSprite.y = (Math.floor(posBros.y) - Math.floor(posBros.y) % moveStep); + } + + if (selectedSprite != null && FlxG.keys.pressed.R) + { + if (FlxG.keys.justPressed.LEFT || FlxG.keys.justPressed.RIGHT) + { + saved = false; + this.createAndPushAction(OBJECT_ROTATED); + } + + if (FlxG.keys.justPressed.LEFT) selectedSprite.angle -= Save.instance.stageEditorAngleStep; + if (FlxG.keys.justPressed.RIGHT) selectedSprite.angle += Save.instance.stageEditorAngleStep; + } + + arrowMovement(selectedSprite); + + for (char in getCharacters()) + char.alpha = 1; + } + else + { + selectedChar.alpha = 1; + + for (char in getCharacters()) + { + if (char != selectedChar) char.alpha = 0.3; + + if (char != null && checkCharOverlaps(char)) // flxg.mouse.overlaps crashes the game + { + if (char.visible && !FlxG.keys.pressed.SHIFT) nameTxt.text = Std.string(char.characterType); + + if (FlxG.mouse.justPressed && allowInput && char.visible && !FlxG.keys.pressed.SHIFT && !isCursorOverHaxeUI) + { + selectedChar = char; + updateDialog(StageEditorDialogType.CHARACTER); + } + } + + if (selectedChar == char) + { + infoSelection = Std.string(char.characterType); + + if (FlxG.keys.pressed.SHIFT) nameTxt.text = Std.string(char.characterType) + " (LOCKED)"; + } + } + + if (FlxG.mouse.pressed && allowInput && checkCharOverlaps(selectedChar) && FlxG.mouse.justMoved && !isCursorOverHaxeUI) + { + saved = false; + updateDialog(StageEditorDialogType.CHARACTER); + + if (moveOffset.length == 0) + { + this.createAndPushAction(CHARACTER_MOVED); + + moveOffset = [ + FlxG.mouse.getWorldPosition().x - selectedChar.cornerPosition.x, + FlxG.mouse.getWorldPosition().y - selectedChar.cornerPosition.y + ]; + } + + var posBros = new FlxPoint(FlxG.mouse.getWorldPosition().x - moveOffset[0], FlxG.mouse.getWorldPosition().y - moveOffset[1]); + + selectedChar.cornerPosition = new FlxPoint(Math.floor(posBros.x) - Math.floor(posBros.x) % moveStep, + Math.floor(posBros.y) - Math.floor(posBros.y) % moveStep); + } + + arrowMovement(selectedChar); + updateMarkerPos(); + } + + if ((selectedSprite == null && moveMode == "assets") || (selectedChar == null && moveMode == "chars")) infoSelection = "None"; + bottomBarSelectText.text = infoSelection; + + // ui stuff + nameTxt.x = FlxG.mouse.getScreenPosition(camHUD).x; + nameTxt.y = FlxG.mouse.getScreenPosition(camHUD).y - nameTxt.height; + + spriteMarker.visible = (moveMode == "assets" && selectedSprite != null); + camMarker.visible = moveMode == "chars"; + if (selectedSprite != null) spriteMarker.setPosition(selectedSprite.x, selectedSprite.y); + + for (item in sprDependant) + item.disabled = !spriteMarker.visible; + + menubarItemPaste.disabled = copiedSprite == null; + menubarItemFindObj.disabled = !(moveMode == "assets"); + + if (moveMode == "chars") findObjDialog.hideDialog(DialogButton.CANCEL); + + menubarItemUndo.disabled = undoArray.length == 0; + menubarItemRedo.disabled = redoArray.length == 0; + } + + public function getCharacters() + { + return [gf, dad, bf]; + } + + function autosavePerCrash(message:String) + { + trace("fuuuucckkkkk we crashed, reason: " + message); + + if (!saved) + { + trace("dw we're making a backup!!!"); + autoSaveTimer.onComplete(autoSaveTimer); + } + } + + function windowClose(exitCode:Int) + { + trace("closing da window ye"); + + if (!saved) + { + trace("dum dum why no save >:["); + autoSaveTimer.onComplete(autoSaveTimer); + } + } + + public function updateRecentFiles() + { + var files = Save.instance.stageEditorPreviousFiles; + files.remove(currentFile); + files.unshift(currentFile); + + while (files.length > Constants.MAX_PREVIOUS_WORKING_FILES) + files.pop(); + + Save.instance.stageEditorPreviousFiles = files; + Save.instance.flush(); + } + + public function updateMarkerPos() + { + for (i in 0...getCharacters().length) + { + var char = getCharacters()[i]; + var type = char.characterType; + + charPos.set(type, [ + char.feetPosition.x - char.globalOffsets[0], + char.feetPosition.y - char.globalOffsets[1] + ]); + + floorLines[i].y = charPos.get(type)[1] - floorLines[i].height / 2; + + posCircles[i].y = charPos.get(type)[1] - posCircles[i].height / 2; + posCircles[i].x = charPos.get(type)[0] - posCircles[i].width / 2; + + camFields.members[i].scale.set(1 / stageZoom, 1 / stageZoom); + camFields.members[i].updateHitbox(); + + camFields.members[i].x = char.cameraFocusPoint.x + charCamOffsets.get(type)[0] - camFields.members[i].width / 2; + camFields.members[i].y = char.cameraFocusPoint.y + charCamOffsets.get(type)[1] - camFields.members[i].height / 2; + + if (char == selectedChar) + { + camMarker.x = camFields.members[i].getMidpoint().x - camMarker.width / 2; + camMarker.y = camFields.members[i].getMidpoint().y - camMarker.height / 2; + } + } + } + + // made because characters have shitty hitboxes and often cause the game to straight up crash + // it comes from some flxobject/polymod error apparently and I have no idea why + function checkCharOverlaps(char:BaseCharacter) + { + var mouseX = FlxG.mouse.x >= char.x && FlxG.mouse.x <= char.x + char.width; + var mouseY = FlxG.mouse.y >= char.y && FlxG.mouse.y <= char.y + char.height; + + return mouseX && mouseY && !isCursorOverHaxeUI; + } + + var moveUndoed:Bool = false; + + // i wish there was a better way to do this this looks like an eyesore + // yanderedev fr + function arrowMovement(obj:FlxSprite) + { + if (obj == null) return; + if (FlxG.keys.pressed.R) return; // rotations + + if (allowInput) + { + if ((FlxG.keys.justPressed.UP || FlxG.keys.justPressed.DOWN || FlxG.keys.justPressed.LEFT || FlxG.keys.justPressed.RIGHT) + && !moveUndoed) + { + saved = false; + moveUndoed = true; + this.createAndPushAction(moveMode == "assets" ? OBJECT_MOVED : CHARACTER_MOVED); + } + + if ((FlxG.keys.justReleased.UP || FlxG.keys.justReleased.DOWN || FlxG.keys.justReleased.LEFT || FlxG.keys.justReleased.RIGHT) + && moveUndoed) + { + moveUndoed = false; + } + + if (FlxG.keys.pressed.SHIFT) + { + if (FlxG.keys.pressed.UP) obj.y--; + if (FlxG.keys.pressed.DOWN) obj.y++; + if (FlxG.keys.pressed.LEFT) obj.x--; + if (FlxG.keys.pressed.RIGHT) obj.x++; + } + else + { + if (FlxG.keys.justPressed.UP) obj.y -= moveStep; + if (FlxG.keys.justPressed.DOWN) obj.y += moveStep; + if (FlxG.keys.justPressed.LEFT) obj.x -= moveStep; + if (FlxG.keys.justPressed.RIGHT) obj.x += moveStep; + } + } + } + + public function updateArray() + { + spriteArray = []; + + for (thing in members) + { + if (Std.isOfType(thing, StageEditorObject)) spriteArray.push(cast thing); // characters do not extend stageeditorobject so we ball + } + + findObjDialog.updateIndicator(); + updateDialog(StageEditorDialogType.OBJECT); + } + + public function sortAssets() + { + sort(funkin.util.SortUtil.byZIndex, flixel.util.FlxSort.ASCENDING); + } + + public function updateDialog(type:StageEditorDialogType) + { + if (!dialogs.exists(type)) return; + + dialogs[type].refresh(); + } + + public function toggleDialog(type:StageEditorDialogType, show:Bool = true) + { + if (!dialogs.exists(type)) return; + + dialogs[type].toggle(show); + } + + public function updateWindowTitle() + { + var defaultTitle = "Friday Night Funkin\' Stage Editor"; + + if (currentFile == "") defaultTitle += " - New File" + else + defaultTitle += " - " + currentFile; + + if (!saved) defaultTitle += "*"; + + WindowUtil.setWindowTitle(defaultTitle); + } + + function resetWindowTitle():Void + { + WindowUtil.setWindowTitle('Friday Night Funkin\''); + } + + function updateBGColors() + { + var colArray = Save.instance.stageEditorTheme == StageEditorTheme.Dark ? DARK_MODE_COLORS : LIGHT_MODE_COLORS; + + var index = members.indexOf(bg); + bg.kill(); + remove(bg); + bg.destroy(); + + bg = FlxGridOverlay.create(10, 10, -1, -1, true, colArray[0], colArray[1]); + bg.scrollFactor.set(); + members.insert(index, bg); + } + + function updateBGSize() + { + bg.scale.set(1 / FlxG.camera.zoom, 1 / FlxG.camera.zoom); + bg.updateHitbox(); + bg.screenCenter(); + } + + function checkOverlaps(spr:FlxSprite) + { + if (FlxG.mouse.overlaps(spr) /*spr.overlapsPoint(FlxG.mouse.getWorldPosition(spr.camera), true, spr.camera) */ + && Screen.instance != null + && !Screen.instance.hasSolidComponentUnderPoint(FlxG.mouse.screenX, FlxG.mouse.screenY) + && WindowManager.instance.windows.length == 0) // ik its stupid but maybe I have other cases soon (i did) + return true; + + return false; + } + + var sprDependant:Array = []; + + function addUI() + { + menubarItemNewStage.onClick = function(_) onMenuItemClick("new stage"); + menubarItemOpenStage.onClick = function(_) onMenuItemClick("open stage"); + menubarItemSaveStage.onClick = function(_) onMenuItemClick("save stage"); + menubarItemSaveStageAs.onClick = function(_) onMenuItemClick("save stage as"); + menubarItemClearAssets.onClick = function(_) onMenuItemClick("clear assets"); + menubarItemExit.onClick = function(_) onMenuItemClick("exit"); + menubarItemUndo.onClick = function(_) onMenuItemClick("undo"); + menubarItemRedo.onClick = function(_) onMenuItemClick("redo"); + menubarItemCopy.onClick = function(_) onMenuItemClick("copy object"); + menubarItemCut.onClick = function(_) onMenuItemClick("cut object"); + menubarItemPaste.onClick = function(_) onMenuItemClick("paste stage"); + menubarItemDelete.onClick = function(_) onMenuItemClick("delete object"); + menubarItemNewObj.onClick = function(_) onMenuItemClick("new object"); + menubarItemFindObj.onClick = function(_) onMenuItemClick("find object"); + menubarButtonText.onClick = function(_) onMenuItemClick("test stage"); + menubarItemUserGuide.onClick = function(_) onMenuItemClick("user guide"); + menubarItemGoToBackupsFolder.onClick = function(_) onMenuItemClick("open folder"); + menubarItemAbout.onClick = function(_) onMenuItemClick("about"); + + bottomBarModeText.onClick = function(_) onMenuItemClick("switch mode"); + bottomBarSelectText.onClick = function(_) onMenuItemClick("switch focus"); + + var stepOptions = ["1px", "2px", "3px", "5px", "10px", "25px", "50px", "100px"]; + bottomBarMoveStepText.text = stepOptions.contains(Save.instance.stageEditorMoveStep) ? Save.instance.stageEditorMoveStep : "1px"; + + var changeStep = function(change:Int = 0) { + var id = stepOptions.indexOf(bottomBarMoveStepText.text); + id += change; + + if (id >= stepOptions.length) id = stepOptions.length - 1; + else if (id < 0) id = 0; + + bottomBarMoveStepText.text = Save.instance.stageEditorMoveStep = stepOptions[id]; + var shit = Std.parseInt(StringTools.replace(bottomBarMoveStepText.text, "px", "")); + moveStep = shit; + + updateDialog(StageEditorDialogType.OBJECT); + updateDialog(StageEditorDialogType.CHARACTER); + updateDialog(StageEditorDialogType.STAGE); + } + + bottomBarMoveStepText.onClick = function(_) changeStep(1); + bottomBarMoveStepText.onRightClick = function(_) changeStep(-1); + + changeStep(); // update + + var angleOptions = [0.5, 1, 2, 5, 10, 15, 45, 75, 90, 180]; + bottomBarAngleStepText.text = (angleOptions.contains(Save.instance.stageEditorAngleStep) ? Save.instance.stageEditorAngleStep : 5) + "°"; + + var changeAngle = function(change:Int = 0) { + var id = angleOptions.indexOf(Save.instance.stageEditorAngleStep); + id += change; + + if (id >= angleOptions.length) id = angleOptions.length - 1; + else if (id < 0) id = 0; + + Save.instance.stageEditorAngleStep = angleOptions[id]; + bottomBarAngleStepText.text = (angleOptions.contains(Save.instance.stageEditorAngleStep) ? Save.instance.stageEditorAngleStep : 5) + "°"; + + updateDialog(StageEditorDialogType.OBJECT); + } + + bottomBarAngleStepText.onClick = function(_) changeAngle(1); + bottomBarAngleStepText.onRightClick = function(_) changeAngle(-1); + + changeAngle(); // update + + dialogs.set(StageEditorDialogType.OBJECT, new StageEditorObjectToolbox(this)); + dialogs.set(StageEditorDialogType.CHARACTER, new StageEditorCharacterToolbox(this)); + dialogs.set(StageEditorDialogType.STAGE, new StageEditorStageToolbox(this)); + + menubarItemWindowObject.onChange = function(_) toggleDialog(StageEditorDialogType.OBJECT, menubarItemWindowObject.selected); + menubarItemWindowCharacter.onChange = function(_) toggleDialog(StageEditorDialogType.CHARACTER, menubarItemWindowCharacter.selected); + menubarItemWindowStage.onChange = function(_) toggleDialog(StageEditorDialogType.STAGE, menubarItemWindowStage.selected); + + menubarItemThemeLight.onClick = function(_) { + Save.instance.stageEditorTheme = StageEditorTheme.Light; + updateBGColors(); + } + + menubarItemThemeDark.onClick = function(_) { + Save.instance.stageEditorTheme = StageEditorTheme.Dark; + updateBGColors(); + } + + menubarItemThemeDark.selected = Save.instance.stageEditorTheme == StageEditorTheme.Dark; + menubarItemThemeLight.selected = Save.instance.stageEditorTheme == StageEditorTheme.Light; + + menubarItemViewChars.onChange = function(_) showChars = menubarItemViewChars.selected; + menubarItemViewNameText.onChange = function(_) nameTxt.visible = menubarItemViewNameText.selected; + menubarItemViewCamBounds.onChange = function(_) camFields.visible = menubarItemViewCamBounds.selected; + + menubarItemViewFloorLines.onChange = function(_) { + for (awesome in floorLines) + awesome.visible = menubarItemViewFloorLines.selected; + } + + menubarItemViewPosMarkers.onChange = function(_) { + for (coolbeans in posCircles) + coolbeans.visible = menubarItemViewPosMarkers.selected; + } + + sprDependant = [menubarItemCopy, menubarItemCut, menubarItemDelete]; + reloadRecentFiles(); + } + + function reloadRecentFiles() + { + for (a in menubarItemOpenRecent.childComponents) + menubarItemOpenRecent.removeComponent(a); + + for (file in Save.instance.stageEditorPreviousFiles) + { + var filePath = new haxe.io.Path(file); + var item = new MenuItem(); + item.text = filePath.file + "." + filePath.ext; + item.disabled = !FileUtil.doesFileExist(file); + + var load = function(file:String) { + currentFile = file; + + this.unpackShitFromZip(FileUtil.readBytesFromPath(file)); + + reloadRecentFiles(); + } + + item.onClick = function(_) { + if (!saved) + { + Dialogs.messageBox("Opening a new Stage will reset all your progress for this Stage.\n\nAre you sure you want to proceed?", "Open Stage", + MessageBoxType.TYPE_YESNO, true, function(btn:DialogButton) { + if (btn == DialogButton.YES) + { + saved = true; + load(file); + } + }); + } + else + { + load(file); + } + } + + menubarItemOpenRecent.addComponent(item); + } + } + + public var objNameDialog:NewObjDialog; + public var findObjDialog:FindObjDialog; + public var welcomeDialog:WelcomeDialog; + public var userGuideDialog:UserGuideDialog; + public var aboutDialog:AboutDialog; + public var loadUrlDialog:LoadFromUrlDialog; + + public function onMenuItemClick(item:String) + { + switch (item.toLowerCase()) + { + case "undo" | "redo": + this.performLastAction(item.toLowerCase() == "redo"); + + case "save stage as": + var bytes = this.packShitToZip(); + + if (bytes == null) + { + notifyChange("Stage Save", "Problem Saving a Stage. Please try again later.", true); + return; + } + + FileUtil.saveFile(bytes, [FileUtil.FILE_FILTER_FNFS], function(path:String) { + saved = true; + currentFile = path; + }, null, stageName + "." + FileUtil.FILE_EXTENSION_INFO_FNFS.extension); + + bitmaps.clear(); + + case "save stage": + if (currentFile == "") + { + onMenuItemClick("save stage as"); // ah I love coding shortcuts + return; + } + + var bytes = this.packShitToZip(); + + if (bytes == null) + { + notifyChange("Stage Save", "Problem Saving a Stage. Please try again later.", true); + return; + } + + FileUtil.writeBytesToPath(currentFile, bytes, Force); // mhm + + saved = true; + + updateRecentFiles(); + bitmaps.clear(); + + case "open stage": + if (!saved) + { + Dialogs.messageBox("Opening a new Stage will reset all your progress for this Stage.\n\nAre you sure you want to proceed?", "Open Stage", + MessageBoxType.TYPE_YESNO, true, function(btn:DialogButton) { + if (btn == DialogButton.YES) + { + saved = true; + onMenuItemClick("open stage"); // ough + } + }); + + return; + } + + FileUtil.browseForSaveFile([FileUtil.FILE_FILTER_FNFS], function(path:String) { + clearAssets(); + + currentFile = path; + this.unpackShitFromZip(FileUtil.readBytesFromPath(path)); + + reloadRecentFiles(); + }, null, null, "Open Stage Data"); + + case "exit": + if (!saved) + { + Dialogs.messageBox("You are about to leave the Editor without Saving.\n\nAre you sure? ", "Leave Editor", MessageBoxType.TYPE_YESNO, true, + function(btn:DialogButton) { + if (btn == DialogButton.YES) + { + saved = true; + onMenuItemClick("exit"); + } + }); + + return; + } + + resetWindowTitle(); + + WindowUtil.windowExit.remove(windowClose); + CrashHandler.errorSignal.remove(autosavePerCrash); + CrashHandler.criticalErrorSignal.remove(autosavePerCrash); + + Cursor.hide(); + FlxG.switchState(() -> new DebugMenuSubState()); + FlxG.sound.music.stop(); + + case "switch mode": + if (!testingMode) moveMode = (moveMode == "assets" ? "chars" : "assets"); + + case "switch focus": + if (testingMode) + { + curTestChar++; + } + else + { + if (moveMode == "chars") + { + var chars = getCharacters(); + var index = chars.indexOf(selectedChar); + index++; + + if (index >= chars.length) index = 0; + + selectedChar = chars[index]; + } + else + { + if (selectedSprite == null) return; + + var index = spriteArray.indexOf(selectedSprite); + index++; + + if (index >= spriteArray.length) index = 0; + + selectedSprite = spriteArray[index]; + } + } + + case "new object": + findObjDialog.hideDialog(DialogButton.CANCEL); + + trace("aignt we making a new object baby"); + + objNameDialog = new NewObjDialog(this); + objNameDialog.showDialog(); + + objNameDialog.onDialogClosed = function(_) { + objNameDialog = null; + } + + case "find object": + findObjDialog.hideDialog(DialogButton.CANCEL); + findObjDialog = new FindObjDialog(this, selectedSprite == null ? "" : selectedSprite.name); + findObjDialog.showDialog(false); + + case "about": + aboutDialog = new AboutDialog(); + aboutDialog.showDialog(); + + case "user guide": + userGuideDialog = new UserGuideDialog(); + userGuideDialog.showDialog(); + + case "open folder": + #if sys + var absoluteBackupsPath:String = haxe.io.Path.join([Sys.getCwd(), BACKUPS_PATH]); + WindowUtil.openFolder(absoluteBackupsPath); + #end + + case "test stage": + if (!allowInput) return; + + camFollow.velocity.set(); + + for (a in spriteArray) + { + a.active = true; + a.isDebugged = testingMode; + } + + if (!testingMode) menubarItemWindowObject.selected = menubarItemWindowCharacter.selected = menubarItemWindowStage.selected = false; + + testingMode = !testingMode; + + case "clear assets": + Dialogs.messageBox("This will destroy all the Objects in this Stage.\n\nAre you sure? This cannot be undone.", "Clear Assets", + MessageBoxType.TYPE_YESNO, true, function(btn:DialogButton) { + if (btn == DialogButton.YES) + { + clearAssets(); + saved = false; + + updateDialog(StageEditorDialogType.OBJECT); + } + }); + + case "center on screen": + if (selectedSprite != null && moveMode == "assets") + { + selectedSprite.screenCenter(); + updateDialog(StageEditorDialogType.OBJECT); + saved = false; + } + + if (selectedChar != null && moveMode == "chars") + { + selectedChar.screenCenter(); + updateDialog(StageEditorDialogType.CHARACTER); + saved = false; + } + + case "delete object": + this.createAndPushAction(OBJECT_DELETED); + + spriteArray.remove(selectedSprite); + + selectedSprite.kill(); + remove(selectedSprite, true); + selectedSprite.destroy(); + selectedSprite = null; + + updateArray(); + sortAssets(); + + case "copy object": + if (selectedSprite == null) return; + + copiedSprite = selectedSprite.toData(true); + + case "paste object": + if (copiedSprite == null) return; + + saved = false; + var spr = new StageEditorObject().fromData(copiedSprite); + + var objNames = [for (a in spriteArray) a.name]; + + if (objNames.contains(spr.name)) + { + var i = 1; + while (objNames.contains(spr.name + " (" + i + ")")) + i++; + + spr.name += " (" + i + ")"; + } + + selectedSprite = spr; + updateArray(); + + case "cut object": // rofl + onMenuItemClick("copy object"); + onMenuItemClick("delete object"); // already changes the saved var + + case "new stage": + if (menubarItemWindowObject.selected) menubarItemWindowObject.selected = false; + if (menubarItemWindowCharacter.selected) menubarItemWindowCharacter.selected = false; + if (menubarItemWindowStage.selected) menubarItemWindowStage.selected = false; + + welcomeDialog = new WelcomeDialog(this); + welcomeDialog.showDialog(); + welcomeDialog.closable = true; + welcomeDialog.onDialogClosed = function(_) { + updateWindowTitle(); + welcomeDialog = null; + + updateDialog(StageEditorDialogType.OBJECT); + updateDialog(StageEditorDialogType.CHARACTER); + updateDialog(StageEditorDialogType.STAGE); + } + } + } + + public function clearAssets() + { + selectedSprite = null; + + while (spriteArray.length > 0) + { + var spr = spriteArray.pop(); + spr.kill(); + remove(spr, true); + spr.destroy(); + spr = null; + } + + undoArray = []; + redoArray = []; + + updateArray(); + sortAssets(); + removeUnusedBitmaps(); + } + + public function removeUnusedBitmaps() + { + var usedBitmaps:Array = []; + + for (asset in spriteArray) + { + var data = asset.toData(false); + if (data.assetPath.startsWith("#")) continue; // the simple graphics + + usedBitmaps.push(data.assetPath); + } + + for (name => bit in bitmaps) + { + if (usedBitmaps.contains(name)) continue; + bitmaps.remove(name); + } + } + + public function addBitmap(newBitmap:BitmapData):String + { + // first we check for existing bitmaps so we dont like add an extra one + for (name => bitmap in bitmaps) + { + if (bitmap == newBitmap) return name; + } + + var id:Int = 0; + while (bitmaps.exists("image" + id)) + id++; + + bitmaps.set("image" + id, newBitmap); + return "image" + id; + } + + public function notifyChange(change:String, notif:String, isError:Bool = false) + { + NotificationManager.instance.addNotification( + { + title: change, + body: notif, + type: isError ? NotificationType.Error : NotificationType.Info + }); + } + + public function createURLDialog(onComplete:lime.utils.Bytes->Void = null, onFail:String->Void = null) + { + loadUrlDialog = new LoadFromUrlDialog(onComplete, onFail); + loadUrlDialog.onDialogClosed = function(_) { + loadUrlDialog = null; + } + + loadUrlDialog.showDialog(); + } +} + +/** + * Available themes for the stage editor state. + */ +enum StageEditorTheme +{ + /** + * The default theme for the stage editor. + */ + Light; + + /** + * A theme which introduces stage colors. + */ + Dark; +} + +enum StageEditorDialogType +{ + /** + * The Stage Options Dialog. + */ + STAGE; + + /** + * The Character Options Dialog. + */ + CHARACTER; + + /** + * The Object Options Dialog. + */ + OBJECT; +} diff --git a/source/funkin/ui/debug/stageeditor/components/AboutDialog.hx b/source/funkin/ui/debug/stageeditor/components/AboutDialog.hx new file mode 100644 index 000000000..e419b964c --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/components/AboutDialog.hx @@ -0,0 +1,6 @@ +package funkin.ui.debug.stageeditor.components; + +import haxe.ui.containers.dialogs.Dialog; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/about.xml")) +class AboutDialog extends Dialog {} diff --git a/source/funkin/ui/debug/stageeditor/components/BackupAvailableDialog.hx b/source/funkin/ui/debug/stageeditor/components/BackupAvailableDialog.hx new file mode 100644 index 000000000..6c3041e28 --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/components/BackupAvailableDialog.hx @@ -0,0 +1,80 @@ +package funkin.ui.debug.stageeditor.components; + +import haxe.ui.containers.dialogs.Dialog; +import haxe.ui.containers.dialogs.Dialog.DialogButton; +import funkin.util.FileUtil; +import haxe.io.Path; +import funkin.util.DateUtil; +import funkin.util.WindowUtil; + +using StringTools; + +@:xml(' + + + +') +class BackupAvailableDialog extends Dialog +{ + override public function new(state:StageEditorState, filePath:String) + { + super(); + + if (!FileUtil.doesFileExist(filePath)) return; + + // time text + var fileDate = Path.withoutExtension(Path.withoutDirectory(filePath)); + var dateParts = fileDate.split("-"); + + while (dateParts.length < 8) + dateParts.push("0"); + + var year:Int = Std.parseInt(dateParts[2]) ?? 0; // copied parts from ChartEditorImportExportHandler.hx + var month:Int = Std.parseInt(dateParts[3]) ?? 1; + var day:Int = Std.parseInt(dateParts[4]) ?? 0; + var hour:Int = Std.parseInt(dateParts[5]) ?? 0; + var minute:Int = Std.parseInt(dateParts[6]) ?? 0; + var second:Int = Std.parseInt(dateParts[7]) ?? 0; + + backupTimeLabel.text = DateUtil.generateCleanTimestamp(new Date(year, month - 1, day, hour, minute, second)); + + // button callbacks + dialogCancel.onClick = function(_) hideDialog(DialogButton.CANCEL); + + buttonGoToFolder.onClick = function(_) { + // :[ + #if sys + var absoluteBackupsPath:String = Path.join([Sys.getCwd(), StageEditorState.BACKUPS_PATH]); + WindowUtil.openFolder(absoluteBackupsPath); + #end + } + + buttonOpenBackup.onClick = function(_) { + if (FileUtil.doesFileExist(filePath) && state.welcomeDialog != null) // doing a check in case a sleezy FUCK decides to delete the backup file AFTER dialog opens + { + state.welcomeDialog.loadFromFilePath(filePath); + } + hideDialog(DialogButton.APPLY); + } + + // uhhh + onDialogClosed = function(event) { + if (event.button == DialogButton.APPLY) + { + if (state.welcomeDialog != null) state.welcomeDialog.hideDialog(DialogButton.APPLY); + } + }; + } +} diff --git a/source/funkin/ui/debug/stageeditor/components/ExitConfirmDialog.hx b/source/funkin/ui/debug/stageeditor/components/ExitConfirmDialog.hx new file mode 100644 index 000000000..91669dc4a --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/components/ExitConfirmDialog.hx @@ -0,0 +1,31 @@ +package funkin.ui.debug.stageeditor.components; + +import haxe.ui.containers.dialogs.Dialog; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/exit-confirm.xml")) +class ExitConfirmDialog extends Dialog +{ + var onComplete:Void->Void = null; + + override public function new(onComp:Void->Void) + { + super(); + + onComplete = onComp; + + buttons = DialogButton.CANCEL | "{{Proceed}}"; + defaultButton = "{{Proceed}}"; + + destroyOnClose = true; + } + + public override function validateDialog(button:DialogButton, fn:Bool->Void) + { + if (button == "{{Proceed}}" && onComplete != null) + { + onComplete(); + } + + fn(true); + } +} diff --git a/source/funkin/ui/debug/stageeditor/components/FindObjDialog.hx b/source/funkin/ui/debug/stageeditor/components/FindObjDialog.hx new file mode 100644 index 000000000..affb862fe --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/components/FindObjDialog.hx @@ -0,0 +1,105 @@ +package funkin.ui.debug.stageeditor.components; + +import haxe.ui.containers.dialogs.Dialog; +import funkin.play.stage.StageProp; +import funkin.ui.debug.stageeditor.handlers.AssetDataHandler; +import haxe.ui.components.TextField; +import haxe.ui.components.CheckBox; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/find-object.xml")) +class FindObjDialog extends Dialog +{ + var stageEditorState:StageEditorState; + + var assets:Array = []; + var curSelected:Int = 0; + var field:TextField; + + var checkWord:CheckBox; + var checkCaps:CheckBox; + + override public function new(state:StageEditorState, searchFor:String = "") + { + super(); + + stageEditorState = state; + nameField.text = searchFor; + + this.field = nameField; + this.checkWord = wordCheck; + this.checkCaps = capsCheck; + + field.onChange = function(_) updateIndicator(); + indicator.hide(); + + top = 20; + left = FlxG.width - width - 20; + + buttons = DialogButton.CANCEL | "{{Find Next}}"; + defaultButton = "{{Find Next}}"; + } + + public function updateIndicator() + { + var prevObjCheck = assets[curSelected]; + + assets = []; + + for (ass in stageEditorState.spriteArray) + { + var name = ass.name; + var checkFor = field.text; + + if (!checkCaps.selected) + { + name = name.toLowerCase(); + checkFor = checkFor.toLowerCase(); + } + + if (((name.contains(checkFor) && !checkWord.selected) || (name == checkFor && checkWord.selected)) && ass.visible) assets.push(ass); + } + + if (assets.length > 0 && prevObjCheck == null) + { + stageEditorState.selectedSprite = assets[0]; + } + + if (assets.length > 0) + { + indicator.text = "Selected: " + (assets.indexOf(stageEditorState.selectedSprite) + 1) + " / " + assets.length; + } + else + { + indicator.text = "No Matches Found"; + } + + if (field.text != "" && field.text != null) indicator.show(); + else + indicator.hide(); + } + + public override function validateDialog(button:DialogButton, fn:Bool->Void) + { + var done = true; + + if (button == "{{Find Next}}") + { + done = false; + + if (assets.length > 0) + { + curSelected = assets.indexOf(stageEditorState.selectedSprite); + curSelected++; + + if (curSelected >= assets.length) curSelected = 0; + + stageEditorState.selectedSprite = assets[curSelected]; + indicator.text = "Selected: " + (assets.indexOf(stageEditorState.selectedSprite) + 1) + " / " + assets.length; + + stageEditorState.camFollow.x = assets[curSelected].getMidpoint().x; + stageEditorState.camFollow.y = assets[curSelected].getMidpoint().y; + } + } + fn(done); + } +} diff --git a/source/funkin/ui/debug/stageeditor/components/LoadFromUrlDialog.hx b/source/funkin/ui/debug/stageeditor/components/LoadFromUrlDialog.hx new file mode 100644 index 000000000..18f16a70d --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/components/LoadFromUrlDialog.hx @@ -0,0 +1,70 @@ +package funkin.ui.debug.stageeditor.components; + +import haxe.ui.containers.dialogs.Dialog; +import lime.utils.Bytes; +import haxe.ui.components.TextField; +import openfl.net.URLLoader; +import openfl.net.URLRequest; +import openfl.events.Event; +import openfl.events.IOErrorEvent; +import openfl.events.ProgressEvent; +import openfl.events.SecurityErrorEvent; +import openfl.utils.ByteArray; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/load-url.xml")) +class LoadFromUrlDialog extends Dialog +{ + var urlField:TextField; + var loader:URLLoader; + + override public function new(successCallback:Bytes->Void = null, failCallback:String->Void = null) + { + super(); + destroyOnClose = true; + + loader = new URLLoader(); + loader.dataFormat = BINARY; + + urlField.text = ""; + + loader.addEventListener(Event.COMPLETE, function(event:Event) { + var bytes:Bytes = cast(loader.data, ByteArray); + + if (successCallback != null) successCallback(bytes); + + trace("loaded the image and did success callback"); + + @:privateAccess + loader.__removeAllListeners(); + + hideDialog(DialogButton.CANCEL); + }); + + loader.addEventListener(IOErrorEvent.IO_ERROR, function(event:IOErrorEvent) { + if (failCallback != null) failCallback(urlField.text); + + trace("error with this shit"); + }); + + loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function(event:SecurityErrorEvent) { + if (failCallback != null) failCallback(urlField.text); + + trace("error with this shit"); + }); + + buttons = DialogButton.CANCEL | "{{Load}}"; + defaultButton = "{{Load}}"; + } + + override public function validateDialog(button:DialogButton, fn:Bool->Void) + { + if (button == DialogButton.CANCEL) + { + fn(true); + } + else + { + loader.load(new URLRequest(urlField.text)); + } + } +} diff --git a/source/funkin/ui/debug/stageeditor/components/NewObjDialog.hx b/source/funkin/ui/debug/stageeditor/components/NewObjDialog.hx new file mode 100644 index 000000000..0ad831731 --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/components/NewObjDialog.hx @@ -0,0 +1,90 @@ +package funkin.ui.debug.stageeditor.components; + +import haxe.ui.containers.dialogs.Dialog; +import haxe.ui.components.Link; +import funkin.ui.debug.stageeditor.handlers.AssetDataHandler; +import funkin.save.Save; +import funkin.util.FileUtil; +import lime.ui.FileDialog; +import flixel.FlxG; +import openfl.display.BitmapData; +import haxe.ui.notifications.NotificationType; +import haxe.ui.notifications.NotificationManager; +import funkin.play.stage.StageProp; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/new-object.xml")) +class NewObjDialog extends Dialog +{ + var stageEditorState:StageEditorState; + var bitmap:BitmapData; + + override public function new(state:StageEditorState, img:BitmapData = null) + { + super(); + + stageEditorState = state; + bitmap = img; + + field.onChange = function(_) { + field.removeClasses(["invalid-value", "valid-value"]); + } + + buttons = DialogButton.CANCEL | "{{Create}}"; + defaultButton = "{{Create}}"; + + destroyOnClose = true; + } + + public override function validateDialog(button:DialogButton, fn:Bool->Void) + { + var done = true; + + if (button == "{{Create}}") + { + var objNames = [for (obj in StageEditorState.instance.spriteArray) obj.name]; + + if (field.text == "" || field.text == null || objNames.contains(field.text)) + { + field.swapClass("invalid-value", "valid-value"); + done = false; + NotificationManager.instance.addNotification( + { + title: "Problem Creating an Object", + body: objNames.contains(field.text) ? "Object with the Name " + field.text + " already exists!" : "Invalid Object Name!", + type: NotificationType.Error + }); + } + else + { + var spr = new StageEditorObject(); + + if (bitmap != null) + { + var bitToLoad = stageEditorState.addBitmap(bitmap); + spr.loadGraphic(stageEditorState.bitmaps[bitToLoad]); + } + else + spr.loadGraphic(AssetDataHandler.getDefaultGraphic()); + + spr.name = field.text; + spr.screenCenter(); + spr.zIndex = 0; + + stageEditorState.selectedSprite = spr; + stageEditorState.createAndPushAction(OBJECT_CREATED); + + stageEditorState.add(spr); + stageEditorState.updateArray(); + stageEditorState.saved = false; + + NotificationManager.instance.addNotification( + { + title: "Object Creating Successful", + body: "Successfully created an Object with the Name " + field.text + "!", + type: NotificationType.Success + }); + } + } + fn(done); + } +} diff --git a/source/funkin/ui/debug/stageeditor/components/UserGuideDialog.hx b/source/funkin/ui/debug/stageeditor/components/UserGuideDialog.hx new file mode 100644 index 000000000..190267184 --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/components/UserGuideDialog.hx @@ -0,0 +1,6 @@ +package funkin.ui.debug.stageeditor.components; + +import haxe.ui.containers.dialogs.Dialog; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/user-guide.xml")) +class UserGuideDialog extends Dialog {} diff --git a/source/funkin/ui/debug/stageeditor/components/WelcomeDialog.hx b/source/funkin/ui/debug/stageeditor/components/WelcomeDialog.hx new file mode 100644 index 000000000..c20cc5f8a --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/components/WelcomeDialog.hx @@ -0,0 +1,146 @@ +package funkin.ui.debug.stageeditor.components; + +import haxe.ui.containers.dialogs.Dialog; +import haxe.ui.containers.dialogs.Dialogs; +import haxe.ui.containers.dialogs.MessageBox.MessageBoxType; +import haxe.ui.components.Link; +import funkin.ui.debug.stageeditor.handlers.StageDataHandler; +import funkin.save.Save; +import funkin.util.FileUtil; +import lime.ui.FileDialog; +import flixel.FlxG; +import funkin.input.Cursor; +import funkin.data.stage.StageData; +import funkin.data.stage.StageRegistry; +import funkin.ui.debug.stageeditor.StageEditorState.StageEditorDialogType; + +using funkin.util.tools.FloatTools; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/welcome.xml")) +class WelcomeDialog extends Dialog +{ + var stageEditorState:StageEditorState; + + override public function new(state:StageEditorState) + { + super(); + + stageEditorState = state; + + buttonNew.onClick = function(_) { + stageEditorState.clearAssets(); + stageEditorState.loadDummyData(); + stageEditorState.currentFile = ""; + killDaDialog(); + } + + for (file in Save.instance.stageEditorPreviousFiles) + { + trace(file); + + if (!FileUtil.doesFileExist(file)) continue; // whats the point of loading something that doesnt exist + + var patj = new haxe.io.Path(file); + + var fileText = new Link(); + fileText.percentWidth = 100; + fileText.text = patj.file + "." + patj.ext; + fileText.onClick = function(_) loadFromFilePath(file); + + #if sys + var stat = sys.FileSystem.stat(file); + var sizeInMB = (stat.size / 1000000).round(2); + + fileText.tooltip = "Full Name: " + file + "\nLast Modified: " + stat.mtime.toString() + "\nSize: " + sizeInMB + "MB"; + #end + + contentRecent.addComponent(fileText); + } + + boxDrag.onClick = function(_) FileUtil.browseForSaveFile([FileUtil.FILE_FILTER_FNFS], loadFromFilePath, null, null, "Open Stage Data"); + + var defaultStages = StageRegistry.instance.listBaseGameStageIds(); + defaultStages.sort(funkin.util.SortUtil.alphabetically); + + for (stage in defaultStages) + { + var baseStage = StageRegistry.instance.parseEntryDataWithMigration(stage, StageRegistry.instance.fetchEntryVersion(stage)); + if (baseStage == null) continue; + + var link = new Link(); // this is how the legend of zelda started btw + link.percentWidth = 100; + link.text = baseStage.name; + + link.onClick = function(_) loadFromPreset(baseStage); + + contentPresets.addComponent(link); + } + + FlxG.stage.window.onDropFile.add(loadFromFilePath); + } + + public function loadFromPreset(data:StageData) + { + if (data == null) return; + + if (!stageEditorState.saved) + { + Dialogs.messageBox("This will destroy all of your Unsaved Work.\n\nAre you sure? This cannot be undone.", "Load Stage", MessageBoxType.TYPE_YESNO, true, + function(btn:DialogButton) { + if (btn == DialogButton.YES) + { + stageEditorState.saved = true; + loadFromPreset(data); + } + }); + + return; + } + + stageEditorState.clearAssets(); + stageEditorState.currentFile = ""; + stageEditorState.loadFromDataRaw(data); + killDaDialog(); + } + + public function loadFromFilePath(file:String) + { + if (!stageEditorState.saved) + { + Dialogs.messageBox("This will destroy all of your Unsaved Work.\n\nAre you sure? This cannot be undone.", "Load Stage", MessageBoxType.TYPE_YESNO, true, + function(btn:DialogButton) { + if (btn == DialogButton.YES) + { + stageEditorState.saved = true; + loadFromFilePath(file); + } + }); + + return; + } + + var bytes = FileUtil.readBytesFromPath(file); + + if (bytes == null) + { + stageEditorState.notifyChange("Problem Loading the Stage", "The Stage File could not be loaded.", true); + return; + } + + stageEditorState.clearAssets(); + stageEditorState.currentFile = file; + stageEditorState.unpackShitFromZip(bytes); + killDaDialog(); + } + + function killDaDialog() + { + stageEditorState.updateDialog(StageEditorDialogType.OBJECT); + stageEditorState.updateDialog(StageEditorDialogType.CHARACTER); + stageEditorState.updateDialog(StageEditorDialogType.STAGE); + + FlxG.stage.window.onDropFile.remove(loadFromFilePath); + hide(); + destroy(); + } +} diff --git a/source/funkin/ui/debug/stageeditor/handlers/AssetDataHandler.hx b/source/funkin/ui/debug/stageeditor/handlers/AssetDataHandler.hx new file mode 100644 index 000000000..a6f57bd7c --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/handlers/AssetDataHandler.hx @@ -0,0 +1,208 @@ +package funkin.ui.debug.stageeditor.handlers; + +import flixel.FlxG; +import openfl.display.BitmapData; +import flixel.FlxSprite; +import flixel.util.FlxColor; +import flixel.graphics.frames.FlxAtlasFrames; +import flixel.math.FlxRect; +import openfl.display.BlendMode; +import flixel.math.FlxPoint; +import funkin.data.stage.StageData.StageDataProp; + +using StringTools; + +/** + * Handles the Stage Props and Datas - being able to convert one to the other. + */ +class AssetDataHandler +{ + static var state:StageEditorState; + + public static function init(state:StageEditorState) + { + AssetDataHandler.state = state; + } + + /** + * Turns an Object into Data. + * @param obj the Object whose data to read. + * @param useBitmaps Whether to Save object's BitmapData directly. + * @return Data of the Object + */ + public static function toData(obj:StageEditorObject, useBitmaps:Bool = false):StageEditorObjectData + { + var outputData:StageEditorObjectData = + { + name: obj.name, + assetPath: "", + position: [obj.x, obj.y], + zIndex: obj.zIndex, + isPixel: !obj.antialiasing, + scale: obj.scale.x == obj.scale.y ? Left(obj.scale.x) : Right([obj.scale.x, obj.scale.y]), + alpha: obj.alpha, + danceEvery: obj.animation.getNameList().length > 0 ? obj.danceEvery : 0, + scroll: [obj.scrollFactor.x, obj.scrollFactor.y], + animations: [for (n => d in obj.animDatas) d], + startingAnimation: obj.startingAnimation, + animType: "sparrow", // automatically making sparrow atlases yeah + angle: obj.angle, + blend: obj.blend == null ? "" : Std.string(obj.blend), + color: obj.color.toWebString(), + xmlData: obj.generateXML() + } + + if (useBitmaps) + { + outputData.bitmap = obj.pixels.clone(); + return outputData; + } + + for (name => bit in state.bitmaps) + { + if (areTheseBitmapsEqual(bit, obj.pixels)) + { + outputData.assetPath = name; + return outputData; + } + } + + outputData.assetPath = "#FFFFFF"; + + return outputData; + } + + /** + * Modifies an Object based on the Data. + * @param object Object to modify. Set to null to create a new one. + * @param data The Data used for the Object. + */ + public static function fromData(object:StageEditorObject, data:StageEditorObjectData) + { + if (data.bitmap != null) + { + var bitToLoad = state.addBitmap(data.bitmap.clone()); + object.loadGraphic(state.bitmaps[bitToLoad]); + } + else + { + if (data.animations != null && data.animations.length > 0) // considering we're unpacking we might as well just do this instead of switch + { + object.frames = flixel.graphics.frames.FlxAtlasFrames.fromSparrow(state.bitmaps[data.assetPath].clone(), data.xmlData); + } + else if (data.assetPath.startsWith("#")) + { + object.loadGraphic(getDefaultGraphic()); + object.color = FlxColor.fromString(data.assetPath); + } + else + object.loadGraphic(state.bitmaps[data.assetPath].clone()); + } + + object.name = data.name; + object.setPosition(data.position[0], data.position[1]); + object.zIndex = data.zIndex; + object.antialiasing = !data.isPixel; + object.alpha = data.alpha; + object.danceEvery = data.danceEvery; + object.scrollFactor.set(data.scroll[0], data.scroll[1]); + object.startingAnimation = data.startingAnimation; + object.angle = data.angle; + object.blend = blendFromString(data.blend); + if (!data.assetPath.startsWith("#")) object.color = FlxColor.fromString(data.color); + + // yeah + object.pixelPerfectRender = data.isPixel; + object.pixelPerfectPosition = data.isPixel; + + for (anim in data.animations) + { + object.addAnim(anim.name, anim.prefix, anim.offsets ?? [0, 0], anim.frameIndices ?? [], anim.frameRate ?? 24, anim.looped ?? false, anim.flipX ?? false, + anim.flipY ?? false); + } + + if (object.animation.getNameList().contains(data.startingAnimation)) object.startingAnimation = data.startingAnimation; + + switch (data.scale) + { + case Left(value): + object.scale.set(value, value); + + case Right(values): + object.scale.set(values[0], values[1]); + } + object.updateHitbox(); + + object.playAnim(object.startingAnimation); + + flixel.util.FlxTimer.wait(StageEditorState.TIME_BEFORE_ANIM_STOP, function() { + if (object != null && object.animation.curAnim != null) object.animation.stop(); + }); + + return object; + } + + /** + * Returns a default BitmapData to be used for all the props. + * @return BitmapData + */ + public static function getDefaultGraphic():BitmapData + { + return new FlxSprite().makeGraphic(1, 1, FlxColor.WHITE).pixels.clone(); + } + + /** + * Returns OpenFL's BlendMode based on the Name. + * @param blend the BlendMode Name. + * @return BlendMode + */ + public static function blendFromString(blend:String):BlendMode + { + // originally this was a MASSIVE and I do mean MASSIVE switch case, though then I found out that blendmode already has one implemented + @:privateAccess + return BlendMode.fromString(blend.toLowerCase().trim()); + } + + public static function generateXML(obj:StageEditorObject) + { + // the last check is for if the only frame is the standard graphic frame + if (obj == null || obj.frames.frames.length == 0 || obj.frames.frames[0].name == null) return ""; + + var xml = [ + "", + '', + '' + ].join("\n"); + + for (daFrame in obj.frames.frames) + { + xml += ' \n'; + } + + xml += ""; + return xml; + } + + // I am aware OpenFL has it's own compare bitmap function, though I find this to be better ngl + static function areTheseBitmapsEqual(bitmap1:BitmapData, bitmap2:BitmapData) + { + if (bitmap1.width != bitmap2.width || bitmap1.height != bitmap2.height) return false; + + for (px in 0...bitmap1.width) + { + for (py in 0...bitmap1.height) + { + if (bitmap1.getPixel32(px, py) != bitmap2.getPixel32(px, py)) return false; + } + } + + return true; + } +} + +typedef StageEditorObjectData = +{ + > StageDataProp, + var xmlData:String; + var ?bitmap:BitmapData; +} diff --git a/source/funkin/ui/debug/stageeditor/handlers/StageDataHandler.hx b/source/funkin/ui/debug/stageeditor/handlers/StageDataHandler.hx new file mode 100644 index 000000000..56dc7a775 --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/handlers/StageDataHandler.hx @@ -0,0 +1,365 @@ +package funkin.ui.debug.stageeditor.handlers; + +import funkin.ui.debug.stageeditor.handlers.AssetDataHandler; +import haxe.io.Bytes; +import funkin.util.FileUtil; +import openfl.display.BitmapData; +import haxe.Json; +import haxe.zip.Entry; +import funkin.play.character.BaseCharacter.CharacterType; +import funkin.play.character.BaseCharacter; +import funkin.data.stage.StageData; +import funkin.data.stage.StageData.StageDataCharacter; +import funkin.data.stage.StageRegistry; +import openfl.utils.Assets as OpenFLAssets; +import lime.utils.Assets as LimeAssets; + +using StringTools; + +class StageDataHandler +{ + public static function checkForCharacter(char:BaseCharacter) + return char != null; + + public static function packShitToZip(state:StageEditorState) + { + // step 1: data + var endData:StageData = new StageData(); + endData.name = state.stageName; + endData.cameraZoom = state.stageZoom; + endData.directory = state.stageFolder; + + // step 1 phase 1: object data + var xmlMap:Map = []; + + for (obj in state.spriteArray) + { + var data = obj.toData(false); + endData.props.push( + { + name: data.name, + assetPath: data.assetPath.startsWith("#") ? data.color : data.assetPath, + position: data.position.copy(), + zIndex: data.zIndex, + isPixel: data.isPixel, + scale: data.scale, + alpha: data.alpha, + danceEvery: data.danceEvery, + scroll: data.scroll.copy(), + animations: data.animations, + startingAnimation: data.startingAnimation, + animType: data.animType, + angle: data.angle, + blend: data.blend, + color: data.assetPath.startsWith("#") ? "#FFFFFF" : data.color + }); + + if (!xmlMap.exists(data.assetPath) && data.xmlData != "") xmlMap.set(data.assetPath, data.xmlData); + } + + // step 1 phase 2: character data + endData.characters.bf.zIndex = state.charGroups[CharacterType.BF].zIndex; + endData.characters.dad.zIndex = state.charGroups[CharacterType.DAD].zIndex; + endData.characters.gf.zIndex = state.charGroups[CharacterType.GF].zIndex; + + endData.characters.bf.scale = state.bf.scale.x / state.bf.getBaseScale(); + endData.characters.dad.scale = state.dad.scale.x / state.dad.getBaseScale(); + endData.characters.gf.scale = state.gf.scale.x / state.gf.getBaseScale(); + + endData.characters.bf.cameraOffsets = state.charCamOffsets[CharacterType.BF].copy(); + endData.characters.gf.cameraOffsets = state.charCamOffsets[CharacterType.GF].copy(); + endData.characters.dad.cameraOffsets = state.charCamOffsets[CharacterType.DAD].copy(); + + endData.characters.bf.position = [ + state.bf.feetPosition.x - state.bf.globalOffsets[0], + state.bf.feetPosition.y - state.bf.globalOffsets[1] + ]; + + endData.characters.gf.position = [ + state.gf.feetPosition.x - state.gf.globalOffsets[0], + state.gf.feetPosition.y - state.gf.globalOffsets[1] + ]; + + endData.characters.dad.position = [ + state.dad.feetPosition.x - state.dad.globalOffsets[0], + state.dad.feetPosition.y - state.dad.globalOffsets[1] + ]; + + // step 2: saving everything to entryList + var entryList = new Array(); + + // step 2 phase 1: images + state.removeUnusedBitmaps(); + for (name => img in state.bitmaps) + { + var bytes = img?.image?.encode(PNG); + if (bytes == null) continue; + + var entry:Entry = + { + fileName: name + ".png", + fileSize: bytes.length, + fileTime: Date.now(), + compressed: false, + dataSize: bytes.length, + data: bytes, + crc32: null // apparently fileutil.hx does not like crc32, idk why but i dont even know what crc32 is + } + + entryList.push(entry); + } + + // step 2 phase 2: xmls + for (obj in endData.props) + { + if (!xmlMap.exists(obj.assetPath)) continue; // damn + + var bytes = Bytes.ofString(xmlMap[obj.assetPath]); + + var entry:Entry = + { + fileName: obj.assetPath + ".xml", + fileSize: bytes.length, + fileTime: Date.now(), + compressed: false, + dataSize: bytes.length, + data: bytes, + crc32: null + } + + entryList.push(entry); + } + + // step 2 phase 3: the main data + var stageBytes = Bytes.ofString(endData.serialize()); + entryList.push( + { + fileName: "yourstagename.json", + fileSize: stageBytes.length, + fileTime: Date.now(), + compressed: false, + dataSize: stageBytes.length, + data: stageBytes, + crc32: null + }); + + var zipFileBytes = FileUtil.createZIPFromEntries(entryList); + return zipFileBytes; + } + + public static function unpackShitFromZip(state:StageEditorState, zip:Bytes) + { + state.clearAssets(); + state.bitmaps.clear(); + + var entries = FileUtil.readZIPFromBytes(zip); + var stageData:StageData = new StageData(); + + var xmls:Map = []; + + for (stuff in entries) + { + var ext = stuff.fileName.split(".")[1]; + + switch (ext) + { + case "png": + var data = BitmapData.fromBytes(stuff.data); + state.bitmaps.set(stuff.fileName.replace(".png", ""), data); + + case "xml": + xmls.set(stuff.fileName.replace(".xml", ""), stuff.data.toString()); + + case "json": + stageData = StageRegistry.instance.parseEntryDataRaw(stuff.data.toString(), stuff.fileName); + } + } + + if (stageData == null) + { + // TODO: throw an error, then load a dummy data + loadDummyData(state); + return; + } + + // actual data unpacking + state.stageName = stageData.name; + state.stageZoom = stageData.cameraZoom; + state.stageFolder = stageData.directory ?? "shared"; + + // chars + state.loadCharDatas(stageData); + + // objects + for (objData in stageData.props) + { + // make the data and roll with it + var spr = new StageEditorObject(); + spr.fromData( + { + name: objData.name ?? "Unnamed", + assetPath: objData.assetPath, + animations: objData.animations.copy(), + scale: objData.scale, + position: objData.position, + alpha: objData.alpha, + angle: objData.angle, + zIndex: objData.zIndex, + danceEvery: objData.danceEvery, + isPixel: objData.isPixel, + scroll: objData.scroll.copy(), + color: objData.color, + blend: objData.blend, + startingAnimation: objData.startingAnimation, + xmlData: xmls[objData.assetPath] ?? "" + }); + + state.add(spr); + } + + state.updateArray(); + state.sortAssets(); + state.updateMarkerPos(); + } + + static function loadCharDatas(state:StageEditorState, data:StageData) + { + var chars = state.getCharacters(); + for (char in chars) + { + var charData:StageDataCharacter = null; + + switch (char.characterType) + { + case CharacterType.BF: + charData = data.characters.bf; + case CharacterType.GF: + charData = data.characters.gf; + case CharacterType.DAD: + charData = data.characters.dad; + default: // nothing rip + } + + char.resetCharacter(true); + + if (charData == null) continue; + + char.x = charData.position[0] - char.characterOrigin.x + char.globalOffsets[0]; + char.y = charData.position[1] - char.characterOrigin.y + char.globalOffsets[1]; + state.charGroups[char.characterType].zIndex = charData.zIndex; + + char.setScale(char.getBaseScale() * charData.scale); + char.cameraFocusPoint.x += charData.cameraOffsets[0]; + char.cameraFocusPoint.y += charData.cameraOffsets[1]; + + state.charCamOffsets[char.characterType] = charData.cameraOffsets.copy(); + } + } + + public static function loadFromDataRaw(state:StageEditorState, data:StageData) + { + state.clearAssets(); + state.bitmaps.clear(); + + if (data == null) + { + loadDummyData(state); + return; + } + @:privateAccess + if (!LimeAssets.libraryPaths.exists(data.directory)) + { + loadDummyData(state); + return; + } + + Paths.setCurrentLevel(data.directory); + + if (OpenFLAssets.getLibrary(data.directory) == null) + { + OpenFLAssets.loadLibrary(data.directory).onComplete(function(_) { + loadFromDataRaw(state, data); + }); + return; + } + + state.stageName = data.name; + state.stageZoom = data.cameraZoom; + state.stageFolder = data.directory ?? "shared"; + + state.loadCharDatas(data); + + for (objData in data.props) + { + var spr = new StageEditorObject(); + if (!objData.assetPath.startsWith("#")) state.bitmaps.set(objData.assetPath, Assets.getBitmapData(Paths.image(objData.assetPath))); + + spr.fromData( + { + name: objData.name ?? "Unnamed", + assetPath: objData.assetPath, + animations: objData.animations.copy(), + scale: objData.scale, + position: objData.position, + alpha: objData.alpha, + angle: objData.angle, + zIndex: objData.zIndex, + danceEvery: objData.danceEvery, + isPixel: objData.isPixel, + scroll: objData.scroll.copy(), + color: objData.color, + blend: objData.blend, + startingAnimation: objData.startingAnimation, + xmlData: Assets.exists(Paths.file("images/" + objData.assetPath + ".xml")) ? Assets.getText(Paths.file("images/" + objData.assetPath + ".xml")) : "" + }); + + state.add(spr); + } + + state.updateArray(); + state.sortAssets(); + state.updateMarkerPos(); + } + + public static function loadDummyData(state:StageEditorState) + { + state.clearAssets(); + + state.stageName = "Unnamed"; + state.stageZoom = 1.0; + state.stageFolder = "shared"; + + state.charCamOffsets = StageEditorState.DEFAULT_CAMERA_OFFSETS.copy(); + state.charPos = StageEditorState.DEFAULT_POSITIONS.copy(); + + state.gf.resetCharacter(true); + state.dad.resetCharacter(true); + state.bf.resetCharacter(true); + + state.charGroups[CharacterType.BF].zIndex = 300; + state.charGroups[CharacterType.DAD].zIndex = 200; + state.charGroups[CharacterType.GF].zIndex = 100; + + state.gf.x = state.charPos[CharacterType.GF][0] - state.gf.characterOrigin.x + state.gf.globalOffsets[0]; + state.gf.y = state.charPos[CharacterType.GF][1] - state.gf.characterOrigin.y + state.gf.globalOffsets[1]; + state.dad.x = state.charPos[CharacterType.DAD][0] - state.dad.characterOrigin.x + state.dad.globalOffsets[0]; + state.dad.y = state.charPos[CharacterType.DAD][1] - state.dad.characterOrigin.y + state.dad.globalOffsets[1]; + state.bf.x = state.charPos[CharacterType.BF][0] - state.bf.characterOrigin.x + state.bf.globalOffsets[0]; + state.bf.y = state.charPos[CharacterType.BF][1] - state.bf.characterOrigin.y + state.bf.globalOffsets[1]; + + state.gf.setScale(state.gf.getBaseScale()); + state.dad.setScale(state.dad.getBaseScale()); + state.bf.setScale(state.bf.getBaseScale()); + + state.gf.cameraFocusPoint.x += state.charCamOffsets[CharacterType.GF][0]; + state.gf.cameraFocusPoint.y += state.charCamOffsets[CharacterType.GF][1]; + state.dad.cameraFocusPoint.x += state.charCamOffsets[CharacterType.DAD][0]; + state.dad.cameraFocusPoint.y += state.charCamOffsets[CharacterType.DAD][1]; + state.bf.cameraFocusPoint.x += state.charCamOffsets[CharacterType.BF][0]; + state.bf.cameraFocusPoint.y += state.charCamOffsets[CharacterType.BF][1]; + + // no props :p + + state.updateMarkerPos(); + } +} diff --git a/source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx b/source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx new file mode 100644 index 000000000..947bb5cf3 --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx @@ -0,0 +1,191 @@ +package funkin.ui.debug.stageeditor.handlers; + +import funkin.play.character.BaseCharacter.CharacterType; +import funkin.ui.debug.stageeditor.handlers.AssetDataHandler.StageEditorObjectData; +import funkin.ui.debug.stageeditor.StageEditorState.StageEditorDialogType; + +class UndoRedoHandler +{ + public static function performLastAction(state:StageEditorState, redo:Bool = false) + { + if (state == null || (state.undoArray.length <= 0 && !redo) || (state.redoArray.length <= 0 && redo)) return; + var actionToDo = redo ? state.redoArray.pop() : state.undoArray.pop(); + + switch (actionToDo.type) + { + case CHARACTER_MOVED: + createAndPushAction(state, actionToDo.type, !redo); + + var type = actionToDo.data.type == null ? CharacterType.BF : actionToDo.data.type; + var pos = actionToDo.data.pos == null ? [0, 0] : actionToDo.data.pos; + + for (char in state.getCharacters()) + { + if (char.characterType == type) state.selectedChar = char; + } + + state.selectedChar.x = pos[0] - state.selectedChar.characterOrigin.x + state.selectedChar.globalOffsets[0]; + state.selectedChar.y = pos[1] - state.selectedChar.characterOrigin.y + state.selectedChar.globalOffsets[1]; + + state.updateMarkerPos(); + state.updateDialog(StageEditorDialogType.CHARACTER); + + case OBJECT_MOVED: + var id = actionToDo.data.ID ?? -1; + var pos = actionToDo.data.pos ?? [0, 0]; + + for (obj in state.spriteArray) + { + if (obj.ID == id) state.selectedSprite = obj; + } + + if (state.selectedSprite != null) + { + createAndPushAction(state, actionToDo.type, !redo); + + state.selectedSprite.x = pos[0]; + state.selectedSprite.y = pos[1]; + + state.updateDialog(StageEditorDialogType.OBJECT); + } + + case OBJECT_CREATED: // this removes the object + var id = actionToDo.data.ID ?? -1; + + for (obj in state.spriteArray) + { + if (obj.ID == id) + { + state.selectedSprite = obj; + createAndPushAction(state, OBJECT_DELETED, !redo); + + state.selectedSprite = null; + + obj.kill(); + state.remove(obj, true); + obj.destroy(); + + state.updateArray(); + state.updateDialog(StageEditorDialogType.OBJECT); + trace("found object"); + + continue; + } + } + + case OBJECT_DELETED: // this creates the object + if (actionToDo.data.data == null) return; + + var id = actionToDo.data.ID ?? -1; + var data:StageEditorObjectData = cast actionToDo.data.data; + + var obj = new StageEditorObject().fromData(data); + obj.ID = id; + state.selectedSprite = obj; + + createAndPushAction(state, OBJECT_CREATED, !redo); + state.add(obj); + + state.updateDialog(StageEditorDialogType.OBJECT); + state.updateArray(); + + case OBJECT_ROTATED: // primarily copied from OBJECT_MOVED + var id = actionToDo.data.ID ?? -1; + var angle = actionToDo.data.angle ?? 0; + + for (obj in state.spriteArray) + { + if (obj.ID == id) state.selectedSprite = obj; + } + + if (state.selectedSprite != null) + { + createAndPushAction(state, actionToDo.type, !redo); + state.selectedSprite.angle = angle; + state.updateDialog(StageEditorDialogType.OBJECT); + } + + default: // do nothing dumbass + } + } + + public static function createAndPushAction(state:StageEditorState, action:UndoActionType, redo:Bool = false) + { + if (state == null) return; + + var finalAction:UndoAction = {type: action, data: null}; + + if (!redo && state.redoArray.length > 0) state.redoArray = []; // incorporate resetting as well + + switch (action) + { + case CHARACTER_MOVED: + var char = state.selectedChar.characterType; + finalAction.data = {type: char, pos: state.charPos[char].copy()}; + + case OBJECT_MOVED: + finalAction.data = {ID: state.selectedSprite.ID, pos: [state.selectedSprite.x, state.selectedSprite.y]} + + case OBJECT_CREATED: + finalAction.data = {ID: state.selectedSprite.ID} + + case OBJECT_DELETED: + finalAction.data = + { + ID: state.selectedSprite.ID, + data: state.selectedSprite.toData(true) + } + + case OBJECT_ROTATED: + finalAction.data = {ID: state.selectedSprite.ID, angle: state.selectedSprite.angle} + + default: // nop + } + + if (finalAction.data == null) return; + + if (redo) state.redoArray.push(finalAction); + else if (!redo) state.undoArray.push(finalAction); + } +} + +typedef UndoAction = +{ + /** + * The Type of Undo Action to store. + */ + var type:UndoActionType; + + /** + * The added Data of the Action. + */ + var data:Dynamic; +} + +enum abstract UndoActionType(String) from String +{ + /** + * Triggerred when an Object is deleted. + */ + var OBJECT_DELETED = "object_deleted"; + + /** + * Triggerred when an Object is created. + */ + var OBJECT_CREATED = "object_created"; + + /** + * Triggerred when an Object is moved. + */ + var OBJECT_MOVED = "object_moved"; + + /** + * Triggerred when a Character is moved. + */ + var CHARACTER_MOVED = "character_moved"; + + /** + * Triggerred when an Object is rotated. + */ + var OBJECT_ROTATED = "object_rotated"; +} diff --git a/source/funkin/ui/debug/stageeditor/import.hx b/source/funkin/ui/debug/stageeditor/import.hx new file mode 100644 index 000000000..194f501b6 --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/import.hx @@ -0,0 +1,7 @@ +package funkin.ui.debug.stageeditor; + +#if !macro +using funkin.ui.debug.stageeditor.handlers.StageDataHandler; +using funkin.ui.debug.stageeditor.handlers.AssetDataHandler; +using funkin.ui.debug.stageeditor.handlers.UndoRedoHandler; +#end diff --git a/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorCharacterToolbox.hx b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorCharacterToolbox.hx new file mode 100644 index 000000000..cd01f1fca --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorCharacterToolbox.hx @@ -0,0 +1,242 @@ +package funkin.ui.debug.stageeditor.toolboxes; + +import haxe.ui.components.NumberStepper; +import funkin.play.character.BaseCharacter; +import funkin.play.character.BaseCharacter.CharacterType; +import funkin.play.character.CharacterData.CharacterDataParser; +import funkin.util.SortUtil; +import haxe.ui.data.ArrayDataSource; +import haxe.ui.components.DropDown; +import haxe.ui.components.Button; +import haxe.ui.components.Slider; +import haxe.ui.components.Label; +import funkin.ui.debug.stageeditor.handlers.StageDataHandler; +import haxe.ui.containers.menus.Menu; +import haxe.ui.containers.ScrollView; +import haxe.ui.core.Screen; +import flixel.tweens.FlxTween; +import flixel.tweens.FlxEase; +import haxe.ui.containers.Grid; +import funkin.play.character.CharacterData; + +using StringTools; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/character-properties.xml")) +class StageEditorCharacterToolbox extends StageEditorDefaultToolbox +{ + var characterPosXStepper:NumberStepper; + var characterPosYStepper:NumberStepper; + var characterPosReset:Button; + + var characterZIdxStepper:NumberStepper; + var characterZIdxReset:Button; + + var characterCamXStepper:NumberStepper; + var characterCamYStepper:NumberStepper; + var characterCamReset:Button; + + var characterScaleSlider:Slider; + var characterScaleReset:Button; + + var characterTypeButton:Button; + var charMenu:StageEditorCharacterMenu; + + override public function new(state:StageEditorState) + { + super(state); + + // position + characterPosXStepper.onChange = characterPosYStepper.onChange = function(_) { + repositionCharacter(); + state.saved = false; + } + + characterPosReset.onClick = function(_) { + if (!StageEditorState.DEFAULT_POSITIONS.exists(state.selectedChar.characterType)) return; + + var oldPositions = StageEditorState.DEFAULT_POSITIONS[state.selectedChar.characterType]; + characterPosXStepper.pos = oldPositions[0]; + characterPosYStepper.pos = oldPositions[1]; + } + + // zidx + characterZIdxStepper.max = StageEditorState.MAX_Z_INDEX; + characterZIdxStepper.onChange = function(_) { + state.charGroups[state.selectedChar.characterType].zIndex = Std.int(characterZIdxStepper.pos); + state.saved = false; + state.sortAssets(); + } + + characterZIdxReset.onClick = function(_) { + var thingies = [CharacterType.GF, CharacterType.DAD, CharacterType.BF]; + var thingIdxies = thingies.indexOf(state.selectedChar.characterType); + + characterZIdxStepper.pos = (thingIdxies * 100); + } + + // camera + characterCamXStepper.onChange = characterCamYStepper.onChange = function(_) { + state.charCamOffsets[state.selectedChar.characterType] = [characterCamXStepper.pos, characterCamYStepper.pos]; + state.updateMarkerPos(); + state.saved = false; + } + + characterCamReset.onClick = function(_) characterCamXStepper.pos = characterCamYStepper.pos = 0; // lol + + // scale + characterScaleSlider.onChange = function(_) { + state.selectedChar.setScale(state.selectedChar.getBaseScale() * characterScaleSlider.pos); + repositionCharacter(); + state.saved = false; + } + + characterScaleReset.onChange = function(_) characterScaleSlider.pos = 1; + + // character button + characterTypeButton.onClick = function(_) { + charMenu = new StageEditorCharacterMenu(state, this); + Screen.instance.addComponent(charMenu); + } + + refresh(); + } + + override public function refresh() + { + var name = stageEditorState.selectedChar.characterType; + + characterPosXStepper.step = characterPosYStepper.step = stageEditorState.moveStep; + characterCamXStepper.step = characterCamYStepper.step = stageEditorState.moveStep; + + if (characterPosXStepper.pos != stageEditorState.charPos[name][0]) characterPosXStepper.pos = stageEditorState.charPos[name][0]; + if (characterPosYStepper.pos != stageEditorState.charPos[name][1]) characterPosYStepper.pos = stageEditorState.charPos[name][1]; + + if (characterZIdxStepper.pos != stageEditorState.charGroups[stageEditorState.selectedChar.characterType].zIndex) + characterZIdxStepper.pos = stageEditorState.charGroups[stageEditorState.selectedChar.characterType].zIndex; + + if (characterCamXStepper.pos != stageEditorState.charCamOffsets[name][0]) characterCamXStepper.pos = stageEditorState.charCamOffsets[name][0]; + if (characterCamYStepper.pos != stageEditorState.charCamOffsets[name][1]) characterCamYStepper.pos = stageEditorState.charCamOffsets[name][1]; + + if (characterScaleSlider.pos != stageEditorState.selectedChar.scale.x / stageEditorState.selectedChar.getBaseScale()) + characterScaleSlider.pos = stageEditorState.selectedChar.scale.x / stageEditorState.selectedChar.getBaseScale(); + + var prevText = characterTypeButton.text; + + var charData = CharacterDataParser.fetchCharacterData(stageEditorState.selectedChar.characterId); + characterTypeButton.icon = (charData == null ? null : CharacterDataParser.getCharPixelIconAsset(stageEditorState.selectedChar.characterId)); + characterTypeButton.text = (charData == null ? "None" : charData.name.length > 6 ? '${charData.name.substr(0, 6)}.' : '${charData.name}'); + + if (prevText != characterTypeButton.text) + { + Screen.instance.removeComponent(charMenu); + } + } + + public function repositionCharacter() + { + stageEditorState.selectedChar.x = characterPosXStepper.pos - stageEditorState.selectedChar.characterOrigin.x + + stageEditorState.selectedChar.globalOffsets[0]; + stageEditorState.selectedChar.y = characterPosYStepper.pos - stageEditorState.selectedChar.characterOrigin.y + + stageEditorState.selectedChar.globalOffsets[1]; + + stageEditorState.selectedChar.setScale(stageEditorState.selectedChar.getBaseScale() * characterScaleSlider.pos); + + stageEditorState.updateMarkerPos(); + } +} + +@:xml(' + + + + + +') +class StageEditorCharacterMenu extends Menu // copied from chart editor +{ + override public function new(state:StageEditorState, parent:StageEditorCharacterToolbox) + { + super(); + + this.x = Screen.instance.currentMouseX; + this.y = Screen.instance.currentMouseY; + + var charGrid = new Grid(); + charGrid.columns = 5; + charGrid.width = this.width; + charSelectScroll.addComponent(charGrid); + + var charIds = CharacterDataParser.listCharacterIds(); + charIds.sort(SortUtil.alphabetically); + + var defaultText:String = '(choose a character)'; + + for (charIndex => charId in charIds) + { + var charData:CharacterData = CharacterDataParser.fetchCharacterData(charId); + + var charButton = new haxe.ui.components.Button(); + charButton.width = 70; + charButton.height = 70; + charButton.padding = 8; + charButton.iconPosition = "top"; + + if (charId == state.selectedChar.characterId) + { + // Scroll to the character if it is already selected. + charSelectScroll.hscrollPos = Math.floor(charIndex / 5) * 80; + charButton.selected = true; + + defaultText = '${charData.name} [${charId}]'; + } + + var LIMIT = 6; + charButton.icon = CharacterDataParser.getCharPixelIconAsset(charId); + charButton.text = charData.name.length > LIMIT ? '${charData.name.substr(0, LIMIT)}.' : '${charData.name}'; + + charButton.onClick = _ -> { + var type = state.selectedChar.characterType; + if (state.selectedChar.characterId == charId) return; // saves on memory + + var group = state.charGroups[type]; + group.killMembers(); + for (member in group.members) + { + member.kill(); + group.remove(member, true); + member.destroy(); + } + group.clear(); + + // okay i think that was enough cleaning phew you can see how clean this group is now!!! + // anyways new character!!!! + + var newChar = CharacterDataParser.fetchCharacter(charId, true); + newChar.characterType = type; + + newChar.resetCharacter(true); + newChar.flipX = type == CharacterType.BF ? !newChar.getDataFlipX() : newChar.getDataFlipX(); + + state.selectedChar = newChar; + group.add(newChar); + + parent.repositionCharacter(); + }; + + charButton.onMouseOver = _ -> { + charIconName.text = '${charData.name} [${charId}]'; + }; + charButton.onMouseOut = _ -> { + charIconName.text = defaultText; + }; + charGrid.addComponent(charButton); + } + + charIconName.text = defaultText; + + this.alpha = 0; + this.y -= 10; + FlxTween.tween(this, {alpha: 1, y: this.y + 10}, 0.2, {ease: FlxEase.quartOut}); + } +} diff --git a/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorDefaultToolbox.hx b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorDefaultToolbox.hx new file mode 100644 index 000000000..2e5b3048c --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorDefaultToolbox.hx @@ -0,0 +1,44 @@ +package funkin.ui.debug.stageeditor.toolboxes; + +import haxe.ui.containers.dialogs.CollapsibleDialog; +import funkin.audio.FunkinSound; + +@:access(funkin.ui.debug.stageeditor.StageEditorState) +class StageEditorDefaultToolbox extends CollapsibleDialog +{ + var stageEditorState:StageEditorState; + + public var dialogVisible:Bool = false; + + private function new(stageEditorState:StageEditorState) + { + super(); + + this.stageEditorState = stageEditorState; + + closable = false; + modal = true; + destroyOnClose = false; + } + + /** + * Handles the Sound and Visibility + * @param on + */ + public function toggle(on:Bool) + { + if (!dialogVisible && on) FunkinSound.playOnce(Paths.sound('chartingSounds/openWindow')); + else if (dialogVisible && !on) FunkinSound.playOnce(Paths.sound('chartingSounds/exitWindow')); + + if (on) showDialog(false); + else + hide(); + + dialogVisible = on; + } + + /** + * Override to implement this. + */ + public function refresh() {} +} diff --git a/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorObjectToolbox.hx b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorObjectToolbox.hx new file mode 100644 index 000000000..36dd127ec --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorObjectToolbox.hx @@ -0,0 +1,581 @@ +package funkin.ui.debug.stageeditor.toolboxes; + +import haxe.ui.components.HorizontalSlider; +import haxe.ui.components.NumberStepper; +import haxe.ui.components.TextField; +import haxe.ui.components.TextArea; +import haxe.ui.components.Button; +import haxe.ui.components.Image; +import haxe.ui.containers.dialogs.Dialogs.FileDialogTypes; +import haxe.ui.ToolkitAssets; +import haxe.ui.containers.dialogs.Dialogs; +import funkin.ui.debug.stageeditor.handlers.AssetDataHandler; +import flixel.graphics.frames.FlxAtlasFrames; +import haxe.ui.components.DropDown; +import haxe.ui.containers.ListView; +import haxe.ui.components.CheckBox; +import haxe.ui.components.Switch; +import flixel.util.FlxTimer; +import haxe.ui.data.ArrayDataSource; +import haxe.ui.events.ItemEvent; +import haxe.ui.components.ColorPicker; +import flixel.util.FlxColor; +import haxe.ui.util.Color; +import flixel.graphics.frames.FlxFrame; +import flixel.animation.FlxAnimation; +import funkin.util.FileUtil; +import funkin.ui.debug.stageeditor.components.LoadFromUrlDialog; +import openfl.display.BitmapData; + +using StringTools; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/object-properties.xml")) +class StageEditorObjectToolbox extends StageEditorDefaultToolbox +{ + var linkedObject:StageEditorObject = null; + + var objectImagePreview:Image; + var objectLoadImageButton:Button; + var objectLoadInternetButton:Button; + var objectDownloadImageButton:Button; + var objectResetImageButton:Button; + var objectZIdxStepper:NumberStepper; + var objectZIdxReset:Button; + + var objectPosXStepper:NumberStepper; + var objectPosYStepper:NumberStepper; + var objectPosResetButton:Button; + var objectAlphaSlider:HorizontalSlider; + var objectAlphaResetButton:Button; + var objectAngleSlider:HorizontalSlider; + var objectAngleResetButton:Button; + var objectScaleXStepper:NumberStepper; + var objectScaleYStepper:NumberStepper; + var objectScaleResetButton:Button; + var objectSizeXStepper:NumberStepper; + var objectSizeYStepper:NumberStepper; + var objectSizeResetButton:Button; + var objectScrollXSlider:HorizontalSlider; + var objectScrollYSlider:HorizontalSlider; + var objectScrollResetButton:Button; + + var objectFrameText:TextArea; + var objectFrameTextLoad:Button; + var objectFrameTextSparrow:Button; + var objectFrameTextPacker:Button; + var objectFrameImageWidth:NumberStepper; + var objectFrameImageHeight:NumberStepper; + var objectFrameImageSetter:Button; + var objectFrameReset:Button; + + var objectAnimDropdown:DropDown; + var objectAnimName:TextField; + var objectAnimFrameList:ListView; + var objectAnimPrefix:TextField; + var objectAnimFrames:TextField; + var objectAnimLooped:CheckBox; + var objectAnimFlipX:CheckBox; + var objectAnimFlipY:CheckBox; + var objectAnimFramerate:NumberStepper; + var objectAnimOffsetX:NumberStepper; + var objectAnimOffsetY:NumberStepper; + var objectAnimDanceBeat:NumberStepper; + var objectAnimDanceBeatReset:Button; + var objectAnimStart:TextField; + var objectAnimStartReset:Button; + + var objectMiscAntialias:CheckBox; + var objectMiscAntialiasReset:Button; + var objectMiscFlipReset:Button; + var objectMiscBlendDrop:DropDown; + var objectMiscBlendReset:Button; + var objectMiscColor:ColorPicker; + var objectMiscColorReset:Button; + + override public function new(state:StageEditorState) + { + super(state); + + // basic callbacks + objectLoadImageButton.onClick = function(_) { + if (linkedObject == null) return; + + Dialogs.openBinaryFile("Open Image File", FileDialogTypes.IMAGES, function(selectedFile) { + if (selectedFile == null) return; + + objectImagePreview.resource = null; + + ToolkitAssets.instance.imageFromBytes(selectedFile.bytes, function(imageInfo) { + if (imageInfo == null) return; + + objectImagePreview.resource = imageInfo.data; + + linkedObject.frame = imageInfo.data; + + var bit = linkedObject.updateFramePixels(); + var bitToLoad = state.addBitmap(bit); + + linkedObject.loadGraphic(state.bitmaps[bitToLoad]); + linkedObject.updateHitbox(); + + // update size stuff + objectSizeXStepper.pos = linkedObject.width; + objectSizeYStepper.pos = linkedObject.height; + + // remove unused bitmaps + state.removeUnusedBitmaps(); + }); + }); + } + + objectLoadInternetButton.onClick = function(_) { + if (linkedObject == null) return; + + state.createURLDialog(function(bytes:lime.utils.Bytes) { + linkedObject.loadGraphic(BitmapData.fromBytes(bytes)); + linkedObject.updateHitbox(); + refresh(); + }); + } + + objectDownloadImageButton.onClick = function(_) { + if (linkedObject == null) return; + + FileUtil.saveFile(linkedObject.pixels.image.encode(PNG), [FileUtil.FILE_FILTER_PNG], null, null, + linkedObject.name + "-graphic.png"); // i'on need any callbacks + } + + objectZIdxStepper.max = StageEditorState.MAX_Z_INDEX; + objectZIdxStepper.onChange = function(_) { + if (linkedObject != null) + { + linkedObject.zIndex = Std.int(objectZIdxStepper.pos); + state.sortAssets(); + } + } + + // numeric callbacks + objectPosXStepper.onChange = function(_) { + if (linkedObject != null) linkedObject.x = objectPosXStepper.pos; + }; + + objectPosYStepper.onChange = function(_) { + if (linkedObject != null) linkedObject.y = objectPosYStepper.pos; + }; + + objectAlphaSlider.onChange = function(_) { + if (linkedObject != null) linkedObject.alpha = objectAlphaSlider.pos; + }; + + objectAngleSlider.onChange = function(_) { + if (linkedObject != null) linkedObject.angle = objectAngleSlider.pos; + }; + + objectScaleXStepper.onChange = objectScaleYStepper.onChange = function(_) { + if (linkedObject != null) + { + linkedObject.scale.set(objectScaleXStepper.pos, objectScaleYStepper.pos); + linkedObject.updateHitbox(); + objectSizeXStepper.pos = linkedObject.width; + objectSizeYStepper.pos = linkedObject.height; + + linkedObject.playAnim(linkedObject.animation.name); // load offsets + } + }; + + objectSizeXStepper.onChange = objectSizeYStepper.onChange = function(_) { + if (linkedObject != null) + { + linkedObject.setGraphicSize(Std.int(objectSizeXStepper.pos), Std.int(objectSizeYStepper.pos)); + linkedObject.updateHitbox(); + objectScaleXStepper.pos = linkedObject.scale.x; + objectScaleYStepper.pos = linkedObject.scale.y; + + linkedObject.playAnim(linkedObject.animation.name); // load offsets + } + }; + + objectScrollXSlider.onChange = objectScrollYSlider.onChange = function(_) { + if (linkedObject != null) linkedObject.scrollFactor.set(objectScrollXSlider.pos, objectScrollYSlider.pos); + }; + + // frame callbacks + objectFrameTextLoad.onClick = function(_) { + Dialogs.openTextFile("Open Text File", FileDialogTypes.TEXTS, function(selectedFile) { + if (selectedFile.text == null || (!selectedFile.name.endsWith(".xml") && !selectedFile.name.endsWith(".txt"))) return; + + objectFrameText.text = selectedFile.text; + + state.notifyChange("Frame Text Loaded", "The Text File " + selectedFile.name + " has been loaded."); + }); + } + + objectFrameTextSparrow.onClick = function(_) { + if (linkedObject == null || objectFrameText.text == null || objectFrameText.text == "") return; + + try + { + linkedObject.frames = FlxAtlasFrames.fromSparrow(linkedObject.graphic, objectFrameText.text); + } + catch (e) + { + state.notifyChange("Frame Setup Error", e.toString(), true); + return; + } + + // might as well clear animations because frames SUCK + linkedObject.animDatas.clear(); + linkedObject.animation.destroyAnimations(); + linkedObject.updateHitbox(); + refresh(); + + state.notifyChange("Frame Setup Done", "Finished the Sparrow Frame Setup for the Object " + linkedObject.name + "."); + } + + objectFrameTextPacker.onClick = function(_) { + if (linkedObject == null || objectFrameText.text == null || objectFrameText.text == "") return; + + try // crash prevention + { + linkedObject.frames = FlxAtlasFrames.fromSpriteSheetPacker(linkedObject.graphic, objectFrameText.text); + } + catch (e) + { + state.notifyChange("Frame Setup Error", e.toString(), true); + return; + } + + // might as well clear animations because frames SUCK + linkedObject.animDatas.clear(); + linkedObject.animation.destroyAnimations(); + + linkedObject.updateHitbox(); + refresh(); + + state.notifyChange("Frame Setup Done", "Finished the Packer Frame Setup for the Object " + linkedObject.name + "."); + } + + objectFrameImageSetter.onClick = function(_) { + if (linkedObject == null) return; + + linkedObject.loadGraphic(linkedObject.graphic, true, Std.int(objectFrameImageWidth.pos), Std.int(objectFrameImageHeight.pos)); + linkedObject.updateHitbox(); + + // set da names + for (i in 0...linkedObject.frames.frames.length) + { + linkedObject.frames.framesHash.set("Frame" + i, linkedObject.frames.frames[i]); + linkedObject.frames.frames[i].name = "Frame" + i; + } + + // might as well clear animations because frames SUCK + linkedObject.animDatas.clear(); + linkedObject.animation.destroyAnimations(); + refresh(); + + state.notifyChange("Frame Setup Done", "Finished the Image Frame Setup for the Object " + linkedObject.name + "."); + } + + // animation + objectAnimDropdown.onChange = function(_) { + if (linkedObject == null) return; + + if (objectAnimDropdown.selectedIndex == -1) // RESET EVERYTHING INSTANTENEOUSLY + { + objectAnimName.text = ""; + objectAnimLooped.selected = objectAnimFlipX.selected = objectAnimFlipY.selected = false; + objectAnimFramerate.pos = 24; + objectAnimOffsetX.pos = objectAnimOffsetY.pos = 0; + objectAnimFrames.text = ""; + + return; + } + + var animData = linkedObject.animDatas[objectAnimDropdown.selectedItem.text]; + if (animData == null) return; + + objectAnimName.text = objectAnimDropdown.selectedItem.text; + objectAnimPrefix.text = animData.prefix ?? ""; + objectAnimFrames.text = (animData.frameIndices != null && animData.frameIndices.length > 0 ? animData.frameIndices.join(", ") : ""); + + objectAnimLooped.selected = animData.looped ?? false; + objectAnimFlipX.selected = animData.flipX ?? false; + objectAnimFlipY.selected = animData.flipY ?? false; + objectAnimFramerate.pos = animData.frameRate ?? 24; + + objectAnimOffsetX.pos = (animData.offsets != null && animData.offsets.length == 2 ? animData.offsets[0] : 0); + objectAnimOffsetY.pos = (animData.offsets != null && animData.offsets.length == 2 ? animData.offsets[1] : 0); + } + + objectAnimSave.onClick = function(_) { + if (linkedObject == null) return; + + if (objectAnimName.text == null || objectAnimName.text == "") + { + state.notifyChange("Animation Saving Error", "Invalid Animation Name!", true); + return; + } + + if (objectAnimPrefix.text == null || objectAnimPrefix.text == "") + { + state.notifyChange("Animation Saving Error", "Missing Animation Prefix!", true); + return; + } + + if (linkedObject.animation.getNameList().contains(objectAnimName.text)) linkedObject.animation.remove(objectAnimName.text); + + var indices = []; + + if (objectAnimFrames.text != null && objectAnimFrames.text != "") + { + var splitter = objectAnimFrames.text.replace(" ", "").split(","); + + for (num in splitter) + { + indices.push(Std.parseInt(num)); + } + } + + var shouldDoIndices:Bool = (indices.length > 0 && !indices.contains(null)); + + linkedObject.addAnim(objectAnimName.text, objectAnimPrefix.text, [objectAnimOffsetX.pos, objectAnimOffsetY.pos], (shouldDoIndices ? indices : []), + Std.int(objectAnimFramerate.pos), objectAnimLooped.selected, objectAnimFlipX.selected, objectAnimFlipY.selected); + + if (linkedObject.animation.getByName(objectAnimName.text) == null) + { + state.notifyChange("Animation Saving Error", "Invalid Frames!", true); + return; + } + + linkedObject.playAnim(objectAnimName.text); + + state.notifyChange("Animation Saving Done", "Animation " + objectAnimName.text + " has been saved to the Object " + linkedObject.name + "."); + updateAnimList(); + + // stops the animation preview if animation is looped for too long + FlxTimer.wait(StageEditorState.TIME_BEFORE_ANIM_STOP, function() { + if (linkedObject != null && linkedObject.animation.curAnim != null) + linkedObject.animation.stop(); // null check cuz if we stop an anim for a null object the game crashes :[ + }); + } + + objectAnimDelete.onClick = function(_) { + if (linkedObject == null || linkedObject.animation.getNameList().length <= 0 || objectAnimDropdown.selectedIndex < 0) return; + + linkedObject.animation.pause(); + linkedObject.animation.stop(); + linkedObject.animation.curAnim = null; + + var daAnim = linkedObject.animation.getNameList()[objectAnimDropdown.selectedIndex]; + + linkedObject.animation.remove(daAnim); + linkedObject.animDatas.remove(daAnim); + linkedObject.offset.set(); + + state.notifyChange("Animation Deletion Done", + "Animation " + + objectAnimDropdown.selectedItem.text + + " has been removed from the Object " + + linkedObject.name + + "."); + + updateAnimList(); + + objectAnimDropdown.selectedIndex = objectAnimDropdown.dataSource.size - 1; + } + + objectAnimDanceBeat.onChange = function(_) { + if (linkedObject != null) linkedObject.danceEvery = Std.int(objectAnimDanceBeat.pos); + } + + objectAnimStart.onChange = function(_) { + if (linkedObject != null) + { + if (linkedObject.animation.getNameList().contains(objectAnimStart.text)) objectAnimStart.styleString = "color: white"; + else + objectAnimStart.styleString = "color: indianred"; + + linkedObject.startingAnimation = objectAnimStart.text; + } + } + + // misc + objectMiscAntialias.onClick = function(_) { + if (linkedObject != null) linkedObject.antialiasing = objectMiscAntialias.selected; + } + + objectMiscBlendDrop.onChange = function(_) { + if (linkedObject != null) + linkedObject.blend = objectMiscBlendDrop.selectedItem.text == "NONE" ? null : AssetDataHandler.blendFromString(objectMiscBlendDrop.selectedItem.text); + } + + objectMiscColor.onChange = function(_) { + if (linkedObject != null) linkedObject.color = FlxColor.fromRGB(objectMiscColor.currentColor.r, objectMiscColor.currentColor.g, + objectMiscColor.currentColor.b); + } + + // reset button callbacks + objectResetImageButton.onClick = function(_) { + if (linkedObject != null) + { + linkedObject.loadGraphic(AssetDataHandler.getDefaultGraphic()); + linkedObject.updateHitbox(); + refresh(); + + // remove unused bitmaps + state.removeUnusedBitmaps(); + } + } + + objectZIdxReset.onClick = function(_) { + if (linkedObject != null) objectZIdxStepper.pos = 0; // corner cutting because onChange will activate with this + } + + objectPosResetButton.onClick = function(_) { + if (linkedObject != null) + { + linkedObject.screenCenter(); + objectPosXStepper.pos = linkedObject.x; + objectPosYStepper.pos = linkedObject.y; + } + } + + objectAlphaResetButton.onClick = function(_) { + if (linkedObject != null) linkedObject.alpha = objectAlphaSlider.pos = 1; + } + + objectAngleResetButton.onClick = function(_) { + if (linkedObject != null) linkedObject.angle = objectAngleSlider.pos = 0; + } + + objectScaleResetButton.onClick = objectSizeResetButton.onClick = function(_) // the corner cutting goes crazy + { + if (linkedObject != null) + { + linkedObject.scale.set(1, 1); + refresh(); // refreshes like multiple shit + } + } + + objectScrollResetButton.onClick = function(_) { + if (linkedObject != null) linkedObject.scrollFactor.x = linkedObject.scrollFactor.y = objectScrollXSlider.pos = objectScrollYSlider.pos = 1; + } + + objectFrameReset.onClick = function(_) { + if (linkedObject == null) return; + + linkedObject.loadGraphic(linkedObject.pixels); + linkedObject.animDatas.clear(); + linkedObject.animation.destroyAnimations(); + refresh(); + } + + objectMiscAntialiasReset.onClick = function(_) { + if (linkedObject != null) objectMiscAntialias.selected = true; + } + + objectMiscBlendReset.onClick = function(_) { + if (linkedObject != null) objectMiscBlendDrop.selectedItem = "NORMAL"; + } + + objectMiscColorReset.onClick = function(_) { + if (linkedObject != null) objectMiscColor.currentColor = Color.fromString("white"); + } + + objectAnimDanceBeatReset.onClick = function(_) { + if (linkedObject != null) objectAnimDanceBeat.pos = 0; + } + + objectAnimStartReset.onClick = function(_) { + if (linkedObject != null) objectAnimStart.text = ""; + } + + refresh(); + } + + var prevFrames:Array = []; + var prevAnims:Array = []; + + override public function refresh() + { + linkedObject = stageEditorState.selectedSprite; + + objectPosXStepper.step = stageEditorState.moveStep; + objectPosYStepper.step = stageEditorState.moveStep; + objectAngleSlider.step = funkin.save.Save.instance.stageEditorAngleStep; + + if (linkedObject == null) + { + updateFrameList(); + updateAnimList(); + return; + } + + // saving fps + if (objectImagePreview.resource != linkedObject.frame) objectImagePreview.resource = linkedObject.frame; + + if (objectZIdxStepper.pos != linkedObject.zIndex) objectZIdxStepper.pos = linkedObject.zIndex; + if (objectPosXStepper.pos != linkedObject.x) objectPosXStepper.pos = linkedObject.x; + if (objectPosYStepper.pos != linkedObject.y) objectPosYStepper.pos = linkedObject.y; + if (objectAlphaSlider.pos != linkedObject.alpha) objectAlphaSlider.pos = linkedObject.alpha; + if (objectAngleSlider.pos != linkedObject.angle) objectAngleSlider.pos = linkedObject.angle; + if (objectScaleXStepper.pos != linkedObject.scale.x) objectScaleXStepper.pos = linkedObject.scale.x; + if (objectScaleYStepper.pos != linkedObject.scale.y) objectScaleYStepper.pos = linkedObject.scale.y; + if (objectSizeXStepper.pos != linkedObject.width) objectSizeXStepper.pos = linkedObject.width; + if (objectSizeYStepper.pos != linkedObject.height) objectSizeYStepper.pos = linkedObject.height; + if (objectScrollXSlider.pos != linkedObject.scrollFactor.x) objectScrollXSlider.pos = linkedObject.scrollFactor.x; + if (objectScrollYSlider.pos != linkedObject.scrollFactor.y) objectScrollYSlider.pos = linkedObject.scrollFactor.y; + if (objectMiscAntialias.selected != linkedObject.antialiasing) objectMiscAntialias.selected = linkedObject.antialiasing; + + if (objectMiscColor.currentColor != Color.fromString(linkedObject.color.toHexString() ?? "white")) + objectMiscColor.currentColor = Color.fromString(linkedObject.color.toHexString()); + + if (objectAnimDanceBeat.pos != linkedObject.danceEvery) objectAnimDanceBeat.pos = linkedObject.danceEvery; + if (objectAnimStart.text != linkedObject.startingAnimation) objectAnimStart.text = linkedObject.startingAnimation; + + var objBlend = Std.string(linkedObject.blend) ?? "NONE"; + if (objectMiscBlendDrop.selectedItem != objBlend.toUpperCase()) objectMiscBlendDrop.selectedItem = objBlend.toUpperCase(); + + // ough the max + if (objectFrameImageWidth.max != linkedObject.pixels.width) objectFrameImageWidth.max = linkedObject.graphic.width; + if (objectFrameImageHeight.max != linkedObject.pixels.height) objectFrameImageHeight.max = linkedObject.graphic.height; + + // update some anim shit + if (prevFrames != linkedObject.frames.frames.copy()) updateFrameList(); + if (prevAnims != linkedObject.animation.getNameList().copy()) updateAnimList(); + } + + function updateFrameList() + { + prevFrames = []; + objectAnimFrameList.dataSource = new ArrayDataSource(); + + if (linkedObject == null) return; + + for (fname in linkedObject.frames.frames) + { + if (fname != null) objectAnimFrameList.dataSource.add({name: fname.name, tooltip: fname.name}); + + prevFrames.push(fname); + } + } + + function updateAnimList() + { + objectAnimDropdown.dataSource.clear(); + prevAnims = []; + if (linkedObject == null) return; + + for (aname in linkedObject.animation.getNameList()) + { + objectAnimDropdown.dataSource.add({text: aname}); + prevAnims.push(aname); + } + + if (linkedObject.animation.getNameList().contains(objectAnimStart.text)) objectAnimStart.styleString = "color: white"; + else + objectAnimStart.styleString = "color: indianred"; + + linkedObject.startingAnimation = objectAnimStart.text; + } +} diff --git a/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorStageToolbox.hx b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorStageToolbox.hx new file mode 100644 index 000000000..5d7770c0c --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorStageToolbox.hx @@ -0,0 +1,60 @@ +package funkin.ui.debug.stageeditor.toolboxes; + +import haxe.ui.components.NumberStepper; +import haxe.ui.components.TextField; +import haxe.ui.components.DropDown; +import funkin.util.SortUtil; + +@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/stage-settings.xml")) +class StageEditorStageToolbox extends StageEditorDefaultToolbox +{ + var stageNameText:TextField; + var stageZoomStepper:NumberStepper; + var stageLibraryDrop:DropDown; + + override public function new(state:StageEditorState) + { + super(state); + + stageNameText.onChange = function(_) { + state.stageName = stageNameText.text; + state.saved = false; + } + + stageZoomStepper.onChange = function(_) { + state.stageZoom = stageZoomStepper.pos; + state.updateMarkerPos(); + state.saved = false; + } + + final EXCLUDE_LIBS = ["art", "default", "vlc", "videos", "songs"]; + var allLibs = []; + + @:privateAccess + { + for (lib => idk in lime.utils.Assets.libraryPaths) + { + if (!EXCLUDE_LIBS.contains(lib)) allLibs.push(lib); + } + } + allLibs.sort(SortUtil.alphabetically); // this system is VERY stupid, it relies on the possibility that the future libraries will be named week(end)[x] + + for (lib in allLibs) + { + stageLibraryDrop.dataSource.add({text: lib}); + } + + stageLibraryDrop.onChange = function(_) { + state.stageFolder = stageLibraryDrop.selectedItem.text; + } + + refresh(); + } + + override public function refresh() + { + stageNameText.text = stageEditorState.stageName; + stageZoomStepper.pos = stageEditorState.stageZoom; + stageLibraryDrop.selectedItem = stageEditorState.stageFolder; + } +} diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx index a4fee0f8d..26bc9e304 100644 --- a/source/funkin/ui/transition/LoadingState.hx +++ b/source/funkin/ui/transition/LoadingState.hx @@ -86,7 +86,7 @@ class LoadingState extends MusicBeatSubState } checkLibrary('shared'); - checkLibrary(PlayStatePlaylist.campaignId); + checkLibrary(stageDirectory); checkLibrary('tutorial'); var fadeTime:Float = 0.5; @@ -204,6 +204,8 @@ class LoadingState extends MusicBeatSubState return Paths.inst(PlayState.instance.currentSong.id); } + static var stageDirectory:String = "shared"; + /** * Starts the transition to a new `PlayState` to start a new song. * First switches to the `LoadingState` if assets need to be loaded. @@ -213,7 +215,13 @@ class LoadingState extends MusicBeatSubState */ public static function loadPlayState(params:PlayStateParams, shouldStopMusic = false, asSubState = false, ?onConstruct:PlayState->Void):Void { - Paths.setCurrentLevel(PlayStatePlaylist.campaignId); + var daChart = params.targetSong.getDifficulty(params.targetDifficulty ?? Constants.DEFAULT_DIFFICULTY, + params.targetVariation ?? Constants.DEFAULT_VARIATION); + + var daStage = funkin.data.stage.StageRegistry.instance.fetchEntry(daChart.stage); + stageDirectory = daStage?._data?.directory ?? "shared"; + Paths.setCurrentLevel(stageDirectory); + var playStateCtor:() -> PlayState = function() { return new PlayState(params); }; diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx index 00a0a14b7..5b053bb08 100644 --- a/source/funkin/util/FileUtil.hx +++ b/source/funkin/util/FileUtil.hx @@ -22,6 +22,7 @@ class FileUtil public static final FILE_FILTER_JSON:FileFilter = new FileFilter("JSON Data File (.json)", "*.json"); public static final FILE_FILTER_ZIP:FileFilter = new FileFilter("ZIP Archive (.zip)", "*.zip"); public static final FILE_FILTER_PNG:FileFilter = new FileFilter("PNG Image (.png)", "*.png"); + public static final FILE_FILTER_FNFS:FileFilter = new FileFilter("Friday Night Funkin' Stage (.fnfs)", "*.fnfs"); public static final FILE_EXTENSION_INFO_FNFC:FileDialogExtensionInfo = { @@ -39,6 +40,12 @@ class FileUtil label: 'PNG Image', }; + public static final FILE_EXTENSION_INFO_FNFS:FileDialogExtensionInfo = + { + extension: 'fnfs', + label: 'Friday Night Funkin\' Stage', + }; + /** * Browses for a single file, then calls `onSelect(fileInfo)` when a file is selected. * Powered by HaxeUI, so it works on all platforms.