diff --git a/assets/editors/layouts/stage/spriteEditScreen.xml b/assets/editors/layouts/stage/spriteEditScreen.xml new file mode 100644 index 00000000..5b10371f --- /dev/null +++ b/assets/editors/layouts/stage/spriteEditScreen.xml @@ -0,0 +1,67 @@ + + + + + + + + <textbox name="nameTextBox" label="Name Identifier" text="${data.getName()}" x="20" y="title.y + TO" width="200" height="32" /> + <textbox name="spriteTextBox" label="Image File (${stage.spritesParentFolder})" text="${getEx('imageFile')}" x="last.x + last.bWidth + XOFFSET" y="title.y + TO" width="200" height="32" /> + + <set name="curY" value="nameTextBox.y + nameTextBox.bHeight + YOFFSET" /> + + <stepper name="xStepper" label="X" value="sprite.x" x="20" y="curY" step="1" width="100" /> + <stepper name="yStepper" label="Y" value="sprite.y" x="last.x + last.bWidth + XOFFSET - 20" y="curY" step="1" width="100" /> + + <stepper name="scrollXStepper" label="Scroll X" value="sprite.scrollFactor.x" x="spriteTextBox.x" y="curY" step="0.01" width="100" /> + <stepper name="scrollYStepper" label="Scroll Y" value="sprite.scrollFactor.y" x="last.x + last.bWidth + XOFFSET - 20" y="curY" step="0.01" width="100" /> + + <stepper name="scaleXStepper" label="Scale X" value="sprite.scale.x" x="20 + (200 + XOFFSET) * 2" y="curY" step="0.01" width="100" /> + <stepper name="scaleYStepper" label="Scale Y" value="sprite.scale.y" x="last.x + last.bWidth + XOFFSET - 20" y="curY" step="0.01" width="100" /> + + <stepper name="skewXStepper" label="Skew X" value="sprite.skew.x" x="20 + (200 + XOFFSET) * 3" y="curY" step="0.01" width="100" /> + <stepper name="skewYStepper" label="Skew Y" value="sprite.skew.y" x="last.x + last.bWidth + XOFFSET - 20" y="curY" step="0.01" width="100" /> + + <set name="curY" value="xStepper.y + xStepper.bHeight + YOFFSET" /> + + <stepper name="angleStepper" label="Angle" value="sprite.angle" x="20 + (200 + XOFFSET) * 0" y="curY" step="0.01" width="100" /> + <stepper name="zoomFactorStepper" label="Zoom Factor" value="sprite.zoomFactor" x="20 + (200 + XOFFSET) * 1" y="curY" step="0.01" width="100" /> + + <dropdown name="animType" label="Animation Type" value="['NONE','BEAT','LOOP'][sprite.spriteAnimType]" x="20 + (200 + XOFFSET) * 2" y="curY" width="100" height="32" options="NONE,BEAT,LOOP" /> + + <exec> + function onSave() { + sprite.x = xStepper.value; + sprite.y = yStepper.value; + sprite.zoomFactor = zoomFactorStepper.value; + //sprite.antialiasing = antialiasingCheckbox.checked; + //sprite.color = colorWheel.color; + sprite.spriteAnimType = animType.index; + + function saveXY(obj:Dynamic, name:String, x:UINumericStepper, y:UINumericStepper) { + data.xml.remove(name + ""); + data.xml.remove(name + "x"); + data.xml.remove(name + "y"); + + obj.x = x.value; + obj.y = y.value; + + if(x.value == y.value) + data.xml.set(name, x.value); + else { + data.xml.set(name + "x", x.value); + data.xml.set(name + "y", y.value); + } + } + + data.xml.set("x", xStepper.value); + data.xml.set("y", yStepper.value); + data.xml.set("zoom", zoomFactorStepper.value); + data.xml.set("type", ['NONE','BEAT','LOOP'][animType.index]); + + saveXY(sprite.scrollFactor, "scroll", scrollXStepper, scrollYStepper); + saveXY(sprite.scale, "scale", scaleXStepper, scaleYStepper); + saveXY(sprite.skew, "skew", skewXStepper, skewYStepper); + } + </exec> +</window> \ No newline at end of file diff --git a/source/funkin/editors/stage/StageEditor.hx b/source/funkin/editors/stage/StageEditor.hx index cf7fe342..86cf70a1 100644 --- a/source/funkin/editors/stage/StageEditor.hx +++ b/source/funkin/editors/stage/StageEditor.hx @@ -28,8 +28,9 @@ class StageEditor extends UIState { public var uiGroup:FlxTypedGroup<FlxSprite> = new FlxTypedGroup<FlxSprite>(); public var stageSpritesWindow:UIButtonList<StageElementButton>; + public var stageSprites:FlxTypedGroup<StageSprite> = new FlxTypedGroup<StageSprite>(); - public var xmlMap:Map<FlxBasic, Access> = new Map<FlxBasic, Access>(); + public var xmlMap:Map<FlxObject, Access> = new Map<FlxObject, Access>(); public var chars:Array<Character> = []; public var charMap:Map<String, Character> = []; @@ -38,6 +39,8 @@ class StageEditor extends UIState { public var guideCamera:FlxCamera; public var uiCamera:FlxCamera; + public static var selection:Selection; + public var showOutlines:Bool = true; public var showCharacters:Bool = true; @@ -168,11 +171,15 @@ class StageEditor extends UIState { } } stage.onNodeLoaded = function(node:Access, sprite:Dynamic):Dynamic { + var name = ""; if(sprite is FlxSprite) { //sprite.forceIsOnScreen = true; // hack } if(sprite is FunkinSprite) { + var sprite:FunkinSprite = cast sprite; + name = sprite.name; sprite.extra.set(exID("type"), node.name); + sprite.extra.set(exID("imageFile"), '${node.getAtt("sprite").getDefault(sprite.name)}'); //sprite.active = false; } if(sprite is StageCharPos) { @@ -183,6 +190,7 @@ class StageEditor extends UIState { case "girlfriend": "gf"; default: charPos.name; } + name = charPos.name; var char = new Character(0,0, charName, stage.isCharFlipped(charPos.name, charName == "bf"), true); char.debugMode = true; // Play first anim, and make it the last frame @@ -204,6 +212,11 @@ class StageEditor extends UIState { } order.push(sprite); xmlMap.set(sprite, node); + + //var t = new StageSprite(name, sprite); + //t.node = node; + //stageSprites.add(t); + return sprite; } stage.loadXml(stage.stageXML, true); @@ -217,6 +230,12 @@ class StageEditor extends UIState { charMap[charPos.name] = char; } + add(stageSprites); + + for(spr in order) { + + } + setZoom(stage.defaultZoom); topMenuSpr = new UITopMenu(topMenu); @@ -595,4 +614,129 @@ class StageEditor extends UIState { enum StageChange { CEditInfo(oldInfo:Xml, newInfo:Xml); +} + +@:forward abstract Selection(Array<StageSprite>) from Array<StageSprite> to Array<StageSprite> { + public inline function new(?array:Array<StageSprite>) + this = array == null ? [] : array; + + // too lazy to put this in every for loop so i made it a abstract + //public inline function loop(onNote:CharterNote->Void, ?onEvent:CharterEvent->Void, ?draggableOnly:Bool = true) { + // for (s in this) { + // if (s is CharterNote && onNote != null && (draggableOnly ? s.draggable: true)) + // onNote(cast(s, CharterNote)); + // else if (s is CharterEvent && onEvent != null && (draggableOnly ? s.draggable: true)) + // onEvent(cast(s, CharterEvent)); + // } + //} +} + +class StageSprite extends UISprite { + public var tracker:FlxObject; + public var funkinTracker:FunkinSprite; + public var node:Access; + + public var name:String; + + public var extra:Map<String, Dynamic> = new Map<String, Dynamic>(); + + public var scrollX:Float; + public var scrollY:Float; + public var scaleX:Float; + public var scaleY:Float; + public var zoomFactor:Float = 1; // TODO: implement this + public var skewX:Float; + public var skewY:Float; + public var spriteAnimType:XMLAnimType; + + public var skipNegativeBeats:Bool = false; + public var beatInterval:Int = 0; + public var beatOffset:Int = 0; + + public var lowMemory:Bool; + public var highMemory:Bool; + + public var selected:Bool = false; + public var draggable:Bool = false; + + public function new(name:String, sprite:FlxObject) { + super(x, y); + this.name = name; + this.tracker = sprite; + + scrollX = tracker.scrollFactor.x; + scrollY = tracker.scrollFactor.y; + if(tracker is FunkinSprite) { + var tracker:FunkinSprite = cast tracker; + scaleX = tracker.scale.x; + scaleY = tracker.scale.y; + funkinTracker = tracker; + } + else if (tracker is StageCharPos) { + var tracker:StageCharPos = cast tracker; + var char:Character = tracker.extra.get(StageEditor.exID("char")); + scaleX = tracker.scale.x; + scaleY = tracker.scale.y; + funkinTracker = char; + } + + lowMemory = node.x.parent.nodeName == "low-memory"; + highMemory = node.x.parent.nodeName == "high-memory"; + + if(funkinTracker != null) { + skewX = funkinTracker.skew.x; + skewY = funkinTracker.skew.y; + alpha = funkinTracker.alpha; + spriteAnimType = funkinTracker.spriteAnimType; + antialiasing = funkinTracker.antialiasing; + width = funkinTracker.width; + height = funkinTracker.height; + color = funkinTracker.color; + + skipNegativeBeats = funkinTracker.skipNegativeBeats; + beatInterval = funkinTracker.beatInterval; + beatOffset = funkinTracker.beatOffset; + } + + } + + public override function update(elapsed:Float) { + super.update(elapsed); + if (tracker.exists) { + tracker.x = x; + tracker.y = y; + tracker.scrollFactor.set(scrollX, scrollY); + if(tracker is FlxSprite) { + var tracker:FlxSprite = cast tracker; + } + else if (tracker is StageCharPos) { + var tracker:StageCharPos = cast tracker; + tracker.skewX = skewX; + tracker.skewY = skewY; + } + + if(funkinTracker != null) { + funkinTracker.scale.set(scaleX, scaleY); + funkinTracker.skew.set(skewX, skewY); + funkinTracker.alpha = alpha; + funkinTracker.spriteAnimType = spriteAnimType; + funkinTracker.antialiasing = antialiasing; + funkinTracker.width = width; + funkinTracker.height = height; + + funkinTracker.skipNegativeBeats = skipNegativeBeats; + funkinTracker.beatInterval = beatInterval; + funkinTracker.beatOffset = beatOffset; + + funkinTracker.color = color; + } + } + } + + public function handleSelection(selectionBox:UISliceSprite):Bool { + return false; + }; + public function handleDrag(change:FlxPoint):Void { + + }; } \ No newline at end of file diff --git a/source/funkin/editors/stage/StageSpriteEditScreen.hx b/source/funkin/editors/stage/StageSpriteEditScreen.hx new file mode 100644 index 00000000..3559044d --- /dev/null +++ b/source/funkin/editors/stage/StageSpriteEditScreen.hx @@ -0,0 +1,286 @@ +package funkin.editors.stage; + +import funkin.backend.scripting.HScript; +import hscript.Interp; +import haxe.xml.Access; +import flixel.math.FlxPoint; +import funkin.backend.chart.ChartData.ChartMetaData; +import funkin.editors.extra.PropertyButton; +import funkin.editors.stage.elements.StageElementButton; +import hscript.*; +import hscript.Expr.Error; +import hscript.Parser; + +using StringTools; + +class StageSpriteEditScreen extends UISubstateWindow { + public var data:StageElementButton; + public var sprite:FunkinSprite; + + public var saveButton:UIButton; + public var closeButton:UIButton; + + var elementMap:Map<String, UISprite> = []; + + + public function new(data:StageElementButton) { + super(); + this.data = data; + } + + var parser = HScript.initParser(); + var interp = new Interp(); + + function set(name:String, value:Dynamic) { + interp.variables.set(name, value); + } + + function get(name:String):Dynamic { + return interp.variables.get(name); + } + + public override function create() { + var layout = new Access(Xml.parse(Assets.getText('assets/editors/layouts/stage/spriteEditScreen.xml')).firstElement()); + winTitle = layout.getAtt("title").getDefault(winTitle); + winWidth = Std.parseInt(layout.getAtt("width")).getDefault(winWidth); + winHeight = Std.parseInt(layout.getAtt("height")).getDefault(winHeight); + + sprite = data.getSprite(); + + set("stage", StageEditor.instance.stage); + set("sprite", sprite); + set("data", data); + set("getEx", function(name:String):Dynamic { + return sprite.extra.get(StageEditor.exID(name)); + }); + set("last", null); + + function _errorHandler(error:Error) { + var fileName = error.origin; + var fn = '$fileName:${error.line}: '; + var err = error.toString(); + if (err.startsWith(fn)) err = err.substr(fn.length); + + Logs.traceColored([ + Logs.logText(fn, GREEN), + Logs.logText(err, RED) + ], ERROR); + } + + function exec(code:String):Dynamic { + try { + if (code != null && code.trim() != "") + return interp.execute(parser.parseString(code, "spriteEditScreen.xml")); + } catch(e:Error) { + _errorHandler(e); + } catch(e) { + _errorHandler(new Error(ECustom(e.toString()), 0, 0, "spriteEditScreen.xml", 0)); + } + return null; + } + + super.create(); + + function addLabelOn(ui:UISprite, text:String) + add(new UIText(ui.x, ui.y - 24, 0, text)); + + function execString(code:String):String { + if(code.indexOf("${") == -1) return code; + var text = ""; + while(code.length > 0) { + var idx = code.indexOf("${"); + if(idx == -1) { + text += code; + break; + } + text += code.substring(0, idx); + var ei = code.indexOf("}", idx); + if(ei == -1) { + text += code.substring(idx); + break; + } + var block = code.substring(idx+2, ei); + text += exec(block); + code = code.substring(ei + 1); + } + return text; + } + + function execDefault(code:String, defaultValue:Dynamic):Dynamic { + return CoolUtil.getDefault(exec(code), defaultValue); + } + + function execAtt(el:Access, key:String, defaultValue:Dynamic):Dynamic { + return execDefault(el.getAtt(key), defaultValue); + } + + for(el in layout.elements) { + var type = el.name; + //var type:String = el.getAtt("type"); + var name:String = el.getAtt("name"); + var label:String = execString(el.getAtt("label")); + var text:String = execString(el.getAtt("text")); + var x:Float = execAtt(el, "x", 0); + var y:Float = execAtt(el, "y", 0); + var width:Null<Int> = exec(el.getAtt("width")); + var height:Null<Int> = exec(el.getAtt("height")); + var size:Int = execAtt(el, "size", 28); + + trace(type, name, label, text, x, y, width, height, size); + + var sprite:FlxSprite = switch(type) { + case "set": + set(el.getAtt("name"), exec(el.getAtt("value"))); + continue; + case "image": + var spr = XMLUtil.createSpriteFromXML(el, "", LOOP); + cast add(spr); + case "exec": + exec(el.innerData); + continue; + case "textbox": + var textBox = new UITextBox(x, y, text, width, height, (el.getAtt("multiline").getDefault("false") == "true")); + add(textBox); + addLabelOn(textBox, label); + textBox; + case "title": + cast add(new UIText(x, y, 0, label, size)); + case "stepper": + // x:Float, y:Float, value:Float = 0, step:Float = 1, precision:Int = 0, ?min:Float, ?max:Float, w:Int = 180, h:Int = 32 + var stepper = new UINumericStepper( + x, + y, + execAtt(el, "value", 0), + execAtt(el, "step", 1), + execAtt(el, "precision", 0), + execAtt(el, "min", null), + execAtt(el, "max", null), + width, + height + ); + add(stepper); + addLabelOn(stepper, label); + stepper; + case "checkbox": + var checkbox = new UICheckbox(x, y, label, execAtt(el, "value", false)); + add(checkbox); + addLabelOn(checkbox, label); + checkbox; + case "slider": + var slider = new UISlider(x, y, width, execAtt(el, "value", 0), execAtt(el, "segments", []), execAtt(el, "centered", false)); + add(slider); + addLabelOn(slider, label); + slider; + case "dropdown": + var options = execString(el.getAtt("options")).split(","); + var curValue = execAtt(el, "value", null); + var index = options.indexOf(curValue); + if (index == -1) index = 0; + var dropdown = new UIDropDown(x, y, width, height, options, index); + add(dropdown); + addLabelOn(dropdown, label); + dropdown; + // UIColorwheel + // UIAudioPlayer + // UIButtonList + // UIFileExplorer + // UIAutoCompleteTextbox + default: { + Logs.trace("Unknown element type: " + type, ERROR, RED); + Logs.trace("Skipping element: " + el.x.toString(), ERROR, RED); + continue; + } + } + + set("last", sprite); + if(name != null) set(name, sprite); + if(sprite is UISprite) elementMap.set(name, cast sprite); + } + + saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20, windowSpr.y + windowSpr.bHeight - 20, "Save & Close", function() { + saveData(); + close(); + }, 125); + saveButton.x -= saveButton.bWidth; + saveButton.y -= saveButton.bHeight; + + closeButton = new UIButton(saveButton.x - 20, saveButton.y, "Close", function() { + close(); + }, 125); + closeButton.color = 0xFFFF0000; + closeButton.x -= closeButton.bWidth; + //closeButton.y -= closeButton.bHeight; + add(closeButton); + add(saveButton); + } + + /*function updateIcon(icon:String) { + if (iconSprite == null) add(iconSprite = new FlxSprite()); + + if (iconSprite.animation.exists(icon)) return; + @:privateAccess iconSprite.animation.clearAnimations(); + + var path:String = Paths.image('icons/$icon'); + if (!Assets.exists(path)) path = Paths.image('icons/face'); + + iconSprite.loadGraphic(path, true, 150, 150); + iconSprite.animation.add(icon, [0], 0, false); + iconSprite.antialiasing = true; + iconSprite.animation.play(icon); + + iconSprite.scale.set(0.5, 0.5); + iconSprite.updateHitbox(); + iconSprite.setPosition(iconTextBox.x + 150 + 8, (iconTextBox.y + 16) - (iconSprite.height/2)); + }*/ + + function getElement(name:String):UISprite { + return elementMap.get(name); + } + + function callFunc(name:String, args:Array<Dynamic>) { + if (interp == null) return null; + if (!interp.variables.exists(name)) return null; + + var func = interp.variables.get(name); + if (func != null && Reflect.isFunction(func)) + return Reflect.callMethod(null, func, args); + + return null; + } + + public function saveData() { + for (el in elementMap) { + if(el is UINumericStepper) { + var el:UINumericStepper = cast el; + @:privateAccess el.__onChange(el.label.text); + } + } + + var name = cast(getElement("nameTextBox"), UITextBox).label.text; + var imageFile = cast(getElement("spriteTextBox"), UITextBox).label.text; + + // Save in sprite + sprite.name = name; + sprite.extra.set(StageEditor.exID("imageFile"), imageFile); + + // Save in xml + data.xml.x.set("name", name); + data.xml.x.set("sprite", imageFile); + + callFunc("onSave", []); + + data.updateInfo(sprite); + } +} + +typedef Element = { + var type:ElementType; + var label:String; + var ?name:String; + var ?text:String; +} + +enum abstract ElementType(Int) { + var TITLE = 0; + var TEXTBOX = 1; +} \ No newline at end of file diff --git a/source/funkin/editors/stage/elements/StageCharacterButton.hx b/source/funkin/editors/stage/elements/StageCharacterButton.hx index 57dc4e9d..538ae0eb 100644 --- a/source/funkin/editors/stage/elements/StageCharacterButton.hx +++ b/source/funkin/editors/stage/elements/StageCharacterButton.hx @@ -36,6 +36,10 @@ class StageCharacterButton extends StageElementButton { super.updateInfo(charPos); } + public override function getSprite():FunkinSprite { + return char; + } + public override function onSelect() { } @@ -45,8 +49,12 @@ class StageCharacterButton extends StageElementButton { updateInfo(this.charPos); } + public override function getName():String { + return charPos.name; + } + public override function getInfoText():String { - return '${charPos.name} (${charPos.x}, ${charPos.y})'; + return '${getName()} (${charPos.x}, ${charPos.y})'; } public override function updatePos() { diff --git a/source/funkin/editors/stage/elements/StageElementButton.hx b/source/funkin/editors/stage/elements/StageElementButton.hx index a834e653..dc21dcc5 100644 --- a/source/funkin/editors/stage/elements/StageElementButton.hx +++ b/source/funkin/editors/stage/elements/StageElementButton.hx @@ -90,6 +90,14 @@ class StageElementButton extends UIButton { deleteIcon.x = deleteButton.x + (15/2); deleteIcon.y = deleteButton.y + 8; } + public function getSprite():FunkinSprite { + return null; + } + + public function getName():String { + return "UNKNOWN"; + } + public function onSelect() { // TODO: implement } diff --git a/source/funkin/editors/stage/elements/StageSolidButton.hx b/source/funkin/editors/stage/elements/StageSolidButton.hx index 31eeba1d..698069e8 100644 --- a/source/funkin/editors/stage/elements/StageSolidButton.hx +++ b/source/funkin/editors/stage/elements/StageSolidButton.hx @@ -5,7 +5,7 @@ import flixel.util.FlxColor; class StageSolidButton extends StageSpriteButton { - public function new(x:Float,y:Float, sprite:FlxSprite, xml:Access) { + public function new(x:Float,y:Float, sprite:FunkinSprite, xml:Access) { super(x,y, sprite, xml); color = 0xFFD9FF50; } diff --git a/source/funkin/editors/stage/elements/StageSpriteButton.hx b/source/funkin/editors/stage/elements/StageSpriteButton.hx index 425ffd2d..0e509389 100644 --- a/source/funkin/editors/stage/elements/StageSpriteButton.hx +++ b/source/funkin/editors/stage/elements/StageSpriteButton.hx @@ -1,12 +1,13 @@ package funkin.editors.stage.elements; import haxe.xml.Access; +import funkin.editors.stage.StageSpriteEditScreen; import flixel.util.FlxColor; class StageSpriteButton extends StageElementButton { - public var sprite:FlxSprite; + public var sprite:FunkinSprite; - public function new(x:Float,y:Float, sprite:FlxSprite, xml:Access) { + public function new(x:Float,y:Float, sprite:FunkinSprite, xml:Access) { this.sprite = sprite; super(x,y, xml); @@ -20,14 +21,18 @@ class StageSpriteButton extends StageElementButton { } public override function updateInfo(sprite:Dynamic) { - if(sprite is FlxSprite) { - var sprite:FlxSprite = cast sprite; + if(sprite is FunkinSprite) { + var sprite:FunkinSprite = cast sprite; this.sprite = sprite; sprite.visible = !isHidden; } super.updateInfo(sprite); } + public override function getSprite():FunkinSprite { + return sprite; + } + public override function onGhostClick() { isHidden = !isHidden; updateInfo(this.sprite); @@ -35,6 +40,7 @@ class StageSpriteButton extends StageElementButton { public override function onEdit() { // TODO: implement + FlxG.state.openSubState(new StageSpriteEditScreen(this)); } public override function onDelete() { @@ -44,8 +50,12 @@ class StageSpriteButton extends StageElementButton { StageEditor.instance.stageSpritesWindow.remove(this); } + public override function getName():String { + return xml.att.name; + } + public override function getInfoText():String { - return '${xml.att.name} (${sprite.x}, ${sprite.y})'; + return '${getName()} (${sprite.x}, ${sprite.y})'; } public override function updatePos() { diff --git a/source/funkin/game/Stage.hx b/source/funkin/game/Stage.hx index db547bfe..b04c8c13 100644 --- a/source/funkin/game/Stage.hx +++ b/source/funkin/game/Stage.hx @@ -32,7 +32,7 @@ class Stage extends FlxBasic implements IBeatReceiver { public var onNodeFinished:(Access, Dynamic)->Void = null; public var onAddSprite:(FlxObject)->Void = null; - private var spritesParentFolder = ""; + public var spritesParentFolder = ""; public inline function getSprite(name:String) { return stageSprites[name];