package funkin.ui.debug.anim; import funkin.util.SerializerUtil; import funkin.play.character.CharacterData; import flixel.FlxCamera; import flixel.FlxSprite; import flixel.FlxState; import flixel.addons.display.FlxGridOverlay; import flixel.addons.ui.FlxInputText; import flixel.addons.ui.FlxUIDropDownMenu; import flixel.graphics.frames.FlxAtlasFrames; import flixel.graphics.frames.FlxFrame; import flixel.group.FlxGroup; import flixel.math.FlxPoint; import flixel.sound.FlxSound; import flixel.text.FlxText; import flixel.util.FlxColor; import funkin.util.MouseUtil; import flixel.util.FlxSpriteUtil; import flixel.util.FlxTimer; import funkin.play.character.BaseCharacter; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.character.SparrowCharacter; import haxe.ui.RuntimeComponentBuilder; import haxe.ui.components.DropDown; import haxe.ui.core.Component; import haxe.ui.events.ItemEvent; import haxe.ui.events.UIEvent; import funkin.ui.mainmenu.MainMenuState; import lime.utils.Assets as LimeAssets; import openfl.Assets; import openfl.events.Event; import openfl.events.IOErrorEvent; import openfl.geom.Rectangle; import openfl.net.FileReference; import openfl.net.URLLoader; import funkin.ui.mainmenu.MainMenuState; import openfl.net.URLRequest; import openfl.utils.ByteArray; import funkin.input.Cursor; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.util.SortUtil; import haxe.ui.core.Screen; using flixel.util.FlxSpriteUtil; #if web import js.html.FileList; #end #if sys import sys.io.File; #end class DebugBoundingState extends FlxState { /* TODAY'S TO-DO - Cleaner UI */ var bg:FlxSprite; var fileInfo:FlxText; var txtGrp:FlxGroup; var hudCam:FlxCamera; var curView:ANIMDEBUGVIEW = SPRITESHEET; var spriteSheetView:FlxGroup; var offsetView:FlxGroup; var animDropDownMenu:FlxUIDropDownMenu; var dropDownSetup:Bool = false; var onionSkinChar:FlxSprite; var txtOffsetShit:FlxText; var uiStuff:Component; var haxeUIFocused(get, default):Bool = false; function get_haxeUIFocused():Bool { // get the screen position, according to the HUD camera, temp default to FlxG.camera juuust in case? var hudMousePos:FlxPoint = FlxG.mouse.getScreenPosition(hudCam ?? FlxG.camera); return Screen.instance.hasSolidComponentUnderPoint(hudMousePos.x, hudMousePos.y); } override function create() { Paths.setCurrentLevel('week1'); // lv. // lv.onChange = function(e:UIEvent) // { // trace(e.type); // // trace(e.data.curView); // // var item:haxe.ui.core.ItemRenderer = cast e.target; // trace(e.target); // // if (e.type == "change") // // { // // curView = cast e.data; // // } // }; hudCam = new FlxCamera(); hudCam.bgColor.alpha = 0; bg = FlxGridOverlay.create(10, 10); // bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.GREEN); bg.scrollFactor.set(); add(bg); // we are setting this as the default draw camera only temporarily, to trick haxeui FlxG.cameras.add(hudCam); var str = Paths.xml('ui/animation-editor/offset-editor-view'); uiStuff = RuntimeComponentBuilder.fromAsset(str); // uiStuff.findComponent("btnViewSpriteSheet").onClick = _ -> curView = SPRITESHEET; var dropdown:DropDown = cast uiStuff.findComponent("swapper"); dropdown.onChange = function(e:UIEvent) { trace(e.type); curView = cast e.data.curView; trace(e.data); // trace(e.data); }; uiStuff.cameras = [hudCam]; add(uiStuff); // sets the default camera back to FlxG.camera, since we set it to hudCamera for haxeui stuf FlxG.cameras.setDefaultDrawTarget(FlxG.camera, true); FlxG.cameras.setDefaultDrawTarget(hudCam, false); initSpritesheetView(); initOffsetView(); Cursor.show(); super.create(); } var bf:FlxSprite; var swagOutlines:FlxSprite; function initSpritesheetView():Void { spriteSheetView = new FlxGroup(); add(spriteSheetView); var tex = Paths.getSparrowAtlas('characters/BOYFRIEND'); // tex.frames[0].uv bf = new FlxSprite(); bf.loadGraphic(tex.parent); spriteSheetView.add(bf); swagOutlines = new FlxSprite().makeGraphic(tex.parent.width, tex.parent.height, FlxColor.TRANSPARENT); generateOutlines(tex.frames); txtGrp = new FlxGroup(); txtGrp.cameras = [hudCam]; spriteSheetView.add(txtGrp); addInfo('boyfriend.xml', ""); addInfo('Width', bf.width); addInfo('Height', bf.height); spriteSheetView.add(swagOutlines); FlxG.stage.window.onDropFile.add(function(path:String) { // WACKY ASS TESTING SHIT FOR WEB FILE LOADING?? #if web var swagList:FileList = cast path; var objShit = js.html.URL.createObjectURL(swagList.item(0)); trace(objShit); var funnysound = new FlxSound().loadStream('https://cdn.discordapp.com/attachments/767500676166451231/817821618251759666/Flutter.mp3', false, false, null, function() { trace('LOADED SHIT??'); }); funnysound.volume = 1; funnysound.play(); var urlShit = new URLLoader(new URLRequest(objShit)); new FlxTimer().start(3, function(tmr:FlxTimer) { // music lol! if (urlShit.dataFormat == BINARY) { // var daSwagBytes:ByteArray = urlShit.data; // FlxG.sound.playMusic(); // trace('is binary!!'); } trace(urlShit.dataFormat); }); // remove(bf); // FlxG.bitmap.removeByKey(Paths.image('characters/temp')); // Assets.cache.clear(); // bf.loadGraphic(objShit); // add(bf); // trace(swagList.item(0).name); // var urlShit = js.html.URL.createObjectURL(path); #end #if sys trace("DROPPED FILE FROM: " + Std.string(path)); var newPath = "./" + Paths.image('characters/temp'); File.copy(path, newPath); var swag = Paths.image('characters/temp'); if (bf != null) remove(bf); FlxG.bitmap.removeByKey(Paths.image('characters/temp')); Assets.cache.clear(); bf.loadGraphic(Paths.image('characters/temp')); add(bf); #end }); } function generateOutlines(frameShit:Array):Void { // swagOutlines.width = frameShit[0].parent.width; // swagOutlines.height = frameShit[0].parent.height; swagOutlines.pixels.fillRect(new Rectangle(0, 0, swagOutlines.width, swagOutlines.height), 0x00000000); for (i in frameShit) { var lineStyle:LineStyle = {color: FlxColor.RED, thickness: 2}; var uvW:Float = (i.uv.width * i.parent.width) - (i.uv.x * i.parent.width); var uvH:Float = (i.uv.height * i.parent.height) - (i.uv.y * i.parent.height); // trace(Std.int(i.uv.width * i.parent.width)); swagOutlines.drawRect(i.uv.x * i.parent.width, i.uv.y * i.parent.height, uvW, uvH, FlxColor.TRANSPARENT, lineStyle); // swagGraphic.setPosition(, ); // trace(uvH); } } function initOffsetView():Void { offsetView = new FlxGroup(); add(offsetView); onionSkinChar = new FlxSprite().makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.TRANSPARENT); onionSkinChar.visible = false; offsetView.add(onionSkinChar); txtOffsetShit = new FlxText(20, 20, 0, "", 20); txtOffsetShit.setFormat(Paths.font("vcr.ttf"), 26, FlxColor.WHITE, LEFT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK); txtOffsetShit.cameras = [hudCam]; offsetView.add(txtOffsetShit); animDropDownMenu = new FlxUIDropDownMenu(0, 0, FlxUIDropDownMenu.makeStrIdLabelArray(['weed'], true)); animDropDownMenu.cameras = [hudCam]; // Move to bottom right corner animDropDownMenu.x = FlxG.width - animDropDownMenu.width - 20; animDropDownMenu.y = FlxG.height - animDropDownMenu.height - 20; offsetView.add(animDropDownMenu); var characters:Array = CharacterDataParser.listCharacterIds(); characters = characters.filter(function(charId:String) { var char = CharacterDataParser.fetchCharacterData(charId); return char.renderType != AnimateAtlas; }); characters.sort(SortUtil.alphabetically); var charDropdown:DropDown = cast uiStuff.findComponent('characterDropdown'); for (char in characters) { charDropdown.dataSource.add({text: char}); } charDropdown.onChange = function(e:UIEvent) { loadAnimShit(e.data.text); }; } public var mouseOffset:FlxPoint = FlxPoint.get(0, 0); public var oldPos:FlxPoint = FlxPoint.get(0, 0); function mouseOffsetMovement() { if (swagChar != null) { if (FlxG.mouse.justPressed) { mouseOffset.set(FlxG.mouse.x - -swagChar.animOffsets[0], FlxG.mouse.y - -swagChar.animOffsets[1]); } if (FlxG.mouse.pressed) { swagChar.animOffsets = [(FlxG.mouse.x - mouseOffset.x) * -1, (FlxG.mouse.y - mouseOffset.y) * -1]; swagChar.animationOffsets.set(animDropDownMenu.selectedLabel, swagChar.animOffsets); txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets; } } } function addInfo(str:String, value:Dynamic) { var swagText:FlxText = new FlxText(10, 10 + (28 * txtGrp.length)); swagText.setFormat(Paths.font("vcr.ttf"), 26, FlxColor.WHITE, LEFT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK); swagText.scrollFactor.set(); txtGrp.add(swagText); swagText.text = str + ": " + Std.string(value); } function clearInfo() { txtGrp.clear(); } function checkLibrary(library:String) { trace(Assets.hasLibrary(library)); if (Assets.getLibrary(library) == null) { @:privateAccess if (!LimeAssets.libraryPaths.exists(library)) throw "Missing library: " + library; // var callback = callbacks.add("library:" + library); Assets.loadLibrary(library).onComplete(function(_) { trace('LOADED... awesomeness...'); // callback(); }); } } override function update(elapsed:Float) { if (FlxG.keys.justPressed.ONE) { var lv:DropDown = cast uiStuff.findComponent("swapper"); lv.selectedIndex = 0; curView = SPRITESHEET; } if (FlxG.keys.justReleased.TWO) { var lv:DropDown = cast uiStuff.findComponent("swapper"); lv.selectedIndex = 1; curView = ANIMATIONS; if (swagChar != null) { FlxG.camera.focusOn(swagChar.getMidpoint()); FlxG.camera.zoom = 0.95; } } switch (curView) { case SPRITESHEET: spriteSheetView.visible = true; offsetView.visible = false; offsetView.active = false; case ANIMATIONS: spriteSheetView.visible = false; offsetView.visible = true; offsetView.active = true; offsetControls(); if (!haxeUIFocused) mouseOffsetMovement(); } if (FlxG.keys.justPressed.H) hudCam.visible = !hudCam.visible; if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState()); MouseUtil.mouseCamDrag(); if (!haxeUIFocused) MouseUtil.mouseWheelZoom(); // bg.scale.x = FlxG.camera.zoom; // bg.scale.y = FlxG.camera.zoom; bg.setGraphicSize(Std.int(bg.width / FlxG.camera.zoom)); super.update(elapsed); } function offsetControls():Void { if (FlxG.keys.justPressed.RBRACKET || FlxG.keys.justPressed.E) { if (Std.parseInt(animDropDownMenu.selectedId) + 1 <= animDropDownMenu.length) animDropDownMenu.selectedId = Std.string(Std.parseInt(animDropDownMenu.selectedId) + 1); else animDropDownMenu.selectedId = Std.string(0); playCharacterAnimation(animDropDownMenu.selectedId, true); } if (FlxG.keys.justPressed.LBRACKET || FlxG.keys.justPressed.Q) { if (Std.parseInt(animDropDownMenu.selectedId) - 1 >= 0) animDropDownMenu.selectedId = Std.string(Std.parseInt(animDropDownMenu.selectedId) - 1); else animDropDownMenu.selectedId = Std.string(animDropDownMenu.length - 1); playCharacterAnimation(animDropDownMenu.selectedId, true); } // Keyboards controls for general WASD "movement" // modifies the animDropDownMenu so that it's properly updated and shit // and then it's just played and updated from the animDropDownMenu callback, which is set in the loadAnimShit() function probabbly if (FlxG.keys.justPressed.W || FlxG.keys.justPressed.S || FlxG.keys.justPressed.D || FlxG.keys.justPressed.A) { var suffix:String = ''; var targetLabel:String = ''; if (FlxG.keys.pressed.SHIFT) suffix = 'miss'; if (FlxG.keys.justPressed.W) targetLabel = 'singUP$suffix'; if (FlxG.keys.justPressed.S) targetLabel = 'singDOWN$suffix'; if (FlxG.keys.justPressed.A) targetLabel = 'singLEFT$suffix'; if (FlxG.keys.justPressed.D) targetLabel = 'singRIGHT$suffix'; if (targetLabel != animDropDownMenu.selectedLabel) { // Play the new animation if the IDs are the different. // Override the onion skin. animDropDownMenu.selectedLabel = targetLabel; playCharacterAnimation(animDropDownMenu.selectedId, true); } else { // Replay the current animation if the IDs are the same. // Don't override the onion skin. playCharacterAnimation(animDropDownMenu.selectedId, false); } } if (FlxG.keys.justPressed.F) { onionSkinChar.visible = !onionSkinChar.visible; } // Plays the idle animation if (FlxG.keys.justPressed.SPACE) { animDropDownMenu.selectedLabel = 'idle'; playCharacterAnimation(animDropDownMenu.selectedId, true); } // Playback the animation if (FlxG.keys.justPressed.ENTER) playCharacterAnimation(animDropDownMenu.selectedId, false); if (FlxG.keys.justPressed.RIGHT || FlxG.keys.justPressed.LEFT || FlxG.keys.justPressed.UP || FlxG.keys.justPressed.DOWN) { var animName = animDropDownMenu.selectedLabel; var coolValues:Array = swagChar.animationOffsets.get(animName).copy(); var multiplier:Int = 5; if (FlxG.keys.pressed.CONTROL) multiplier = 1; if (FlxG.keys.pressed.SHIFT) multiplier = 10; if (FlxG.keys.justPressed.RIGHT) coolValues[0] -= 1 * multiplier; else if (FlxG.keys.justPressed.LEFT) coolValues[0] += 1 * multiplier; else if (FlxG.keys.justPressed.UP) coolValues[1] += 1 * multiplier; else if (FlxG.keys.justPressed.DOWN) coolValues[1] -= 1 * multiplier; swagChar.animationOffsets.set(animDropDownMenu.selectedLabel, coolValues); swagChar.playAnimation(animName); txtOffsetShit.text = 'Offset: ' + coolValues; trace(animName); } if (FlxG.keys.justPressed.ESCAPE) { var outputString = FlxG.keys.pressed.CONTROL ? buildOutputStringOld() : buildOutputStringNew(); saveOffsets(outputString, FlxG.keys.pressed.CONTROL ? swagChar.characterId + "Offsets.txt" : swagChar.characterId + ".json"); } } function buildOutputStringOld():String { var outputString:String = ""; for (i in swagChar.animationOffsets.keys()) { outputString += i + " " + swagChar.animationOffsets.get(i)[0] + " " + swagChar.animationOffsets.get(i)[1] + "\n"; } outputString.trim(); return outputString; } function buildOutputStringNew():String { var charData:CharacterData = Reflect.copy(swagChar._data); for (charDataAnim in charData.animations) { var animName:String = charDataAnim.name; charDataAnim.offsets = swagChar.animationOffsets.get(animName); } return SerializerUtil.toJSON(charData, true); } var swagChar:BaseCharacter; /* Called when animation dropdown is changed! */ function loadAnimShit(char:String) { if (swagChar != null) { offsetView.remove(swagChar); swagChar.destroy(); } swagChar = CharacterDataParser.fetchCharacter(char); swagChar.x = 100; swagChar.y = 100; // swagChar.debugMode = true; offsetView.add(swagChar); generateOutlines(swagChar.frames.frames); bf.pixels = swagChar.pixels; clearInfo(); addInfo(swagChar._data.assetPath, ""); addInfo('Width', bf.width); addInfo('Height', bf.height); characterAnimNames = []; for (i in swagChar.animationOffsets.keys()) { characterAnimNames.push(i); trace(i); trace(swagChar.animationOffsets[i]); } animDropDownMenu.setData(FlxUIDropDownMenu.makeStrIdLabelArray(characterAnimNames, true)); animDropDownMenu.callback = function(str:String) { playCharacterAnimation(str, true); }; txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets; dropDownSetup = true; } private var characterAnimNames:Array; function playCharacterAnimation(str:String, setOnionSkin:Bool = true) { if (setOnionSkin) { // clears the canvas onionSkinChar.pixels.fillRect(new Rectangle(0, 0, FlxG.width * 2, FlxG.height * 2), 0x00000000); onionSkinChar.stamp(swagChar, Std.int(swagChar.x), Std.int(swagChar.y)); onionSkinChar.alpha = 0.6; } var animName = characterAnimNames[Std.parseInt(str)]; swagChar.playAnimation(animName, true); // trace(); trace(swagChar.animationOffsets.get(animName)); txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets; } var _file:FileReference; function saveOffsets(saveString:String, fileName:String) { if ((saveString != null) && (saveString.length > 0)) { _file = new FileReference(); _file.addEventListener(Event.COMPLETE, onSaveComplete); _file.addEventListener(Event.CANCEL, onSaveCancel); _file.addEventListener(IOErrorEvent.IO_ERROR, onSaveError); _file.save(saveString,); } } function onSaveComplete(_):Void { _file.removeEventListener(Event.COMPLETE, onSaveComplete); _file.removeEventListener(Event.CANCEL, onSaveCancel); _file.removeEventListener(IOErrorEvent.IO_ERROR, onSaveError); _file = null; FlxG.log.notice("Successfully saved LEVEL DATA."); } /** * Called when the save file dialog is cancelled. */ function onSaveCancel(_):Void { _file.removeEventListener(Event.COMPLETE, onSaveComplete); _file.removeEventListener(Event.CANCEL, onSaveCancel); _file.removeEventListener(IOErrorEvent.IO_ERROR, onSaveError); _file = null; } /** * Called if there is an error while saving the gameplay recording. */ function onSaveError(_):Void { _file.removeEventListener(Event.COMPLETE, onSaveComplete); _file.removeEventListener(Event.CANCEL, onSaveCancel); _file.removeEventListener(IOErrorEvent.IO_ERROR, onSaveError); _file = null; FlxG.log.error("Problem saving Level data"); } } enum abstract ANIMDEBUGVIEW(String) { var SPRITESHEET; var ANIMATIONS; }