diff --git a/assets/data/editors/layouts/stage/characterEditScreen.xml b/assets/data/editors/layouts/stage/characterEditScreen.xml
new file mode 100644
index 00000000..22b64ac5
--- /dev/null
+++ b/assets/data/editors/layouts/stage/characterEditScreen.xml
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+ function col(c:Int) {
+ return 20 + (150 + XOFFSET) * c;
+ }
+ previewSprite = null;
+ if(char.animateAtlas == null) {
+ previewSprite = new FunkinSprite(0, 0);
+ previewSprite.frame = char.frame;
+
+ self.add(previewSprite);
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ self.windowSpr.bHeight -= 170;
+
+
+
+
+ function onUpdate() {
+ if(previewSprite == null) return;
+ previewSprite.skew.x = skewXStepper.value;
+ previewSprite.skew.y = skewYStepper.value;
+ previewSprite.angle = angleStepper.value;
+ previewSprite.antialiasing = char.antialiasing;
+ var ratio = char.frameWidth / char.frameHeight;
+ previewSprite.setGraphicSize(145 * scaleXStepper.value * ratio, 145 * scaleYStepper.value);
+ previewSprite.updateHitbox();
+ if(flipXCheckbox.checked != (charPos.name == "boyfriend"))
+ previewSprite.scale.x *= -1;
+
+ previewSprite.x = 30;
+ previewSprite.y = self.winHeight - previewSprite.height - 30;
+ }
+
+ function onSave() {
+ setEx("highMemory", highMemoryRadio.checked);
+ setEx("lowMemory", lowMemoryRadio.checked);
+
+ charPos.x = xStepper.value;
+ charPos.y = yStepper.value;
+ charPos.scrollFactor.set(scrollXStepper.value, scrollYStepper.value);
+ charPos.scale.set(scaleXStepper.value, scaleYStepper.value);
+ charPos.camxoffset = camxStepper.value;
+ charPos.camyoffset = camyStepper.value;
+ charPos.skewX = skewXStepper.value;
+ charPos.skewY = skewYStepper.value;
+ charPos.alpha = alphaStepper.value;
+ charPos.charSpacingX = spacingXStepper.value;
+ charPos.charSpacingY = spacingYStepper.value;
+ charPos.flipX = flipXCheckbox.checked;
+ charPos.zoomFactor = zoomFactorStepper.value;
+ charPos.angle = angleStepper.value;
+
+ charPos.revertCharacter(char);
+ charPos.prepareCharacter(char, charPos._id);
+ }
+
+
\ No newline at end of file
diff --git a/assets/data/editors/layouts/stage/spriteEditScreen.xml b/assets/data/editors/layouts/stage/spriteEditScreen.xml
new file mode 100644
index 00000000..651756a0
--- /dev/null
+++ b/assets/data/editors/layouts/stage/spriteEditScreen.xml
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+
+ function col(c:Int) {
+ return 20 + (200 + XOFFSET) * c;
+ }
+
+ if(sprite.animateAtlas == null) {
+ previewSprite = new FunkinSprite(50, self.winHeight - 100);
+ previewSprite.frame = sprite.frame;
+
+ self.add(previewSprite);
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ buttonlist.addButton.exists = false;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ function onUpdate() {
+ if(previewSprite == null) return;
+ previewSprite.color = 0xFFFFFFFF; // TODO: Color Wheel
+ previewSprite.skew.x = skewXStepper.value;
+ previewSprite.skew.y = skewYStepper.value;
+ previewSprite.angle = angleStepper.value;
+ previewSprite.alpha = alphaStepper.value;
+ previewSprite.antialiasing = antialiasingCheckbox.checked;
+ var ratio = sprite.width / sprite.height;
+ previewSprite.setGraphicSize(50 * scaleXStepper.value * ratio, 50 * scaleYStepper.value);
+ previewSprite.updateHitbox();
+ }
+
+ function onSave() {
+ sprite.name = nameTextBox.label.text;
+ setEx("imageFile", spriteTextBox.label.text);
+
+ setEx("highMemory", highMemoryRadio.checked);
+ setEx("lowMemory", lowMemoryRadio.checked);
+
+ //sprite.x = xStepper.value;
+ //sprite.y = yStepper.value;
+ //sprite.zoomFactor = zoomFactorStepper.value;
+ //sprite.antialiasing = antialiasingCheckbox.checked;
+ //sprite.color = colorWheel.color;
+ //sprite.spriteAnimType = animType.value;
+ sprite.angle = angleStepper.value;
+ sprite.alpha = alphaStepper.value;
+
+ function saveXY(obj:Dynamic, name:String, x:UINumericStepper, y:UINumericStepper) {
+ button.xml.remove(name + "");
+ button.xml.remove(name + "x");
+ button.xml.remove(name + "y");
+
+ obj.x = x.value;
+ obj.y = y.value;
+
+ if(x.value == y.value)
+ button.xml.set(name, x.value);
+ else {
+ button.xml.set(name + "x", x.value);
+ button.xml.set(name + "y", y.value);
+ }
+ }
+ function save(name:String, value:Dynamic) {
+ button.xml.remove(name);
+ button.xml.set(name, value);
+ }
+
+ // TODO: remove old attributes to prevent possible errors
+ // button.xml.remove();
+
+ button.xml.set("name", nameTextBox.label.text);
+ button.xml.set("sprite", spriteTextBox.label.text);
+ button.xml.set("x", xStepper.value);
+ button.xml.set("y", yStepper.value);
+ button.xml.set("zoom", zoomFactorStepper.value);
+ button.xml.set("type", animType.key);
+ button.xml.set("antialiasing", antialiasingCheckbox.checked);
+ button.xml.set("angle", angleStepper.value);
+ button.xml.set("alpha", alphaStepper.value);
+
+ saveXY(sprite.scrollFactor, "scroll", scrollXStepper, scrollYStepper);
+ saveXY(sprite.scale, "scale", scaleXStepper, scaleYStepper);
+ saveXY(sprite.skew, "skew", skewXStepper, skewYStepper);
+ XMLUtil.loadSpriteFromXML(sprite, button.xml, stage.spritesParentFolder);
+ }
+
+
\ No newline at end of file
diff --git a/assets/data/editors/layouts/stage/stageInfoScreen.xml b/assets/data/editors/layouts/stage/stageInfoScreen.xml
new file mode 100644
index 00000000..1a477da8
--- /dev/null
+++ b/assets/data/editors/layouts/stage/stageInfoScreen.xml
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+ import haxe.ds.StringMap;
+ var oldInfo = {
+ name: stage.stageName,
+ folder: stage.spritesParentFolder,
+ startCamX: stage.startCam.x,
+ startCamY: stage.startCam.y,
+ zoom: stage.defaultZoom,
+ attrib: new StringMap()
+ };
+
+ function col(c:Int) {
+ return MARGIN + (170 + XOFFSET) * c;
+ }
+
+ for(prop => val in stage.extra) {
+ if(!StringTools.startsWith(prop, "stageEditor.")) {
+ storedProps.push(prop);
+ oldInfo.attrib.set(prop, val);
+ }
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ import funkin.editors.extra.PropertyButton;
+
+ var buttonList = buttonlist; // important for now, since it gets reset after the code is run
+
+ var width = buttonList.buttonSize.x;
+ var height = buttonList.buttonSize.y;
+
+ buttonList.addButton.callback = function() {
+ buttonList.add(new PropertyButton("newProperty", "valueHere", buttonList, width, height, 150, 220, 25));
+ }
+
+ //var defaultAttribs = Stage.DEFAULT_ATTRIBUTES;
+ //for (i in stage.stageXML.attributes()) {
+ // trace(i, defaultAttribs);
+ // if (!defaultAttribs.contains(i))
+ // buttonList.add(new PropertyButton(i, stage.stageXML.get(i), buttonList, width, height, 150, 220, 25));
+ //}
+ for(key => val in stage.extra)
+ buttonList.add(new PropertyButton(key, val, buttonList, width, height, 150, 220, 25));
+
+
+
+
+
+ function onSave() {
+ stage.startCam.x = camxStepper.value;
+ stage.startCam.y = camyStepper.value;
+ stage.defaultZoom = zoomStepper.value;
+ stage.stageName = nameTextBox.label.text;
+ stage.spritesParentFolder = spritePathTextBox.label.text;
+
+ for(prop in storedProps)
+ stage.extra.remove(prop);
+
+ var newInfo = {
+ name: stage.stageName,
+ folder: stage.spritesParentFolder,
+ startCamX: stage.startCam.x,
+ startCamY: stage.startCam.y,
+ zoom: stage.defaultZoom,
+ attrib: new StringMap()
+ };
+ for(prop in attributesList.buttons.members) {
+ stage.extra.set(prop.propertyText.label.text, prop.valueText.label.text);
+ newInfo.attrib.set(prop.propertyText.label.text, prop.valueText.label.text);
+ }
+
+ FlxG.state.undos.addToUndo(CEditInfo(oldInfo, newInfo));
+ }
+
+
\ No newline at end of file
diff --git a/assets/data/editors/layouts/stage/unknownEditScreen.xml b/assets/data/editors/layouts/stage/unknownEditScreen.xml
new file mode 100644
index 00000000..ec6f3d71
--- /dev/null
+++ b/assets/data/editors/layouts/stage/unknownEditScreen.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+ function col(c:Int) {
+ return MARGIN + (150 + XOFFSET) * c;
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ import funkin.editors.extra.PropertyButton;
+
+ var buttonList = buttonlist; // important for now, since it gets reset after the code is run
+
+ var width = buttonList.buttonSize.x;
+ var height = buttonList.buttonSize.y;
+
+ buttonList.addButton.callback = function() {
+ buttonList.add(new PropertyButton("newProperty", "valueHere", buttonList, width, height, 110, 145, 25));
+ }
+
+ for (i in xml.attributes())
+ buttonList.add(new PropertyButton(i, xml.get(i), buttonList, width, height, 110, 145, 25));
+
+
+
+
+
+ function onSave() {
+ button.xml.nodeName = nodeNameTextBox.label.text;
+ button.highMemory = highMemoryRadio.checked;
+ button.lowMemory = lowMemoryRadio.checked;
+
+ for (i in xml.attributes())
+ xml.remove(i);
+
+ for (val in attributesList.buttons.members)
+ xml.set(val.propertyText.label.text, val.valueText.label.text);
+ }
+
+
\ No newline at end of file
diff --git a/assets/data/editors/layouts/stage/xmlEditScreen.xml b/assets/data/editors/layouts/stage/xmlEditScreen.xml
new file mode 100644
index 00000000..7a7ac956
--- /dev/null
+++ b/assets/data/editors/layouts/stage/xmlEditScreen.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+ function col(c:Int) {
+ return MARGIN + (150 + XOFFSET) * c;
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+ import funkin.editors.extra.PropertyButton;
+
+ var buttonList = buttonlist; // important for now, since it gets reset after the code is run
+
+ var width = buttonList.buttonSize.x;
+ var height = buttonList.buttonSize.y;
+
+ buttonList.addButton.callback = function() {
+ buttonList.add(new PropertyButton("newProperty", "valueHere", buttonList, width, height, 160, 195, 25));
+ }
+
+ for (i in xml.attributes())
+ buttonList.add(new PropertyButton(i, xml.get(i), buttonList, width, height, 160, 195, 25));
+
+
+
+
+
+ function onSave() {
+ for (i in xml.attributes())
+ xml.remove(i);
+
+ for (val in attributesList.buttons.members)
+ xml.set(val.propertyText.label.text, val.valueText.label.text);
+ }
+
+
\ No newline at end of file
diff --git a/assets/data/stages/test.xml b/assets/data/stages/test.xml
new file mode 100644
index 00000000..697711bb
--- /dev/null
+++ b/assets/data/stages/test.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/assets/data/states/StageEditor.hx b/assets/data/states/StageEditor.hx
new file mode 100644
index 00000000..f56533db
--- /dev/null
+++ b/assets/data/states/StageEditor.hx
@@ -0,0 +1,317 @@
+/**
+* THIS FILE IS TEMPORARY UNTIL TRANSFORMS ARE AT A USEABLE STATE.
+* TODO:
+* - !!!! MOVE BACK TO SOURCE !!!!
+* - Rotation
+* - Skewing
+* - Angle + Scale (like already at a angle)
+*/
+
+import funkin.editors.stage.StageEditor;
+import funkin.editors.ui.UIWarningSubstate;
+import funkin.backend.utils.WindowUtils;
+import flixel.math.FlxAngle;
+import openfl.Lib;
+
+var exID = StageEditor.exID;
+
+function tryUpdateHitbox(sprite) {
+ var spriteNode = sprite.extra.get(exID("node"));
+ if (spriteNode.exists("updateHitbox") && spriteNode.get("updateHitbox") == "true") {
+ sprite.updateHitbox();
+ return true;
+ }
+
+ if (!FlxG.keys.pressed.ALT) {
+ sprite.x = storedPos.x - (sprite.frameWidth * (storedScale.x - sprite.scale.x) * 0.5);
+ sprite.y = storedPos.y - (sprite.frameHeight * (storedScale.y - sprite.scale.y) * 0.5);
+ }
+ return false;
+}
+
+var allowClose = false;
+function create() {
+ WindowUtils.preventClosing = true;
+ WindowUtils.resetClosing();
+ WindowUtils.onClosing = function() {
+ if(!allowClose)
+ Lib.application.window.onClose.cancel();
+
+ var substate;
+ substate = new UIWarningSubstate("nuh uh", "you dont need to close goofus doofus", [
+ {
+ label: "Actually Exit",
+ onClick: function(t) {
+ allowClose = true;
+ Lib.application.window.close();
+ }
+ },
+ {
+ label: "Reload State",
+ onClick: function(t) {
+ FlxG.switchState(new StageEditor(StageEditor.__stage));
+ }
+ },
+ {
+ label: "oh whoops",
+ onClick: function(t) {
+ substate.close();
+ }
+ }
+ ]);
+
+ openSubState(substate);
+ }
+}
+
+function destroy() {
+ WindowUtils.preventClosing = false;
+ WindowUtils.resetClosing();
+ WindowUtils.onClosing = null;
+}
+
+function update() {
+ if (FlxG.mouse.justPressed)
+ lastRelative.set();
+
+ if(FlxG.keys.justPressed.R) {
+ trace("reloading");
+ FlxG.switchState(new StageEditor(StageEditor.__stage));
+ }
+}
+
+function genericScale(sprite, relative, doX, doY) {
+
+ var relativeMult = 1 / (FlxMath.lerp(1, stageCamera.zoom, sprite.zoomFactor) / stageCamera.zoom) * (FlxG.keys.pressed.ALT ? 2 : 1);
+ relative.x *= relativeMult;
+ relative.y *= relativeMult;
+
+ var width = sprite.frameWidth * storedScale.x;
+ var height = sprite.frameHeight * storedScale.y;
+ if(doX) width -= relative.x;
+ if(doY) height -= relative.y;
+ CoolUtil.setGraphicSizeFloat(sprite, width, height);
+
+ if(FlxG.keys.pressed.SHIFT) {
+ var nscale = Math.max(sprite.scale.x, sprite.scale.y);
+ sprite.scale.set(nscale, nscale);
+ }
+
+ var updatedHitbox = tryUpdateHitbox(sprite);
+ if (FlxG.keys.pressed.ALT) {
+ sprite.x = storedPos.x;
+ sprite.y = storedPos.y;
+ if(updatedHitbox) {
+ sprite.x += (sprite.frameWidth * (storedScale.x - sprite.scale.x) * 0.5);
+ sprite.y += (sprite.frameHeight * (storedScale.y - sprite.scale.y) * 0.5);
+ }
+ return true;
+ }
+ return !updatedHitbox;
+}
+
+function genericOppositeScale(sprite, relative, scaleX, scaleY, repositionX, repositionY) {
+ if(repositionX) relative.x *= -1;
+ if(repositionY) relative.y *= -1;
+ var repositioned = genericScale(sprite, relative, scaleX, scaleY);
+ if (!repositioned) {
+ if(repositionX) sprite.x = storedPos.x + (sprite.frameWidth * (storedScale.x - sprite.scale.x));
+ if(repositionY) sprite.y = storedPos.y + (sprite.frameHeight * (storedScale.y - sprite.scale.y));
+ } else if (!FlxG.keys.pressed.ALT) {
+ if(repositionX) sprite.x += (sprite.frameWidth * (storedScale.x - sprite.scale.x));
+ if(repositionY) sprite.y += (sprite.frameHeight * (storedScale.y - sprite.scale.y));
+ }
+}
+
+var oldSpritePos = FlxPoint.get();
+function SCALE_BOTTOM_RIGHT(sprite, relative) {
+ preRotBullshit(sprite, relative);
+ genericScale(sprite, relative, true, true);
+ postRotBullshit(sprite, relative);
+}
+
+/**
+ ROTATION
+**/
+function preRotBullshit(sprite, relative) {
+ if (sprite.angle != 0)
+ relative = rotateAround(relative, FlxPoint.weak(0, 0), -sprite.angle);
+
+ if (FlxG.mouse.justPressed) {
+ oldSpritePos.x = sprite.x;
+ oldSpritePos.y = sprite.y;
+ }
+}
+
+function postRotBullshit(sprite, relative) {
+ if (sprite.angle != 0) {
+ var p = rotateAround(FlxPoint.get(sprite.x, sprite.y), oldSpritePos, sprite.angle);
+ sprite.x = p.x;
+ sprite.y = p.y;
+ }
+}
+
+function rotateAround(p, origin, angle) {
+ var rel = FlxPoint.get(p.x - origin.x, p.y - origin.y);
+ rotateByDegrees(rel, angle);
+ p.x = origin.x + rel.x;
+ p.y = origin.y + rel.y;
+ rel.put();
+ return p;
+}
+function rotateByDegrees(p, angle) {
+ var rads = angle * FlxAngle.TO_RAD;
+ var s:Float = Math.sin(rads);
+ var c:Float = Math.cos(rads);
+ var tempX:Float = p.x;
+
+ p.x = tempX * c - p.y * s;
+ p.y = tempX * s + p.y * c;
+}
+ /**
+ END OF ROTATION
+ **/
+
+
+function SCALE_TOP_RIGHT(sprite, relative) {
+ preRotBullshit(sprite, relative);
+ genericOppositeScale(sprite, relative, true, true, false, true);
+ postRotBullshit(sprite, relative);
+}
+
+function MOVE_CENTER(sprite, relative) {
+ preRotBullshit(sprite, relative);
+ sprite.x = storedPos.x-relative.x;
+ sprite.y = storedPos.y-relative.y;
+ postRotBullshit(sprite, relative);
+}
+
+function SCALE_TOP_LEFT(sprite, relative) {
+ preRotBullshit(sprite, relative);
+ genericOppositeScale(sprite, relative, true, true, true, true);
+ postRotBullshit(sprite, relative);
+}
+
+function SCALE_BOTTOM_LEFT(sprite, relative) {
+ preRotBullshit(sprite, relative);
+ genericOppositeScale(sprite, relative, true, true, true, false);
+ postRotBullshit(sprite, relative);
+}
+
+function SCALE_LEFT(sprite, relative) {
+ preRotBullshit(sprite, relative);
+ genericOppositeScale(sprite, relative, true, false, true, false);
+ postRotBullshit(sprite, relative);
+}
+
+function SCALE_RIGHT(sprite, relative) {
+ preRotBullshit(sprite, relative);
+ genericScale(sprite, relative, true, false);
+ postRotBullshit(sprite, relative);
+}
+
+function SCALE_TOP(sprite, relative) {
+ preRotBullshit(sprite, relative);
+ genericOppositeScale(sprite, relative, false, true, false, true);
+ postRotBullshit(sprite, relative);
+}
+
+function SCALE_BOTTOM(sprite, relative) {
+ preRotBullshit(sprite, relative);
+ genericScale(sprite, relative, false, true);
+ postRotBullshit(sprite, relative);
+}
+
+var lastRelative = FlxPoint.get();
+
+function SKEW_LEFT(sprite, relative) {
+ preRotBullshit(sprite, relative);
+ if (!FlxG.keys.pressed.SHIFT)
+ genericOppositeScale(sprite, relative, true, false, true, false);
+
+ var topLeft = sprite.extra.get(exID("buttonBoxes"))[0];
+ var topRight = sprite.extra.get(exID("buttonBoxes"))[1];
+
+ sprite.y = storedPos.y - relative.y * 0.5;
+ sprite.skew.y = Math.atan2(
+ topRight.y - (topLeft.y + (lastRelative.y - relative.y)),
+ topRight.x - (topLeft.x + (FlxG.keys.pressed.SHIFT ? 0 : relative.x - lastRelative.x))
+ ) * FlxAngle.TO_DEG;
+
+ lastRelative.set((FlxG.keys.pressed.SHIFT ? lastRelative.x : relative.x), relative.y);
+ postRotBullshit(sprite, relative);
+}
+
+function SKEW_BOTTOM(sprite, relative) {
+ preRotBullshit(sprite, relative);
+ if (!FlxG.keys.pressed.SHIFT)
+ genericScale(sprite, relative, false, true);
+
+ var topLeft = sprite.extra.get(exID("buttonBoxes"))[0];
+ var bottomLeft = sprite.extra.get(exID("buttonBoxes"))[2];
+
+ sprite.x = storedPos.x - relative.x * 0.5;
+ sprite.skew.x = Math.atan2(
+ topLeft.x - (bottomLeft.x + (lastRelative.x - relative.x)),
+ topLeft.y - (bottomLeft.y + (FlxG.keys.pressed.SHIFT ? 0 : lastRelative.y - relative.y))
+ ) * FlxAngle.TO_DEG;
+
+ lastRelative.set(relative.x, (FlxG.keys.pressed.SHIFT ? lastRelative.y : relative.y));
+ postRotBullshit(sprite, relative);
+}
+
+function SKEW_TOP(sprite, relative) {
+ preRotBullshit(sprite, relative);
+ if (!FlxG.keys.pressed.SHIFT)
+ genericOppositeScale(sprite, relative, false, true, false, true);
+
+ var topLeft = sprite.extra.get(exID("buttonBoxes"))[0];
+ var bottomLeft = sprite.extra.get(exID("buttonBoxes"))[2];
+
+ sprite.x = storedPos.x - relative.x * 0.5;
+ sprite.skew.x = Math.atan2(
+ bottomLeft.x - (topLeft.x + (lastRelative.x - relative.x)),
+ bottomLeft.y - (topLeft.y + (FlxG.keys.pressed.SHIFT ? 0 : relative.y - lastRelative.y))
+ ) * FlxAngle.TO_DEG;
+
+ lastRelative.set(relative.x, (FlxG.keys.pressed.SHIFT ? lastRelative.y : relative.y));
+ postRotBullshit(sprite, relative);
+}
+
+function SKEW_RIGHT(sprite, relative) {
+ preRotBullshit(sprite, relative);
+ if (!FlxG.keys.pressed.SHIFT)
+ genericScale(sprite, relative, true, false);
+
+ var topLeft = sprite.extra.get(exID("buttonBoxes"))[0];
+ var topRight = sprite.extra.get(exID("buttonBoxes"))[1];
+
+ sprite.y = storedPos.y - relative.y * 0.5;
+ sprite.skew.y = Math.atan2(
+ topLeft.y - (topRight.y + (lastRelative.y - relative.y)),
+ topLeft.x - (topRight.x + (FlxG.keys.pressed.SHIFT ? 0 : lastRelative.x - relative.x))
+ ) * FlxAngle.TO_DEG;
+
+ lastRelative.set((FlxG.keys.pressed.SHIFT ? lastRelative.x : relative.x), relative.y);
+ postRotBullshit(sprite, relative);
+}
+
+function ROTATE(sprite, relative) {
+ var buttonBoxes:Array = sprite.extra.get(exID("buttonBoxes"));
+ var p:FlxPoint = buttonBoxes[8];
+
+ FlxG.mouse.getWorldPosition(stageCamera, _point);
+
+ var dx:Float = _point.x - p.x;
+ var dy:Float = _point.y - p.y;
+ var angle = FlxAngle.angleFromOrigin(dx, dy, true) + angleOffset;
+ if(FlxG.keys.pressed.SHIFT) angle = Std.int(angle / 45) * 45;
+ sprite.angle = angle;
+}
+
+
+/*function postDraw() {
+ if(storedCenter.x != 0 || storedCenter.y != 0) {
+ drawLine(storedCenter, storedRelative, 0.3);
+ }
+}*/
diff --git a/assets/images/editors/character/edit-button.png b/assets/images/editors/character/edit-button.png
index e0018a54..47991177 100644
Binary files a/assets/images/editors/character/edit-button.png and b/assets/images/editors/character/edit-button.png differ
diff --git a/assets/images/editors/stage/selectionDot.png b/assets/images/editors/stage/selectionDot.png
new file mode 100644
index 00000000..d5aa449e
Binary files /dev/null and b/assets/images/editors/stage/selectionDot.png differ
diff --git a/assets/images/editors/stage/trash-icon.png b/assets/images/editors/stage/trash-icon.png
new file mode 100644
index 00000000..37041078
Binary files /dev/null and b/assets/images/editors/stage/trash-icon.png differ
diff --git a/assets/images/editors/stage/visible-icon.png b/assets/images/editors/stage/visible-icon.png
new file mode 100644
index 00000000..2e603333
Binary files /dev/null and b/assets/images/editors/stage/visible-icon.png differ
diff --git a/assets/images/editors/ui/radiobutton.png b/assets/images/editors/ui/radiobutton.png
new file mode 100644
index 00000000..a6f7dbce
Binary files /dev/null and b/assets/images/editors/ui/radiobutton.png differ
diff --git a/commandline/Main.hx b/commandline/Main.hx
index 8c2f9301..b4992402 100644
--- a/commandline/Main.hx
+++ b/commandline/Main.hx
@@ -49,7 +49,8 @@ class Main {
"If you want a full build which contains all assets, run \"cne release\" or \"cne test-release\"",
"Additional arguments will be sent to the lime compiler.",
"",
- "-debug : Builds a debug build."
+ "-debug : Builds a debug build.",
+ "-clean : Compiled files will be deleted before compiling."
].join("\n")
},
{
@@ -67,7 +68,20 @@ class Main {
"If you want a full build which contains all assets, run \"cne release\" or \"cne test-release\"",
"Additional arguments will be sent to the lime compiler.",
"",
- "-debug : Builds a debug build."
+ "-debug : Builds a debug build.",
+ "-clean : Compiled files will be deleted before compiling."
+ ].join("\n")
+ },
+ {
+ names: ["run"],
+ doc: "Runs the last build that was created.",
+ func: Compiler.run,
+ dDoc: [
+ "Usage: run ",
+ "",
+ "This will run the last build that was created.",
+ "If the last build was a debug build, you need to pass the -debug argument to run it.",
+ "Additional arguments will be sent to the lime compiler."
].join("\n")
},
{
@@ -79,7 +93,9 @@ class Main {
"",
"This will create and run a final ready-for-release build,",
"which means this build will be able to be release on websites such as GameBanana without worrying about source-dependant stuff.",
- "Additional arguments will be sent to the lime compiler."
+ "Additional arguments will be sent to the lime compiler.",
+ "",
+ "-clean : Compiled files will be deleted before compiling."
].join("\n")
},
{
@@ -91,7 +107,9 @@ class Main {
"",
"This will create and run a final ready-for-release build,",
"which means this build will be able to be release on websites such as GameBanana without worrying about source-dependant stuff.",
- "Additional arguments will be sent to the lime compiler."
+ "Additional arguments will be sent to the lime compiler.",
+ "",
+ "-clean : Compiled files will be deleted before compiling."
].join("\n")
},
{
diff --git a/commandline/commands/Compiler.hx b/commandline/commands/Compiler.hx
index 6d09f5aa..08c39f24 100644
--- a/commandline/commands/Compiler.hx
+++ b/commandline/commands/Compiler.hx
@@ -2,19 +2,23 @@ package commands;
class Compiler {
public static function test(args:Array) {
- __build(args, ["test", getBuildTarget(), "-DTEST_BUILD"]);
+ __runLime(args, ["test", getBuildTarget(), "-DTEST_BUILD"]);
}
public static function build(args:Array) {
- __build(args, ["build", getBuildTarget(), "-DTEST_BUILD"]);
+ __runLime(args, ["build", getBuildTarget(), "-DTEST_BUILD"]);
}
public static function release(args:Array) {
- __build(args, ["build", getBuildTarget()]);
+ __runLime(args, ["build", getBuildTarget()]);
}
public static function testRelease(args:Array) {
- __build(args, ["test", getBuildTarget()]);
+ __runLime(args, ["test", getBuildTarget()]);
}
- private static function __build(args:Array, arg:Array) {
+ public static function run(args:Array) {
+ __runLime(args, ["run", getBuildTarget()]);
+ }
+
+ private static function __runLime(args:Array, arg:Array) {
for(a in args)
arg.push(a);
Sys.command("lime", arg);
diff --git a/libs.xml b/libs.xml
index cfdec718..f3d27d0f 100644
--- a/libs.xml
+++ b/libs.xml
@@ -9,7 +9,7 @@
-
+
diff --git a/source/external/ExternalMac.hx b/source/external/ExternalMac.hx
new file mode 100644
index 00000000..46a9e5f1
--- /dev/null
+++ b/source/external/ExternalMac.hx
@@ -0,0 +1,12 @@
+package external;
+
+#if mac
+@:build(external.LinkerMacro.xml('external_code.xml'))
+@:include('Mac.h')
+@:native('ExternalMac')
+extern class ExternalMac
+{
+ @:native('ExternalMac::setCursorIcon')
+ public static function setCursorIcon(icon:Int, customCursor:cpp.ConstCharStar, customX:Single, customY:Single):Bool;
+}
+#end
diff --git a/source/external/LinkerMacro.hx b/source/external/LinkerMacro.hx
new file mode 100644
index 00000000..3f3912b9
--- /dev/null
+++ b/source/external/LinkerMacro.hx
@@ -0,0 +1,40 @@
+package external;
+
+import haxe.io.Path;
+import haxe.macro.Expr;
+import haxe.macro.Context;
+import haxe.xml.Printer;
+import sys.FileSystem;
+
+class LinkerMacro
+{
+ public static macro function xml(filename:String = 'Build.xml'):Array
+ {
+ var pos:Position = Context.currentPos();
+
+ var sourcePath:String = Path.directory(Context.getPosInfos(pos).file);
+
+ if (!Path.isAbsolute(sourcePath))
+ sourcePath = FileSystem.absolutePath(sourcePath);
+
+ sourcePath = Path.removeTrailingSlashes(Path.normalize(sourcePath));
+
+ var includeElement:Xml = Xml.createElement('include');
+
+ includeElement.set('name', Path.join([sourcePath, filename]));
+
+ @:privateAccess
+ {
+ var printer:Printer = new Printer(true);
+
+ printer.writeNode(includeElement, '\n');
+
+ Context.getLocalClass().get().meta.add(':buildXml', [{
+ expr: EConst(CString(printer.output.toString())),
+ pos: pos
+ }], pos);
+ }
+
+ return Context.getBuildFields();
+ }
+}
\ No newline at end of file
diff --git a/source/external/external_code.xml b/source/external/external_code.xml
new file mode 100644
index 00000000..308b0d35
--- /dev/null
+++ b/source/external/external_code.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/source/external/include/mac/CursorHelper.h b/source/external/include/mac/CursorHelper.h
new file mode 100644
index 00000000..6045293a
--- /dev/null
+++ b/source/external/include/mac/CursorHelper.h
@@ -0,0 +1,7 @@
+#import
+
+@interface CursorHelper : NSObject
+
++ (NSCursor *)getCursorForSelector:(SEL)selector defaultCursor:(NSCursor *)defaultCursor;
+
+@end
\ No newline at end of file
diff --git a/source/external/include/mac/Mac.h b/source/external/include/mac/Mac.h
new file mode 100644
index 00000000..516b5050
--- /dev/null
+++ b/source/external/include/mac/Mac.h
@@ -0,0 +1,13 @@
+#ifndef MAC_H
+#define MAC_H
+
+#if defined(__APPLE__)
+
+namespace ExternalMac
+{
+ bool setCursorIcon(int icon, const char *customCursor, float customX, float customY);
+}
+
+#endif
+
+#endif
diff --git a/source/external/src/mac/CursorHelper.mm b/source/external/src/mac/CursorHelper.mm
new file mode 100644
index 00000000..3999bbb5
--- /dev/null
+++ b/source/external/src/mac/CursorHelper.mm
@@ -0,0 +1,24 @@
+#import
+#import "CursorHelper.h"
+
+@implementation CursorHelper
+
++ (NSCursor *)getCursorForSelector:(SEL)selector defaultCursor:(NSCursor *)defaultCursor
+{
+ NSCursor *cursor = nil;
+
+ if ([NSCursor respondsToSelector:selector])
+ {
+ cursor = [NSCursor performSelector:selector];
+ }
+ else
+ {
+ NSLog(@"CursorHelper: selector %@ not found", NSStringFromSelector(selector));
+
+ cursor = defaultCursor;
+ }
+
+ return cursor;
+}
+
+@end
diff --git a/source/external/src/mac/Mac.mm b/source/external/src/mac/Mac.mm
new file mode 100644
index 00000000..de4c62f8
--- /dev/null
+++ b/source/external/src/mac/Mac.mm
@@ -0,0 +1,140 @@
+#include
+
+#import
+#import "CursorHelper.h"
+
+#define ARROW 0
+#define CROSSHAIR 1
+#define CLICK 2
+#define IBEAM 3
+#define MOVE 4
+#define HAND 5
+#define DRAG 6
+#define DRAG_OPEN 7
+#define WAIT 8
+#define WAIT_ARROW 9
+#define DISABLED 10
+#define RESIZE_TR 11
+#define RESIZE_BL 12
+#define RESIZE_TL 13
+#define RESIZE_BR 14
+#define RESIZE_H 15
+#define RESIZE_V 16
+#define RESIZE_T 17
+#define RESIZE_B 18
+#define RESIZE_L 19
+#define RESIZE_R 20
+#define RESIZE_TLBR 21
+#define RESIZE_TRBL 22
+#define CUSTOM -1
+
+namespace ExternalMac
+{
+ std::unordered_map customCursorCache;
+
+ bool setCursorIcon(int icon, const char *customCursor, float customX, float customY)
+ {
+ NSCursor *cursor = nil;
+
+ switch (icon)
+ {
+ case ARROW:
+ cursor = [NSCursor arrowCursor];
+ break;
+ case CLICK:
+ cursor = [NSCursor pointingHandCursor];
+ break;
+ case CROSSHAIR:
+ cursor = [NSCursor crosshairCursor];
+ break;
+ case HAND:
+ cursor = [NSCursor closedHandCursor];
+ break;
+ case IBEAM:
+ cursor = [NSCursor IBeamCursor];
+ break;
+ case RESIZE_H:
+ cursor = [NSCursor resizeLeftRightCursor];
+ break;
+ case RESIZE_V:
+ cursor = [NSCursor resizeUpDownCursor];
+ break;
+ // TODO: DEFAULTS
+ case RESIZE_TL:
+ cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeNorthWestCursor)defaultCursor:nil];
+ break;
+ case RESIZE_TR:
+ cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeNorthEastCursor)defaultCursor:nil];
+ break;
+ case RESIZE_BL:
+ cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeSouthWestCursor)defaultCursor:nil];
+ break;
+ case RESIZE_BR:
+ cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeSouthEastCursor)defaultCursor:nil];
+ break;
+ case RESIZE_T:
+ cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeNorthCursor)defaultCursor:nil];
+ break;
+ case RESIZE_B:
+ cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeSouthCursor)defaultCursor:nil];
+ break;
+ case RESIZE_L:
+ cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeWestCursor)defaultCursor:nil];
+ break;
+ case RESIZE_R:
+ cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeEastCursor)defaultCursor:nil];
+ break;
+ case RESIZE_TLBR:
+ cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeNorthWestSouthEastCursor)defaultCursor:nil];
+ break;
+ case RESIZE_TRBL:
+ cursor = [CursorHelper getCursorForSelector:@selector(_windowResizeNorthEastSouthWestCursor)defaultCursor:nil];
+ break;
+ case WAIT:
+ cursor = [CursorHelper getCursorForSelector:@selector(_waitCursor)defaultCursor:nil];
+ break;
+ case WAIT_ARROW:
+ cursor = [CursorHelper getCursorForSelector:@selector(_waitCursor)defaultCursor:nil];
+ break;
+ case DISABLED:
+ cursor = [NSCursor operationNotAllowedCursor];
+ break;
+ case DRAG:
+ cursor = [NSCursor closedHandCursor];
+ break;
+ case DRAG_OPEN:
+ cursor = [NSCursor openHandCursor];
+ break;
+ case CUSTOM:
+ if (customCursorCache.find(customCursor) != customCursorCache.end())
+ cursor = customCursorCache[customCursor];
+ else
+ {
+ NSString *cursorImageName = [NSString stringWithUTF8String:customCursor];
+ NSImage *tmpImage = [[NSImage alloc] initWithContentsOfFile:cursorImageName];
+
+ if (tmpImage != nil)
+ {
+ cursor = [[NSCursor alloc] initWithImage:tmpImage hotSpot:NSMakePoint(customX, customY)];
+ customCursorCache[customCursor] = cursor;
+ [tmpImage release];
+ }
+ else
+ NSLog(@"Failed to load image %@", cursorImageName);
+
+ [cursorImageName release];
+ }
+
+ break;
+ }
+
+ if (cursor != nil)
+ {
+ [cursor set];
+
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/source/funkin/backend/FunkinSprite.hx b/source/funkin/backend/FunkinSprite.hx
index d817f142..40b36a02 100644
--- a/source/funkin/backend/FunkinSprite.hx
+++ b/source/funkin/backend/FunkinSprite.hx
@@ -29,6 +29,15 @@ enum abstract XMLAnimType(Int)
default: def;
}
}
+
+ @:to public function toString():String {
+ return switch (cast this)
+ {
+ case NONE: "none";
+ case BEAT: "beat";
+ case LOOP: "loop";
+ }
+ }
}
class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements IOffsetCompatible implements IXMLEvents
@@ -39,9 +48,10 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
public var beatAnims:Array = [];
public var name:String;
public var zoomFactor:Float = 1;
- public var initialZoom:Float = 1;
public var debugMode:Bool = false;
public var animDatas:Map = [];
+ public var animEnabled:Bool = true;
+ public var zoomFactorEnabled:Bool = true;
/**
* ODD interval -> asynced; EVEN interval -> synced
@@ -86,6 +96,7 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
spr.skew.set(source.skew.x, source.skew.y);
spr.transformMatrix = source.transformMatrix;
spr.matrixExposed = source.matrixExposed;
+ spr.zoomFactor = source.zoomFactor;
spr.animOffsets = source.animOffsets.copy();
}
return spr;
@@ -120,6 +131,7 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
private var countedBeat = 0;
public function beatHit(curBeat:Int)
{
+ if(!animEnabled) return;
if (beatAnims.length > 0 && (curBeat + beatOffset) % beatInterval == 0)
{
if(skipNegativeBeats && curBeat < 0) return;
@@ -138,49 +150,6 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
{
}
- public override function getScreenBounds(?newRect:FlxRect, ?camera:FlxCamera):FlxRect
- {
- __doPreZoomScaleProcedure(camera);
- var r = super.getScreenBounds(newRect, camera);
- __doPostZoomScaleProcedure();
- return r;
- }
-
- public override function drawComplex(camera:FlxCamera)
- {
- super.drawComplex(camera);
- }
-
- public override function doAdditionalMatrixStuff(matrix:FlxMatrix, camera:FlxCamera)
- {
- super.doAdditionalMatrixStuff(matrix, camera);
- matrix.translate(-camera.width / 2, -camera.height / 2);
-
- var requestedZoom = FlxMath.lerp(1, camera.zoom, zoomFactor);
- var diff = requestedZoom / camera.zoom;
- matrix.scale(diff, diff);
- matrix.translate(camera.width / 2, camera.height / 2);
- }
-
- public override function getScreenPosition(?point:FlxPoint, ?Camera:FlxCamera):FlxPoint
- {
- if (__shouldDoScaleProcedure())
- {
- __oldScrollFactor.set(scrollFactor.x, scrollFactor.y);
- var requestedZoom = FlxMath.lerp(initialZoom, camera.zoom, zoomFactor);
- var diff = requestedZoom / camera.zoom;
-
- scrollFactor.scale(1 / diff);
-
- var r = super.getScreenPosition(point, Camera);
-
- scrollFactor.set(__oldScrollFactor.x, __oldScrollFactor.y);
-
- return r;
- }
- return super.getScreenPosition(point, Camera);
- }
-
// ANIMATE ATLAS DRAWING
#if REGION
public override function draw()
@@ -236,33 +205,60 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
}
#end
- // SCALING FUNCS
- #if REGION
- private inline function __shouldDoScaleProcedure()
- return zoomFactor != 1;
+ // ZOOM FACTOR
+ private inline function __shouldDoZoomFactor()
+ return zoomFactorEnabled && zoomFactor != 1;
- static var __oldScrollFactor:FlxPoint = new FlxPoint();
- static var __oldScale:FlxPoint = new FlxPoint();
- var __skipZoomProcedure:Bool = false;
-
- private function __doPreZoomScaleProcedure(camera:FlxCamera)
+ public override function getScreenBounds(?newRect:FlxRect, ?camera:FlxCamera):FlxRect
{
- if (__skipZoomProcedure = !__shouldDoScaleProcedure())
- return;
- __oldScale.set(scale.x, scale.y);
- var requestedZoom = FlxMath.lerp(initialZoom, camera.zoom, zoomFactor);
- var diff = requestedZoom * camera.zoom;
+ if (camera == null)
+ camera = FlxG.camera;
- scale.scale(diff);
+ var r = super.getScreenBounds(newRect, camera);
+
+ if(__shouldDoZoomFactor()) {
+ r.x -= camera.width / 2;
+ r.y -= camera.height / 2;
+
+ var ratio = (camera.zoom > 0 ? Math.max : Math.min)(0, FlxMath.lerp(1 / camera.zoom, 1, zoomFactor));
+ r.x *= ratio;
+ r.y *= ratio;
+ r.width *= ratio;
+ r.height *= ratio;
+
+ r.x += camera.width / 2;
+ r.y += camera.height / 2;
+ }
+ return r;
}
- private function __doPostZoomScaleProcedure()
+ override public function isOnScreen(?camera:FlxCamera):Bool
{
- if (__skipZoomProcedure)
- return;
- scale.set(__oldScale.x, __oldScale.y);
+ if (forceIsOnScreen)
+ return true;
+
+ if (camera == null)
+ camera = FlxG.camera;
+
+ var bounds = getScreenBounds(_rect, camera);
+ if (bounds.width == 0 && bounds.height == 0)
+ return false;
+ return camera.containsRect(bounds);
+ }
+
+ // ZOOM FACTOR RENDERING
+ public override function doAdditionalMatrixStuff(matrix:FlxMatrix, camera:FlxCamera)
+ {
+ super.doAdditionalMatrixStuff(matrix, camera);
+ if(__shouldDoZoomFactor()) {
+ matrix.translate(-camera.width / 2, -camera.height / 2);
+
+ var requestedZoom = (camera.zoom >= 0 ? Math.max : Math.min)(FlxMath.lerp(1, camera.zoom, zoomFactor), 0);
+ var diff = requestedZoom / camera.zoom;
+ matrix.scale(diff, diff);
+ matrix.translate(camera.width / 2, camera.height / 2);
+ }
}
- #end
// OFFSETTING
#if REGION
@@ -350,6 +346,11 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I
public inline function isAnimFinished() {
return animateAtlas != null ? (animateAtlas.anim.finished) : (animation.curAnim != null ? animation.curAnim.finished : true);
}
+
+ override function updateAnimation(elapsed:Float) {
+ if (animEnabled)
+ super.updateAnimation(elapsed);
+ }
#end
// Getter / Setters
diff --git a/source/funkin/backend/assets/Paths.hx b/source/funkin/backend/assets/Paths.hx
index d7567b82..67b92967 100644
--- a/source/funkin/backend/assets/Paths.hx
+++ b/source/funkin/backend/assets/Paths.hx
@@ -97,13 +97,12 @@ class Paths
return getPath('images/$key.$ext', library);
}
- inline static public function script(key:String, ?library:String, isAssetsPath:Bool = false) {
+ static public function script(key:String, ?library:String, isAssetsPath:Bool = false) {
var scriptPath = isAssetsPath ? key : getPath(key, library);
if (!OpenFlAssets.exists(scriptPath)) {
var p:String;
for(ex in Script.scriptExtensions) {
- p = '$scriptPath.$ex';
- if (OpenFlAssets.exists(p)) {
+ if (OpenFlAssets.exists(p = scriptPath + '.' + ex)) {
scriptPath = p;
break;
}
diff --git a/source/funkin/backend/chart/EventsData.hx b/source/funkin/backend/chart/EventsData.hx
index e801f18f..9527c1b1 100644
--- a/source/funkin/backend/chart/EventsData.hx
+++ b/source/funkin/backend/chart/EventsData.hx
@@ -76,7 +76,7 @@ class EventsData {
for (file in Paths.getFolderContent('data/events/', true, BOTH)) {
if (Path.extension(file) != "json" && Path.extension(file) != "pack") continue;
- var eventName:String = Path.withoutExtension(Path.withoutDirectory(file));
+ var eventName:String = CoolUtil.getFilename(file);
var fileTxt:String = Assets.getText(file);
if (Path.extension(file) == "pack") {
diff --git a/source/funkin/backend/system/FakeCamera.hx b/source/funkin/backend/system/FakeCamera.hx
new file mode 100644
index 00000000..5170c246
--- /dev/null
+++ b/source/funkin/backend/system/FakeCamera.hx
@@ -0,0 +1,38 @@
+package funkin.backend.system;
+
+import flixel.graphics.FlxGraphic;
+import flixel.graphics.frames.FlxFrame;
+import flixel.graphics.tile.FlxDrawTrianglesItem;
+import flixel.math.FlxMatrix;
+import flixel.math.FlxPoint;
+import flixel.system.FlxAssets.FlxShader;
+import openfl.display.BitmapData;
+import openfl.display.BlendMode;
+import openfl.geom.ColorTransform;
+import openfl.geom.Point;
+import openfl.geom.Rectangle;
+
+class FakeCamera extends FlxCamera {
+ public static final instance = new FakeCamera();
+
+ public function new() {
+ super();
+
+ visible = false;
+ }
+
+ override function startTrianglesBatch(graphic:FlxGraphic, smoothing:Bool = false, isColored:Bool = false, ?blend:BlendMode, ?hasColorOffsets:Bool, ?shader:FlxShader) { return null;}
+ override function startQuadBatch(graphic:FlxGraphic, colored:Bool, hasColorOffsets:Bool = false, ?blend:BlendMode, smooth:Bool = false, ?shader:FlxShader) { return null;}
+ override function clearDrawStack() {}
+ override function render() {}
+
+ public override function drawPixels(?frame:FlxFrame, ?pixels:BitmapData, matrix:FlxMatrix, ?transform:ColorTransform, ?blend:BlendMode, ?smoothing:Bool = false, ?shader:FlxShader) {}
+ public override function copyPixels(?frame:FlxFrame, ?pixels:BitmapData, ?sourceRect:Rectangle, destPoint:Point, ?transform:ColorTransform, ?blend:BlendMode, ?smoothing:Bool = false, ?shader:FlxShader) {}
+ public override function drawTriangles(graphic:FlxGraphic, vertices:DrawData, indices:DrawData, uvtData:DrawData, ?colors:DrawData, ?position:FlxPoint, ?blend:BlendMode, repeat:Bool = false, smoothing:Bool = false, ?transform:ColorTransform, ?shader:FlxShader):Void {}
+
+ public override function update(elapsed:Float) {}
+
+ // override function set_zoom(Zoom:Float):Float return zoom = (Zoom == 0) ? FlxCamera.defaultZoom : Zoom;
+
+ public override function setScale(X:Float, Y:Float):Void {}
+}
\ No newline at end of file
diff --git a/source/funkin/backend/system/framerate/SystemInfo.hx b/source/funkin/backend/system/framerate/SystemInfo.hx
index a815e3b5..979670e5 100644
--- a/source/funkin/backend/system/framerate/SystemInfo.hx
+++ b/source/funkin/backend/system/framerate/SystemInfo.hx
@@ -17,7 +17,7 @@ class SystemInfo extends FramerateCategory {
static var __formattedSysText:String = "";
- public static inline function init() {
+ public static function init() {
#if linux
var process = new HiddenProcess("cat", ["/etc/os-release"]);
if (process.exitCode() != 0) Logs.trace('Unable to grab OS Label', ERROR, RED);
diff --git a/source/funkin/backend/utils/CoolUtil.hx b/source/funkin/backend/utils/CoolUtil.hx
index 36f817e8..ecfd6156 100644
--- a/source/funkin/backend/utils/CoolUtil.hx
+++ b/source/funkin/backend/utils/CoolUtil.hx
@@ -133,10 +133,8 @@ class CoolUtil
* Whenever a value is NaN or not.
* @param v Value
*/
- public static inline function isNaN(v:Dynamic) {
- if (v is Float || v is Int)
- return Math.isNaN(cast(v, Float));
- return false;
+ public static inline function isNaN(v:Dynamic):Bool {
+ return (v is Float) ? Math.isNaN(cast(v, Float)) : false;
}
/**
@@ -185,20 +183,21 @@ class CoolUtil
return str;
}
+ private static var sizeLabels:Array = ["B", "KB", "MB", "GB", "TB"];
+
/**
* Returns a string representation of a size, following this format: `1.02 GB`, `134.00 MB`
* @param size Size to convert to string
* @return String Result string representation
*/
public static function getSizeString(size:Float):String {
- var labels = ["B", "KB", "MB", "GB", "TB"];
var rSize:Float = size;
var label:Int = 0;
- while(rSize > 1024 && label < labels.length-1) {
+ while(rSize > 1024 && label < sizeLabels.length-1) {
label++;
rSize /= 1024;
}
- return '${Std.int(rSize) + "." + addZeros(Std.string(Std.int((rSize % 1) * 100)), 2)}${labels[label]}';
+ return Std.int(rSize) + "." + addZeros(Std.string(Std.int((rSize % 1) * 100)), 2) + sizeLabels[label];
}
/**
@@ -416,6 +415,21 @@ class CoolUtil
sprite.scale.set(nScale, nScale);
}
+ public static function setGraphicSizeFloat(sprite:FlxSprite, Width:Float = 0, Height:Float = 0):Void
+ {
+ if (Width <= 0 && Height <= 0)
+ return;
+
+ var newScaleX:Float = Width / sprite.frameWidth;
+ var newScaleY:Float = Height / sprite.frameHeight;
+ sprite.scale.set(newScaleX, newScaleY);
+
+ if (Width <= 0)
+ sprite.scale.x = newScaleY;
+ else if (Height <= 0)
+ sprite.scale.y = newScaleX;
+ }
+
/**
* Returns a simple string representation of a FlxKey. Used in Controls options.
* @param key Key
@@ -705,6 +719,16 @@ class CoolUtil
@:noUsing public static inline function flxeaseFromString(mainEase:String, suffix:String)
return Reflect.field(FlxEase, mainEase + (mainEase == "linear" ? "" : suffix));
+ /*
+ * Returns the filename of a path, without the extension.
+ * @param path Path to get the filename from
+ * @return Filename
+ */
+ @:noUsing public static inline function getFilename(file:String) {
+ var file = new haxe.io.Path(file);
+ return file.file;
+ }
+
public static function sortAlphabetically(array:Array, ?lowercase:Bool=false) {
array.sort(function(a1, a2):Int {
if(lowercase) {
diff --git a/source/funkin/backend/utils/MatrixUtil.hx b/source/funkin/backend/utils/MatrixUtil.hx
new file mode 100644
index 00000000..bd278385
--- /dev/null
+++ b/source/funkin/backend/utils/MatrixUtil.hx
@@ -0,0 +1,42 @@
+package funkin.backend.utils;
+
+import flixel.FlxCamera;
+import flixel.math.FlxPoint;
+import flixel.util.typeLimit.OneOfTwo;
+
+class MatrixUtil {
+ public static function getMatrixPosition(sprite:FlxSprite, points:OneOfTwo>, ?camera:FlxCamera, _width:Float = 1, _height:Float = 1):Array
+ {
+ //if(_width == -1) _width = sprite.width;
+ //if(_height == -1) _height = sprite.height;
+ if(camera == null) camera = sprite.camera;
+ if(points is FlxBasePoint) points = [points];
+ var nc = funkin.backend.system.FakeCamera.instance;
+ nc.zoom = camera.zoom;
+ nc.scroll.set(camera.scroll.x, camera.scroll.y);
+ nc.pixelPerfectRender = camera.pixelPerfectRender;
+ @:privateAccess sprite.drawComplex(nc);
+
+ var isFunkinSprite = sprite is FunkinSprite;
+
+ var points:Array = cast points;
+ @:privateAccess for(point in points) {
+ var x = sprite._matrix.__transformX(point.x * _width, point.y * _height);
+ var y = sprite._matrix.__transformY(point.x * _width, point.y * _height);
+
+ // reset to ingame coords
+ x += camera.scroll.x;
+ y += camera.scroll.y;
+
+ if(isFunkinSprite) {
+ var sprite:FunkinSprite = cast sprite;
+
+ var ratio = 1 - FlxMath.lerp(1 / camera.zoom, 1, sprite.zoomFactor);
+ x += camera.width / 2 * ratio;
+ y += camera.height / 2 * ratio;
+ }
+ point.set(x, y);
+ }
+ return points;
+ }
+}
\ No newline at end of file
diff --git a/source/funkin/backend/utils/NativeAPI.hx b/source/funkin/backend/utils/NativeAPI.hx
index a651645e..e1393be8 100644
--- a/source/funkin/backend/utils/NativeAPI.hx
+++ b/source/funkin/backend/utils/NativeAPI.hx
@@ -1,5 +1,8 @@
package funkin.backend.utils;
+import haxe.macro.Expr.Case;
+import openfl.ui.Mouse;
+import openfl.ui.MouseCursor;
import funkin.backend.utils.native.*;
/**
@@ -72,6 +75,17 @@ class NativeAPI {
#end
}
+ /**
+ * Set cursor icon.
+ **/
+ public static function setCursorIcon(icon:CodeCursor) {
+ #if mac
+ Mac.setMouseCursorIcon(cast icon);
+ #else
+ Mouse.cursor = icon.toOpenFL();
+ #end
+ }
+
public static function consoleColorToANSI(color:ConsoleColor) {
return switch(color) {
case BLACK: 30;
@@ -141,4 +155,102 @@ enum abstract MessageBoxIcon(Int) {
var MSG_QUESTION = 0x00000020;
var MSG_WARNING = 0x00000030;
var MSG_INFORMATION = 0x00000040;
+}
+
+enum abstract CodeCursor(String) {
+ var CUSTOM;// = "arrow";
+ var ARROW;// = "arrow";
+ var CLICK;// = "click";
+ var CROSSHAIR;// = "crosshair";
+ var HAND;// = "hand";
+ var IBEAM;// = "ibeam";
+ var MOVE;// = "move";
+
+ var RESIZE_H;// = "resize_we";
+ var RESIZE_V;// = "resize_ns";
+ var RESIZE_TL;// = "resize_nw";
+ var RESIZE_TR;// = "resize_ne";
+ var RESIZE_BL;// = "resize_sw";
+ var RESIZE_BR;// = "resize_se";
+ var RESIZE_T;// = "resize_n";
+ var RESIZE_B;// = "resize_b";
+ var RESIZE_L;// = "resize_w";
+ var RESIZE_R;// = "resize_e";
+
+ var RESIZE_TLBR;// = "resize_nw_se";
+ var RESIZE_TRBL;// = "resize_ne_sw";
+
+ var WAIT;// = "wait";
+ var WAIT_ARROW;// = "waitarrow";
+ var DISABLED;// = "disabled";
+ var DRAG;// = "drag";
+ var DRAG_OPEN;// = "dragopen";
+
+ @:to public function toOpenFL():MouseCursor {
+ return @:privateAccess switch(cast this) {
+ case ARROW: MouseCursor.ARROW;
+ case CROSSHAIR: MouseCursor.__CROSSHAIR;
+ case CLICK: MouseCursor.BUTTON;
+ case IBEAM: MouseCursor.IBEAM;
+ case MOVE: MouseCursor.__MOVE;
+ case HAND: MouseCursor.HAND;
+ case DRAG: MouseCursor.HAND;
+ case DRAG_OPEN: MouseCursor.ARROW; // Could be HAND, but it might be better to use ARROW since on windows it would be weird to have the dragging cursor
+ case WAIT: MouseCursor.__WAIT;
+ case WAIT_ARROW: MouseCursor.__WAIT_ARROW;
+
+ case DISABLED: MouseCursor.ARROW;
+
+ case RESIZE_TR: MouseCursor.__RESIZE_NESW;
+ case RESIZE_BL: MouseCursor.__RESIZE_NESW;
+ case RESIZE_TL: MouseCursor.__RESIZE_NWSE;
+ case RESIZE_BR: MouseCursor.__RESIZE_NWSE;
+ case RESIZE_H: MouseCursor.__RESIZE_WE;
+ case RESIZE_V: MouseCursor.__RESIZE_NS;
+
+ case RESIZE_T: MouseCursor.__RESIZE_NS;
+ case RESIZE_B: MouseCursor.__RESIZE_NS;
+ case RESIZE_L: MouseCursor.__RESIZE_WE;
+ case RESIZE_R: MouseCursor.__RESIZE_WE;
+
+ case RESIZE_TLBR: MouseCursor.__RESIZE_NWSE;
+ case RESIZE_TRBL: MouseCursor.__RESIZE_NESW;
+ case CUSTOM: MouseCursor.__CUSTOM;
+ //default: ARROW;
+ }
+ }
+
+ @:to public function toInt():Int {
+ return switch(cast this) {
+ case ARROW: 0;
+ case CROSSHAIR: 1;
+ case CLICK: 2;
+ case IBEAM: 3;
+ case MOVE: 4;
+ case HAND: 5;
+ case DRAG: 6;
+ case DRAG_OPEN: 7;
+ case WAIT: 8;
+ case WAIT_ARROW: 9;
+
+ case DISABLED: 10;
+
+ case RESIZE_TR: 11;
+ case RESIZE_BL: 12;
+ case RESIZE_TL: 13;
+ case RESIZE_BR: 14;
+ case RESIZE_H: 15;
+ case RESIZE_V: 16;
+
+ case RESIZE_T: 17;
+ case RESIZE_B: 18;
+ case RESIZE_L: 19;
+ case RESIZE_R: 20;
+
+ case RESIZE_TLBR: 21;
+ case RESIZE_TRBL: 22;
+
+ case CUSTOM: -1;
+ }
+ }
}
\ No newline at end of file
diff --git a/source/funkin/backend/utils/WindowUtils.hx b/source/funkin/backend/utils/WindowUtils.hx
index 571f781a..d6210d9a 100644
--- a/source/funkin/backend/utils/WindowUtils.hx
+++ b/source/funkin/backend/utils/WindowUtils.hx
@@ -28,7 +28,7 @@ class WindowUtils {
static var __triedClosing:Bool = false;
public static inline function resetClosing() __triedClosing = false;
- public static inline function init() {
+ public static function init() {
resetTitle();
resetClosing();
diff --git a/source/funkin/backend/utils/XMLUtil.hx b/source/funkin/backend/utils/XMLUtil.hx
index 8ee00025..52a3bd47 100644
--- a/source/funkin/backend/utils/XMLUtil.hx
+++ b/source/funkin/backend/utils/XMLUtil.hx
@@ -150,10 +150,15 @@ class XMLUtil {
if(node.has.color)
spr.color = FlxColor.fromString(node.getAtt("color")).getDefault(0xFFFFFFFF);
+ if(node.has.angle)
+ spr.angle = Std.parseFloat(node.getAtt("angle")).getDefault(spr.angle);
+
if (node.has.playOnCountdown)
spr.skipNegativeBeats = node.att.playOnCountdown == "true";
if (node.has.beatInterval)
spr.beatInterval = Std.parseInt(node.att.beatInterval);
+ if (node.has.interval)
+ spr.beatInterval = Std.parseInt(node.att.interval);
if (node.has.beatOffset)
spr.beatOffset = Std.parseInt(node.att.beatOffset);
diff --git a/source/funkin/backend/utils/native/Mac.hx b/source/funkin/backend/utils/native/Mac.hx
index fa68700a..abdb478c 100644
--- a/source/funkin/backend/utils/native/Mac.hx
+++ b/source/funkin/backend/utils/native/Mac.hx
@@ -1,8 +1,12 @@
package funkin.backend.utils.native;
#if mac
-@:cppFileCode("#include ")
-class Mac {
+import funkin.backend.utils.NativeAPI.CodeCursor;
+import openfl.ui.Mouse;
+
+@:headerInclude('sys/sysctl.h')
+class Mac
+{
@:functionCode('
int mib [] = { CTL_HW, HW_MEMSIZE };
int64_t value = 0;
@@ -17,5 +21,39 @@ class Mac {
{
return 0;
}
+
+ public static function setMouseCursorIcon(icon:CodeCursor):Void
+ {
+ final valid:Bool = external.ExternalMac.setCursorIcon(icon.toInt(), null, 0, 0);
+
+ if (!valid)
+ Mouse.cursor = icon.toOpenFL();
+ }
}
-#end
\ No newline at end of file
+#end
+
+
+/*
+
+Cursor
+
+Description
+The arrow cursor (arrow)
+The I-beam cursor for indicating insertion points (iBeam)
+The cross-hair cursor (crosshair)
+The closed-hand cursor (closedHand)
+The open-hand cursor (openHand)
+The pointing-hand cursor (pointingHand)
+The resize-left cursor (resizeLeft)
+The resize-right cursor (resizeRight)
+The resize-left-and-right cursor (resizeLeftRight)
+The resize-up cursor (resizeUp)
+The resize-down cursor (resizeDown)
+The resize-up-and-down cursor (resizeUpDown)
+The disappearing item cursor (disappearingItem)
+The I-Beam text cursor for vertical layout (iBeamCursorForVerticalLayout).
+The not allowed cursor (operationNotAllowed).
+The drag link cursor (dragLink).
+The drag copy cursor (dragCopy).
+The contextual menu cursor (contextualMenu).
+*/
diff --git a/source/funkin/editors/EditorPicker.hx b/source/funkin/editors/EditorPicker.hx
index 8a2ebd89..7b98f0e6 100644
--- a/source/funkin/editors/EditorPicker.hx
+++ b/source/funkin/editors/EditorPicker.hx
@@ -20,7 +20,7 @@ class EditorPicker extends MusicBeatSubstate {
{
name: "Stage Editor",
iconID: 2,
- state: null
+ state: funkin.editors.stage.StageSelection
},
{
name: "UI Debug State",
diff --git a/source/funkin/editors/character/CharacterAnimButtons.hx b/source/funkin/editors/character/CharacterAnimButtons.hx
index c80913cd..01ba60d4 100644
--- a/source/funkin/editors/character/CharacterAnimButtons.hx
+++ b/source/funkin/editors/character/CharacterAnimButtons.hx
@@ -44,7 +44,10 @@ class CharacterAnimButtons extends UIButton {
editButton.autoAlpha = false;
members.push(editButton);
- editIcon = new FlxSprite(editButton.x + 8, editButton.y + 8).loadGraphic(Paths.image('editors/character/edit-button'));
+ editIcon = new FlxSprite(editButton.x + 8, editButton.y + 8).loadGraphic(Paths.image('editors/character/edit-button'), true, 16, 16);
+ editIcon.animation.add("edit", [0]);
+ editIcon.animation.add("advanced", [1]);
+ editIcon.animation.play("edit");
editIcon.antialiasing = false;
members.push(editIcon);
diff --git a/source/funkin/editors/charter/Charter.hx b/source/funkin/editors/charter/Charter.hx
index e67b59ba..94ddc68d 100644
--- a/source/funkin/editors/charter/Charter.hx
+++ b/source/funkin/editors/charter/Charter.hx
@@ -804,7 +804,7 @@ class Charter extends UIState {
}
if (s is CharterNote) cast(s, CharterNote).snappedToStrumline = true;
- if (s is UISprite) cast(s, UISprite).cursor = BUTTON;
+ if (s is UISprite) cast(s, UISprite).cursor = CLICK;
}
if (!(verticalChange == 0 && horizontalChange == 0)) {
notesGroup.sortNotes(); eventsGroup.sortEvents();
@@ -866,7 +866,7 @@ class Charter extends UIState {
case SUSTAIN_DRAG:
selectionDragging = FlxG.mouse.pressed;
if (selectionDragging) {
- currentCursor = BUTTON;
+ currentCursor = CLICK;
selection.loop(function (n:CharterNote) {
var change:Float = Math.max((mousePos.y-(FlxG.keys.pressed.SHIFT ? dragStartPos.y : quantStep(dragStartPos.y))) / 40, -n.susLength);
n.tempSusLength = change;
diff --git a/source/funkin/editors/charter/CharterEvent.hx b/source/funkin/editors/charter/CharterEvent.hx
index af8996a4..92243689 100644
--- a/source/funkin/editors/charter/CharterEvent.hx
+++ b/source/funkin/editors/charter/CharterEvent.hx
@@ -23,7 +23,7 @@ class CharterEvent extends UISliceSprite implements ICharterSelectable {
this.step = step;
this.events = events.getDefault([]);
- cursor = BUTTON;
+ cursor = CLICK;
}
public override function update(elapsed:Float) {
diff --git a/source/funkin/editors/charter/CharterEventAdd.hx b/source/funkin/editors/charter/CharterEventAdd.hx
index 038725d3..89fbf3cf 100644
--- a/source/funkin/editors/charter/CharterEventAdd.hx
+++ b/source/funkin/editors/charter/CharterEventAdd.hx
@@ -13,7 +13,7 @@ class CharterEventAdd extends UISliceSprite {
text = new UIText(0, 0, 0, "");
members.push(text);
- cursor = BUTTON;
+ cursor = CLICK;
}
public override function onHovered() {
diff --git a/source/funkin/editors/charter/CharterNote.hx b/source/funkin/editors/charter/CharterNote.hx
index bc214209..8d67adc2 100644
--- a/source/funkin/editors/charter/CharterNote.hx
+++ b/source/funkin/editors/charter/CharterNote.hx
@@ -44,7 +44,7 @@ class CharterNote extends UISprite implements ICharterSelectable {
typeText = new UIText(x, y, 0, Std.string(type));
- cursor = sustainSpr.cursor = BUTTON;
+ cursor = sustainSpr.cursor = CLICK;
moves = false;
}
@@ -139,7 +139,7 @@ class CharterNote extends UISprite implements ICharterSelectable {
sustainDraggable = false;
if (!hovered && susLength != 0) {
- UIState.state.updateSpriteRect(sustainSpr);
+ sustainSpr.updateSpriteRect();
sustainDraggable = UIState.state.isOverlapping(sustainSpr, @:privateAccess sustainSpr.__rect);
}
diff --git a/source/funkin/editors/charter/CharterStrumline.hx b/source/funkin/editors/charter/CharterStrumline.hx
index c7b332e3..032521a6 100644
--- a/source/funkin/editors/charter/CharterStrumline.hx
+++ b/source/funkin/editors/charter/CharterStrumline.hx
@@ -64,7 +64,7 @@ class CharterStrumline extends UISprite {
draggingSprite.alpha = 0.4;
draggingSprite.y = 9;
draggingSprite.antialiasing = true;
- draggingSprite.cursor = BUTTON;
+ draggingSprite.cursor = CLICK;
members.push(draggingSprite);
button = new CharterStrumlineOptions(this);
@@ -85,7 +85,7 @@ class CharterStrumline extends UISprite {
healthIcons.follow(this, ((40 * 4) - healthIcons.width) / 2, 7 + (__healthYOffset = FlxMath.lerp(__healthYOffset, draggable ? 8 : 0, 1/20)));
draggingSprite.selectable = draggable;
- UIState.state.updateSpriteRect(draggingSprite);
+ draggingSprite.updateSpriteRect();
var dragScale:Float = FlxMath.lerp(draggingSprite.scale.x, draggable ? 1 : 0.8, 1/16);
draggingSprite.scale.set(dragScale, dragScale);
diff --git a/source/funkin/editors/charter/CharterStrumlineButton.hx b/source/funkin/editors/charter/CharterStrumlineButton.hx
index a0a8c7df..39207e5f 100644
--- a/source/funkin/editors/charter/CharterStrumlineButton.hx
+++ b/source/funkin/editors/charter/CharterStrumlineButton.hx
@@ -24,7 +24,7 @@ class CharterStrumlineButton extends UISprite {
button.scale.set(0.85,0.85);
button.updateHitbox();
button.antialiasing = true;
- button.cursor = BUTTON;
+ button.cursor = CLICK;
members.push(button);
this.text = new UIText(0,0, 160, text);
@@ -58,7 +58,7 @@ class CharterStrumlineButton extends UISprite {
super.update(elapsed);
- UIState.state.updateSpriteRect(button);
+ button.updateSpriteRect();
if(UIState.state.curContextMenu == null && UIState.state.isOverlapping(button, button.__rect)) {
buttonScale.set(0.95, 0.95);
if (FlxG.mouse.justPressed && onClick != null) {
diff --git a/source/funkin/editors/extra/PropertyButton.hx b/source/funkin/editors/extra/PropertyButton.hx
index 0ffad946..4cf5a9c3 100644
--- a/source/funkin/editors/extra/PropertyButton.hx
+++ b/source/funkin/editors/extra/PropertyButton.hx
@@ -6,28 +6,36 @@ class PropertyButton extends UIButton {
public var deleteButton:UIButton;
public var deleteIcon:FlxSprite;
- public function new(property, value, parent) {
- super(0, 0, '', function () {}, 280, 35);
- members.push(propertyText = new UITextBox(5, 5, property, 100, 25));
- members.push(valueText = new UITextBox(propertyText.x + propertyText.bWidth + 10, 5, value, 135, 25));
+ public function new(property, value, parent, width:Int = 280, height:Int = 35, nameWidth:Int = 100, valueWidth:Int = 135, inputHeight:Int = 25) {
+ super(0, 0, '', function () {}, width, height);
+ members.push(propertyText = new UITextBox(5, 5, property, nameWidth, inputHeight));
+ members.push(valueText = new UITextBox(propertyText.x + propertyText.bWidth + 10, 5, value, valueWidth, inputHeight));
- deleteButton = new UIButton(valueText.x + 135, bHeight/2 - (25/2), "", function () {
+ var deleteSize = height - 5 * 2;
+ deleteButton = new UIButton(valueText.x + 135, bHeight/2 - (deleteSize/2), "", function () {
parent.remove(this);
- }, 25, 25);
+ }, deleteSize, deleteSize);
deleteButton.color = 0xFFFF0000;
deleteButton.autoAlpha = false;
members.push(deleteButton);
- deleteIcon = new FlxSprite(deleteButton.x + (15/2), deleteButton.y + 8).loadGraphic(Paths.image('editors/delete-button'));
+ deleteIcon = new FlxSprite(deleteButton.x, deleteButton.y).loadGraphic(Paths.image('editors/delete-button'));
deleteIcon.antialiasing = false;
members.push(deleteIcon);
+
+ updatePos();
}
public override function update(elapsed) {
super.update(elapsed);
- propertyText.follow(this, 5, 5);
- valueText.follow(this, propertyText.x + propertyText.bWidth, 5);
- deleteButton.follow(this, valueText.x + 135, bHeight/2 - (25/2));
- deleteIcon.follow(deleteButton, 5, 5);
+ updatePos();
+ }
+
+ public function updatePos() {
+ propertyText.follow(this, 5, bHeight/2 - (propertyText.bHeight/2));
+ valueText.follow(this, propertyText.x + propertyText.bWidth, bHeight/2 - (valueText.bHeight/2));
+ //deleteButton.follow(this, valueText.x + 135, bHeight/2 - (25/2));
+ deleteButton.follow(this, bWidth - deleteButton.bWidth - 5, bHeight/2 - deleteButton.bHeight/2);
+ deleteIcon.follow(deleteButton, deleteButton.bWidth / 2 - deleteIcon.width / 2, deleteButton.bHeight / 2 - deleteIcon.height / 2);
}
}
\ No newline at end of file
diff --git a/source/funkin/editors/import.hx b/source/funkin/editors/import.hx
index 3d64ccc2..de888fa0 100644
--- a/source/funkin/editors/import.hx
+++ b/source/funkin/editors/import.hx
@@ -3,6 +3,7 @@ package funkin.editors;
import flixel.math.FlxPoint;
import funkin.editors.ui.*;
+import funkin.editors.ui.notifications.*;
using funkin.editors.ui.UIUtil;
#end
\ No newline at end of file
diff --git a/source/funkin/editors/stage/StageCreationScreen.hx b/source/funkin/editors/stage/StageCreationScreen.hx
new file mode 100644
index 00000000..957ff896
--- /dev/null
+++ b/source/funkin/editors/stage/StageCreationScreen.hx
@@ -0,0 +1,62 @@
+package funkin.editors.stage;
+
+typedef StageCreationData = {
+ var name:String;
+ var path:String;
+}
+
+class StageCreationScreen extends UISubstateWindow {
+ private var onSave:Null -> Void = null;
+
+ public var stageNameTextBox:UITextBox;
+ public var stagePathTextBox:UITextBox;
+ public var voicesExplorer:UIFileExplorer;
+
+ public var saveButton:UIButton;
+ public var closeButton:UIButton;
+
+ public function new(?onSave:StageCreationData->Void) {
+ super();
+ if (onSave != null) this.onSave = onSave;
+ }
+
+ public override function create() {
+ winTitle = "Creating New Stage";
+
+ winWidth = 748 - 32 + 40;
+ winHeight = 220;
+
+ super.create();
+
+ function addLabelOn(ui:UISprite, text:String):UIText {
+ var text:UIText = new UIText(ui.x, ui.y - 24, 0, text);
+ ui.members.push(text);
+ return text;
+ }
+
+ var stageInfo:UIText;
+ add(stageInfo = new UIText(windowSpr.x + 20, windowSpr.y + 30 + 16, 0, "Stage Info", 28));
+
+ add(stageNameTextBox = new UITextBox(stageInfo.x, stageInfo.y + stageInfo.height + 36, "Stage Name"));
+ addLabelOn(stageNameTextBox, "Stage Name");
+
+ add(stagePathTextBox = new UITextBox(stageNameTextBox.x + 320 + 26, stageNameTextBox.y, "Stage Path"));
+ addLabelOn(stagePathTextBox, "Stage Path");
+
+ add(saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, "Save & Close", function() {
+ saveStageInfo();
+ close();
+ }, 125));
+
+ add(closeButton = new UIButton(saveButton.x - 20 - saveButton.bWidth, saveButton.y, "Cancel", close, 125));
+ closeButton.color = 0xFFFF0000;
+ }
+
+ function saveStageInfo() {
+ if (onSave != null) onSave({
+ name: stageNameTextBox.label.text,
+ path: stagePathTextBox.label.text
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/source/funkin/editors/stage/StageEditor.hx b/source/funkin/editors/stage/StageEditor.hx
new file mode 100644
index 00000000..51f20731
--- /dev/null
+++ b/source/funkin/editors/stage/StageEditor.hx
@@ -0,0 +1,1412 @@
+package funkin.editors.stage;
+
+import flixel.math.FlxAngle;
+import flixel.math.FlxPoint;
+import flixel.math.FlxRect;
+import flixel.util.FlxColor;
+import flixel.util.FlxSort;
+import funkin.backend.system.framerate.Framerate;
+import funkin.backend.utils.XMLUtil.AnimData;
+import funkin.editors.stage.elements.*;
+import funkin.editors.stage.elements.StageSpriteButton.StageSpriteEditScreen;
+import funkin.editors.ui.UIContextMenu.UIContextMenuOption;
+import funkin.game.Character;
+import funkin.game.Stage;
+import haxe.xml.Access;
+import haxe.xml.Printer;
+import lime.ui.MouseCursor;
+import openfl.ui.Mouse;
+
+using funkin.backend.utils.MatrixUtil;
+
+class StageEditor extends UIState {
+ static var __stage:String;
+ public var stage:Stage;
+
+ private var _point:FlxPoint = new FlxPoint();
+
+ public static var instance(get, null):StageEditor;
+
+ private static inline function get_instance()
+ return FlxG.state is StageEditor ? cast FlxG.state : null;
+
+ public var topMenu:Array;
+ public var topMenuSpr:UITopMenu;
+
+ public var uiGroup:FlxTypedGroup = new FlxTypedGroup();
+ public var stageSpritesWindow:UIButtonList;
+
+ public var xmlMap:Map = new Map();
+
+ public var chars:Array = [];
+ public var charMap:Map = [];
+
+ public var stageCamera:FlxCamera;
+ //public var guideCamera:FlxCamera;
+ public var uiCamera:FlxCamera;
+
+ public var selection:Selection = new Selection();
+ public var mouseMode:StageEditorMouseMode = NONE;
+ public var mousePoint:FlxPoint = new FlxPoint();
+ public var clickPoint:FlxPoint = new FlxPoint();
+ public var storedPos:FlxPoint = new FlxPoint();
+ public var storedScale:FlxPoint = new FlxPoint();
+ public var storedSkew:FlxPoint = new FlxPoint();
+ public var storedAngle:Float = 0;
+ public var angleOffset:Float = 0;
+
+ public var showCharacters:Bool = true;
+
+ public static inline var SPRITE_WINDOW_WIDTH:Int = 400;
+ public static inline var SPRITE_WINDOW_BUTTON_HEIGHT:Int = 64;
+
+ public var undos:UndoList = new UndoList();
+
+ public inline static function exID(id:String) {
+ return "stageEditor." + id;
+ }
+
+ public function new(stage:String) {
+ super();
+ if (stage != null) __stage = stage;
+ }
+
+ public override function create() {
+ super.create();
+
+ WindowUtils.suffix = " (Stage Editor)";
+ SaveWarning.selectionClass = StageSelection;
+ SaveWarning.saveFunc = () -> {_file_save(null);};
+
+ topMenu = [
+ {
+ label: "File",
+ childs: [
+ {
+ label: "Save",
+ keybind: [CONTROL, S],
+ onSelect: _file_save,
+ },
+ {
+ label: "Save As...",
+ keybind: [CONTROL, SHIFT, S],
+ onSelect: _file_saveas,
+ },
+ null,
+ {
+ label: "Exit",
+ onSelect: _file_exit
+ }
+ ]
+ },
+ {
+ label: "Edit",
+ childs: [
+ {
+ label: "Undo",
+ keybind: [CONTROL, Z],
+ onSelect: _edit_undo,
+ },
+ {
+ label: "Redo",
+ keybind: [CONTROL, SHIFT, Z],
+ onSelect: _edit_redo,
+ },
+ null,
+ {
+ label: "Edit Stage Info",
+ onSelect: (_) -> {
+ openSubState(new UISoftcodedWindow(
+ "layouts/stage/stageInfoScreen",
+ [
+ "winTitle" => "Editing Stage Info",
+ "hasSaveButton" => true,
+ "hasCloseButton" => true,
+ "stage" => stage,
+ "Stage" => Stage,
+ "exID" => exID,
+ "CEditInfo" => StageChange.CEditInfo
+ ]
+ ));
+ },
+ }
+ ]
+ },
+ {
+ label: "Select",
+ childs: [
+ {
+ label: "All",
+ keybind: [CONTROL, A],
+ onSelect: (_) -> _select_all(_),
+ },
+ {
+ label: "Deselect",
+ keybind: [CONTROL, D],
+ onSelect: (_) -> _select_deselect(_),
+ },
+ {
+ label: "Inverse",
+ keybind: [CONTROL, SHIFT, I],
+ onSelect: (_) -> _select_inverse(_),
+ },
+ ]
+ },
+ {
+ label: "View",
+ childs: [
+ {
+ label: "Zoom in",
+ keybind: [CONTROL, NUMPADPLUS],
+ onSelect: _view_zoomin
+ },
+ {
+ label: "Zoom out",
+ keybind: [CONTROL, NUMPADMINUS],
+ onSelect: _view_zoomout
+ },
+ {
+ label: "Reset zoom",
+ keybind: [CONTROL, NUMPADZERO],
+ onSelect: _view_zoomreset
+ },
+ null,
+ {
+ label: "Focus Dad",
+ //keybind: [CONTROL, NUMPADZERO],
+ onSelect: _view_focusdad
+ },
+ {
+ label: "Focus Gf",
+ //keybind: [CONTROL, NUMPADZERO],
+ onSelect: _view_focusgf
+ },
+ {
+ label: "Focus BF",
+ //keybind: [CONTROL, NUMPADZERO],
+ onSelect: _view_focusbf
+ },
+ // TODO: add support for custom character focus
+ ]
+ },
+ {
+ label: "Editor",
+ childs: [
+ {
+ label: "Show Characters",
+ //keybind: [CONTROL, NUMPADMINUS],
+ onSelect: _editor_showCharacters,
+ icon: showCharacters ? 1 : 0
+ }
+ ]
+ },
+ ];
+
+ stageCamera = FlxG.camera;
+
+ //guideCamera = new FlxCamera();
+ //guideCamera.bgColor = 0;
+
+ uiCamera = new FlxCamera();
+ uiCamera.bgColor = 0;
+
+ uiCameras = [uiCamera];
+
+ //FlxG.cameras.add(guideCamera, false);
+ FlxG.cameras.add(uiCamera, false);
+
+ // Load from xml
+ var order:Array = [];
+ var orderNodes:Array = [];
+ stage = new Stage(__stage, this, false);
+ stage.onXMLLoaded = function(xml:Access, elems:Array) {
+ return elems;
+ }
+ stage.onNodeFinished = function(node:Access, sprite:Dynamic) {
+ if(sprite is FlxSprite) {
+ sprite.moves = false;
+ }
+ if (sprite is FunkinSprite) {
+ sprite.animEnabled = false;
+ //sprite.zoomFactorEnabled = false;
+ }
+ }
+ stage.onNodeLoaded = function(node:Access, sprite:Dynamic):Dynamic {
+ var parent = new Access(node.x.parent);
+ //var name = "";
+ //trace(node, sprite);
+ if(sprite is FlxSprite) {
+ //sprite.forceIsOnScreen = true; // hack
+ }
+ if(sprite is FunkinSprite) {
+ var sprite:FunkinSprite = cast sprite;
+ //name = sprite.name;
+ sprite.extra.set(exID("node"), node);
+ sprite.extra.set(exID("type"), node.name);
+ sprite.extra.set(exID("imageFile"), '${node.getAtt("sprite").getDefault(sprite.name)}');
+ sprite.extra.set(exID("parentNode"), parent);
+ sprite.extra.set(exID("highMemory"), parent.name == "highMemory");
+ sprite.extra.set(exID("lowMemory"), parent.name == "lowMemory");
+ //sprite.active = false;
+ }
+ if(sprite is StageCharPos) {
+ var charPos:StageCharPos = cast sprite;
+ // TODO: fix default characters not being added
+ var charName = switch(charPos.name) {
+ case "boyfriend": "bf";
+ case "girlfriend": "gf";
+ default: charPos.name;
+ }
+ //name = charPos.name;
+ var char = new Character(0,0, charName, stage.isCharFlipped(charPos.name, charName == "bf"), true);
+ char.name = charName;
+ char.debugMode = true;
+ // Play first anim, and make it the last frame
+ var animToPlay = char.getAnimOrder()[0];
+ char.playAnim(animToPlay, true, NONE);
+ var lastIndx = (char.animateAtlas != null) ?
+ char.animateAtlas.anim.length - 1 :
+ char.animation.curAnim.numFrames - 1;
+ char.playAnim(animToPlay, true, NONE, false, lastIndx);
+ char.stopAnimation();
+
+ // Add it to the stage
+ char.visible = true;
+ char.alpha = 0.5;
+ function setEx(name:String, value:Dynamic) {
+ char.extra.set(exID(name), value);
+ charPos.extra.set(exID(name), value);
+ }
+ setEx("node", node);
+ setEx("pos", charPos);
+ setEx("char", char);
+
+ setEx("parentNode", parent);
+ setEx("highMemory", parent.name == "highMemory");
+ setEx("lowMemory", parent.name == "lowMemory");
+ chars.push(char);
+ }
+ order.push(sprite);
+ orderNodes.push(node);
+ xmlMap.set(sprite, node);
+
+ return sprite;
+ }
+ stage.loadXml(stage.stageXML, true);
+ add(stage);
+
+ for(char in chars) {
+ var charPos = char.extra.get(exID("pos"));
+ //var node:Access = cast char.extra.get(exID("node"));
+
+ stage.applyCharStuff(char, charPos.name, 0);
+ charMap[charPos.name] = char;
+ }
+
+ setZoom(stage.defaultZoom);
+
+ topMenuSpr = new UITopMenu(topMenu);
+ topMenuSpr.cameras = uiGroup.cameras = [uiCamera];
+
+ final margin = 0;//22;
+ final width = SPRITE_WINDOW_WIDTH;
+ final TOP_MENU_HEIGHT = 25;
+ var buttonSize:FlxPoint = FlxPoint.get(width-margin*2, SPRITE_WINDOW_BUTTON_HEIGHT);
+ stageSpritesWindow = new UIButtonList(Std.int(FlxG.width - width), TOP_MENU_HEIGHT, width, Std.int(FlxG.height - TOP_MENU_HEIGHT), "Stage Sprites", buttonSize);
+ stageSpritesWindow.collapsable = true;
+ stageSpritesWindow.topAlpha = 0.9;
+ stageSpritesWindow.middleAlpha = 0.5;
+ stageSpritesWindow.bottomAlpha = 0.5;
+ stageSpritesWindow.buttonSpacing = 0;
+ stageSpritesWindow.dragCallback = (button, oldID, newID) -> {
+ var sprite:FunkinSprite = button.getSprite();
+ var idx = members.indexOf(sprite);
+ members.splice(idx, 1);
+ members.insert(newID, sprite);
+ }
+ stageSpritesWindow.addButton.callback = () -> {
+ var lastDrawCam = stageSpritesWindow.addButton.__lastDrawCameras[0];
+ var screenPos = stageSpritesWindow.addButton.getScreenPosition(null, lastDrawCam == null ? FlxG.camera : lastDrawCam);
+ openContextMenu([
+ {
+ label: "Sprite",
+ onSelect: _sprite_new,
+ color: 0xFF00FF00,
+ icon: 2
+ },
+ {
+ label: "Box",
+ onSelect: function(_) {
+ UIState.state.displayNotification(new UIBaseNotification("Creating a box isnt implemented yet!", 2, BOTTOM_LEFT));
+ CoolUtil.playMenuSFX(WARNING, 0.45);
+ },
+ color: 0xFF00FF00,
+ icon: 2
+ },
+ {
+ label: "Character",
+ onSelect: _character_new,
+ color: 0xFF00FF00,
+ icon: 2
+ }
+ ], null, lastDrawCam.x + screenPos.x, lastDrawCam.y + screenPos.y + stageSpritesWindow.addButton.bHeight, stageSpritesWindow.addButton.bWidth);
+ screenPos.put();
+ }
+ for (i=>sprite in order) {
+ var xml = (sprite != null) ? xmlMap.get(sprite) : orderNodes[i];
+ if(xml != null) {
+ if(sprite is FunkinSprite) {
+ var sprite:FunkinSprite = cast sprite;
+ var type = sprite.extra.get(exID("type"));
+ var button:StageElementButton = (type == "box" || type == "solid") ? new StageSolidButton(0,0, sprite, xml) : new StageSpriteButton(0,0, sprite, xml);
+ sprite.extra.set(exID("button"), button);
+ stageSpritesWindow.add(button);
+ }
+ else if(sprite is StageCharPos) {
+ var charPos:StageCharPos = cast sprite;
+ var char = charMap[charPos.name];
+ var button = new StageCharacterButton(0,0, charPos, xml);
+ char.extra.set(exID("button"), button);
+ charPos.extra.set(exID("button"), button);
+ stageSpritesWindow.add(button);
+ } else if(sprite == null) {
+ var button = new StageUnknownButton(0,0, xml);
+ stageSpritesWindow.add(button);
+ }
+ }
+ }
+ uiGroup.add(stageSpritesWindow);
+
+ add(topMenuSpr);
+ add(uiGroup);
+
+ if(Framerate.isLoaded) {
+ Framerate.fpsCounter.alpha = 0.4;
+ Framerate.memoryCounter.alpha = 0.4;
+ Framerate.codenameBuildField.alpha = 0.4;
+ }
+
+ //DiscordUtil.call("onEditorLoaded", ["Stage Editor", __stage]);
+ }
+
+ override function destroy() {
+ super.destroy();
+ nextScroll = FlxDestroyUtil.destroy(nextScroll);
+ if(Framerate.isLoaded) {
+ Framerate.fpsCounter.alpha = 1;
+ Framerate.memoryCounter.alpha = 1;
+ Framerate.codenameBuildField.alpha = 1;
+ }
+ }
+
+ //private var movingCam:Bool = false;
+ //private var camDragSpeed:Float = 1.2;
+
+ private var movedTillRel:FlxPoint = FlxPoint.get(0,0);
+ private var nextScroll:FlxPoint = FlxPoint.get(0,0);
+
+ public override function update(elapsed:Float) {
+ super.update(elapsed);
+
+ if (true) {
+ if(FlxG.keys.justPressed.ANY)
+ UIUtil.processShortcuts(topMenu);
+ }
+
+ //if (character != null)
+ // characterPropertiresWindow.characterInfo.text = '${character.getNameList().length} Animations\nFlipped: ${character.flipX}\nSprite: ${character.sprite}\nAnim: ${character.getAnimName()}\nOffset: (${character.frameOffset.x}, ${character.frameOffset.y})';
+
+ currentCursor = ARROW;
+
+ mousePoint = FlxG.mouse.getWorldPosition(stageCamera, mousePoint);
+ if ((!stageSpritesWindow.hovered && !stageSpritesWindow.dragging) && !topMenuSpr.hovered) {
+ if (FlxG.mouse.wheel != 0) {
+ zoom += 0.25 * FlxG.mouse.wheel;
+ updateZoom();
+ }
+
+ var prevMode = mouseMode;
+
+ // TODO: make this work with multiple selections
+ if(selection.length == 1) {
+ for(sprite in selection) {
+ if(sprite is FunkinSprite) {
+ handleSelection(cast sprite);
+ }
+ }
+ }
+
+ //if (FlxG.mouse.justReleasedRight) {
+ // closeCurrentContextMenu();
+ // openContextMenu(topMenu[2].childs);
+ //}
+
+ if (mouseMode == NONE && prevMode == NONE) {
+ if (FlxG.mouse.pressed) {
+ var x = FlxG.mouse.deltaScreenX;
+ var y = FlxG.mouse.deltaScreenY;
+ movedTillRel.x += x; movedTillRel.y += y;
+ nextScroll.set(nextScroll.x - x, nextScroll.y - y);
+ currentCursor = HAND;
+ }
+
+ if (FlxG.mouse.justReleased) {
+ if (Math.abs(movedTillRel.x) < 16 && Math.abs(movedTillRel.y) < 16) {
+ var point = FlxG.mouse.getWorldPosition(stageCamera, _point);
+ var sprites = getRealSprites();
+ for (i in 0...sprites.length) {
+ var sprite = sprites[sprites.length - i - 1];
+ if(sprite.animateAtlas != null) continue;
+
+ calcSpriteBounds(sprite);
+ if (cast(sprite.extra.get(exID("bounds")), FlxRect).containsPoint(point)) {
+ selectSprite(sprite); break;
+ }
+ }
+ }
+ movedTillRel.set();
+ }
+ }
+ }/* else if (!FlxG.mouse.pressed)
+ currentCursor = ARROW;*/
+
+ stageCamera.scroll.set(
+ lerp(stageCamera.scroll.x, nextScroll.x, 0.35),
+ lerp(stageCamera.scroll.y, nextScroll.y, 0.35)
+ );
+
+ stageCamera.zoom = lerp(stageCamera.zoom, __camZoom, 0.125);
+
+ WindowUtils.prefix = undos.unsaved ? "* " : "";
+ SaveWarning.showWarning = undos.unsaved;
+ }
+
+ // TOP MENU OPTIONS
+ #if REGION
+ function _file_exit(_) {
+ if (undos.unsaved) SaveWarning.triggerWarning();
+ else FlxG.switchState(new StageSelection());
+ }
+
+ function _file_save(_) {
+ #if sys
+ CoolUtil.safeSaveFile(
+ '${Paths.getAssetsRoot()}/data/stages/${__stage}.xml',
+ buildStage()
+ );
+ undos.save();
+ #else
+ _file_saveas(_);
+ #end
+ }
+
+ function _file_saveas(_) {
+ openSubState(new SaveSubstate(buildStage(), {
+ defaultSaveFile: '${__stage}.xml'
+ }));
+ undos.save();
+ }
+
+ function _sprite_new(_) {
+ var node:Access = new Access(Xml.createElement("sprite"));
+ stage.stageXML.x.addChild(node.x);
+ node.att.name = "sprite_" + stageSpritesWindow.buttons.length;
+
+ var sprite:FunkinSprite = new FunkinSprite();
+ insert(members.indexOf(stage), sprite);
+ sprite.extra.set(exID("node"), node);
+ sprite.extra.set(exID("type"), node.name);
+ sprite.extra.set(exID("imageFile"), '');
+ sprite.extra.set(exID("parentNode"), stage.stageXML.x);
+ sprite.extra.set(exID("highMemory"), false);
+ sprite.extra.set(exID("lowMemory"), false);
+ xmlMap.set(sprite, node);
+
+ var button:StageSpriteButton = new StageSpriteButton(0, 0, sprite, node);
+ sprite.extra.set(exID("button"), button);
+ stageSpritesWindow.add(button);
+
+ var substate = new StageSpriteEditScreen(button);
+ substate.newSprite = true;
+ openSubState(substate);
+ }
+
+ function _character_new(_) {
+ var node:Access = new Access(Xml.createElement("char"));
+ stage.stageXML.x.addChild(node.x);
+ node.att.name = "character_" + stageSpritesWindow.buttons.length;
+
+ var charPos = new StageCharPos();
+ charPos.visible = charPos.active = false;
+ charPos.name = "character_" + stageSpritesWindow.buttons.length;
+ charPos.extra.set(exID("extraChar"), true);
+ stage.characterPoses[charPos.name] = charPos;
+
+ var char = new Character(0,0, "bf", stage.isCharFlipped(charPos.name, true), true);
+ char.name = "bf";
+ char.debugMode = true;
+ // Play first anim, and make it the last frame
+ var animToPlay = char.getAnimOrder()[0];
+ char.playAnim(animToPlay, true, NONE);
+ var lastIndx = (char.animateAtlas != null) ?
+ char.animateAtlas.anim.length - 1 :
+ char.animation.curAnim.numFrames - 1;
+ char.playAnim(animToPlay, true, NONE, false, lastIndx);
+ char.stopAnimation();
+
+ // Add it to the stage
+ char.visible = true;
+ char.alpha = 0.5;
+ function setEx(name:String, value:Dynamic) {
+ char.extra.set(exID(name), value);
+ charPos.extra.set(exID(name), value);
+ }
+ setEx("node", node);
+ setEx("pos", charPos);
+ setEx("char", char);
+
+ setEx("parentNode", stage.stageXML);
+ setEx("highMemory", false);
+ setEx("lowMemory", false);
+ chars.push(char);
+
+ stage.applyCharStuff(char, charPos.name, 0);
+ charMap[charPos.name] = char;
+
+ var button = new StageCharacterButton(0,0, charPos, node);
+ char.extra.set(exID("button"), button);
+ charPos.extra.set(exID("button"), button);
+ stageSpritesWindow.add(button);
+ }
+
+ function saveToXml(xml:Xml, name:String, value:Dynamic, ?defaultValue:Dynamic) {
+ if(value == null || value == defaultValue) return xml;
+ xml.set(name, Std.string(value));
+ return xml;
+ }
+ function savePointToXml(xml:Xml, name:String, point:FlxPoint, ?defaultValueX:Float, ?defaultValueY:Float) {
+ if (point == null) return xml;
+ if(defaultValueY == null) defaultValueY = defaultValueX;
+ if(point.x == point.y) {
+ saveToXml(xml, name, point.x, defaultValueX);
+ } else {
+ saveToXml(xml, name + "x", point.x, defaultValueX);
+ saveToXml(xml, name + "y", point.y, defaultValueY);
+ }
+ return xml;
+ }
+
+ function getBoolOfNode(node:Access, name:String) {
+ var xml:Xml = cast node;
+
+ return xml.exists(name) && xml.get(name) == "true";
+ }
+
+ function buildStage():String {
+ var xml = Xml.createElement("stage");
+ saveToXml(xml, "zoom", stage.defaultZoom, 1.05);
+ saveToXml(xml, "name", stage.stageName);
+ saveToXml(xml, "folder", stage.spritesParentFolder);
+ saveToXml(xml, "startCamPosX", stage.startCam.x, 0);
+ saveToXml(xml, "startCamPosY", stage.startCam.y, 0);
+
+ for(prop in stage.extra.keys())
+ if(!Stage.DEFAULT_ATTRIBUTES.contains(prop) && !prop.startsWith("stageEditor."))
+ saveToXml(xml, prop, stage.extra.get(prop));
+
+ var group:Xml = null;
+ var curGroup:String = null;
+
+ for(sprite in getSprites()) {
+ var button:StageElementButton = sprite.extra.get(exID("button"));
+ var newNode:Xml = null;
+ var sprite:FunkinSprite = button.getSprite();
+ if(button is StageSolidButton) {
+ var button:StageSolidButton = cast button;
+ var node:Access = cast sprite.extra.get(exID("node"));
+ Logs.trace("SOLID / BOX isnt implemented yet!");
+ } else if(button is StageSpriteButton) {
+ var button:StageSpriteButton = cast button;
+ var node:Access = cast sprite.extra.get(exID("node"));
+ var spriteXML = Xml.createElement("sprite");
+ saveToXml(spriteXML, "name", sprite.name);
+ saveToXml(spriteXML, "x", sprite.x, 0);
+ saveToXml(spriteXML, "y", sprite.y, 0);
+ saveToXml(spriteXML, "sprite", sprite.extra.get(exID("imageFile")));
+ savePointToXml(spriteXML, "scale", sprite.scale, 1);
+ savePointToXml(spriteXML, "scroll", sprite.scrollFactor, 1);
+ saveToXml(spriteXML, "skewx", sprite.skew.x, 0);
+ saveToXml(spriteXML, "skewy", sprite.skew.y, 0);
+ saveToXml(spriteXML, "alpha", sprite.alpha, 1);
+ saveToXml(spriteXML, "angle", sprite.angle, 0);
+ //saveToXml(spriteXML, "graphicSize", sprite.width, sprite.width);
+ //saveToXml(spriteXML, "graphicSizex", sprite.height, sprite.height);
+ //saveToXml(spriteXML, "graphicSizey", sprite.height, sprite.height);
+ saveToXml(spriteXML, "zoomfactor", sprite.zoomFactor, 1);
+ saveToXml(spriteXML, "updateHitbox", getBoolOfNode(node, "updateHitbox"), false);
+ saveToXml(spriteXML, "antialiasing", sprite.antialiasing, true);
+ //saveToXml(spriteXML, "width", sprite.width);
+ //saveToXml(spriteXML, "height", sprite.height);
+ saveToXml(spriteXML, "playOnCountdown", getBoolOfNode(node, "playOnCountdown"), false);
+ saveToXml(spriteXML, "interval", node.getAtt("beatInterval"), 2);
+ saveToXml(spriteXML, "interval", node.getAtt("interval"), 2);
+ saveToXml(spriteXML, "beatOffset", node.getAtt("beatOffset"), 0);
+ if(sprite.spriteAnimType != LOOP)
+ spriteXML.set("type", sprite.spriteAnimType.toString());
+ saveToXml(spriteXML, "color", sprite.color.toWebString(), "#FFFFFF");
+ // TODO: save custom parameters
+ //saveToXml(spriteXML, "flipX", sprite.flipX, false);
+ newNode = spriteXML;
+ } else if(button is StageCharacterButton) {
+ var button:StageCharacterButton = cast button;
+ var charPos:StageCharPos = button.charPos;
+ var char:Character = cast charPos.extra.get(exID("char"));
+ var node:Access = cast charPos.extra.get(exID("node"));
+ var nodeName = switch(charPos.name) {
+ case "boyfriend": "boyfriend";
+ case "dad": "dad";
+ case "girlfriend": "girlfriend";
+ default: "character";
+ }
+ var defaultPos = Stage.getDefaultPos(charPos.name);
+ var charXML:Xml = Xml.createElement(nodeName);
+ if(nodeName == "character")
+ saveToXml(charXML, "name", char.curCharacter);
+ saveToXml(charXML, "x", charPos.x, defaultPos.x);
+ saveToXml(charXML, "y", charPos.y, defaultPos.y);
+ saveToXml(charXML, "camxoffset", charPos.camxoffset, 0);
+ saveToXml(charXML, "camyoffset", charPos.camyoffset, 0);
+ saveToXml(charXML, "skewx", charPos.skewX, 0);
+ saveToXml(charXML, "skewy", charPos.skewY, 0);
+ saveToXml(charXML, "spacingx", charPos.charSpacingX, 20);
+ saveToXml(charXML, "spacingy", charPos.charSpacingY, 0);
+ saveToXml(charXML, "alpha", charPos.alpha, 1);
+ saveToXml(charXML, "angle", charPos.angle, 0);
+ saveToXml(charXML, "zoomfactor", charPos.zoomFactor, 1);
+ saveToXml(charXML, "flipX", charPos.flipX, defaultPos.flip);
+ savePointToXml(charXML, "scroll", charPos.scrollFactor, defaultPos.scroll);
+ savePointToXml(charXML, "scale", charPos.scale, 1);
+ // TODO: save custom parameters
+ newNode = charXML;
+ } else if(button is StageUnknownButton) {
+ var button:StageUnknownButton = cast button;
+ newNode = button.xml.x;
+ }
+ else {
+ Logs.trace("Unknown Stage Type : " + Type.getClassName(Type.getClass(button)));
+ Logs.trace("> Sprite : " + Type.getClassName(Type.getClass(sprite)));
+ }
+
+ if(newNode != null && sprite != null) {
+ var isLowMemory = sprite.extra.get(exID("lowMemory")) == true;
+ var isHighMemory = sprite.extra.get(exID("highMemory")) == true;
+ /* // Only if this compiled :sob:
+ var groupName:String = null;
+ if ((groupName = isLowMemory ? "low-memory" : isHighMemory ? "high-memory" : null) != null) {
+ var a = group != null && groupName != curGroup && ((group = cast xml.addChild(group)) != null);
+ (group = (group == null ? Xml.createElement(curGroup = groupName) : group)).addChild(newNode);
+ }else xml.addChild(newNode);
+ */
+
+ var groupName = isLowMemory ? "low-memory" : isHighMemory ? "high-memory" : null;
+ if(group != null && groupName != curGroup) {
+ xml.addChild(group);
+ group = null;
+ }
+ if(groupName != null)
+ (group = (group == null ? Xml.createElement(curGroup = groupName) : group)).addChild(newNode);
+ else
+ xml.addChild(newNode);
+ }
+ }
+
+ Logs.trace(Printer.print(xml, true));
+
+ return "\n" + Printer.print(xml, true);
+ }
+
+ function _edit_undo(_) {
+ var undo = undos.undo();
+ switch(undo) {
+ case null:
+ // do nothing
+ case CEditInfo(oldInfo, newInfo):
+ stage.startCam.x = oldInfo.startCamX;
+ stage.startCam.y = oldInfo.startCamY;
+ stage.defaultZoom = oldInfo.zoom;
+ stage.stageName = oldInfo.name;
+ stage.spritesParentFolder = oldInfo.folder;
+
+ for (attrib in newInfo.attrib.keys())
+ stage.extra.remove(attrib);
+ for (attrib => val in oldInfo.attrib)
+ stage.extra.set(attrib, val);
+ case CTransform(sprite, oldInfo, newInfo):
+ sprite.setPosition(oldInfo.x, oldInfo.y);
+ sprite.scale.set(oldInfo.scaleX, oldInfo.scaleY);
+ sprite.skew.set(oldInfo.skewX, oldInfo.skewY);
+ sprite.angle = oldInfo.angle;
+ cast(sprite.extra.get(exID("button")), StageElementButton).updateInfo();
+ }
+ }
+
+ function _edit_redo(_) {
+ var redo = undos.redo();
+ switch(redo) {
+ case null:
+ // do nothing
+ case CEditInfo(oldInfo, newInfo):
+ stage.startCam.x = newInfo.startCamX;
+ stage.startCam.y = newInfo.startCamY;
+ stage.defaultZoom = newInfo.zoom;
+ stage.stageName = newInfo.name;
+ stage.spritesParentFolder = newInfo.folder;
+
+ for (attrib in oldInfo.attrib.keys())
+ stage.extra.remove(attrib);
+ for (attrib => val in newInfo.attrib)
+ stage.extra.set(attrib, val);
+ case CTransform(sprite, oldInfo, newInfo):
+ sprite.setPosition(newInfo.x, newInfo.y);
+ sprite.scale.set(newInfo.scaleX, newInfo.scaleY);
+ sprite.skew.set(newInfo.skewX, newInfo.skewY);
+ sprite.angle = newInfo.angle;
+ cast(sprite.extra.get(exID("button")), StageElementButton).updateInfo();
+ }
+ }
+
+ public function selectSprite(_sprite:FunkinSprite) {
+ if(!UIUtil.getKeyState(CONTROL, PRESSED))
+ _select_deselect(null, false);
+
+ if(_sprite is FunkinSprite) {
+ if(selection.contains(_sprite))
+ selection.remove(_sprite);
+ else
+ selection.push(_sprite);
+ }
+ updateSelection();
+ }
+
+ function getSprites() {
+ return stageSpritesWindow.buttons.members.map((o) -> o.getSprite()).filter((o) -> o != null);
+ }
+ function getRealSprites() {
+ return stageSpritesWindow.buttons.members.filter(o -> o.canRender()).map((o) -> o.getSprite()).filter((o) -> o != null);
+ }
+
+ function updateSelection() {
+ var sprites = getRealSprites();
+ // Unselect all
+ for(sprite in sprites) {
+ if(sprite is FunkinSprite) {
+ var sprite:FunkinSprite = cast sprite;
+ sprite.extra.set(exID("selected"), false);
+ sprite.extra.get(exID("button")).selected = false;
+ }
+ }
+ // Mark selected as selected
+ for(sprite in selection) {
+ if(sprite is FunkinSprite) {
+ var sprite:FunkinSprite = cast sprite;
+ sprite.extra.set(exID("selected"), true);
+ sprite.extra.get(exID("button")).selected = true;
+ Logs.trace("Selected " + sprite.name);
+ }
+ }
+ }
+
+ function _select_all(_, checkSelection:Bool = true) {
+ _select_deselect(null, false);
+ var sprites = getRealSprites();
+ selection = new Selection(sprites);
+ if(checkSelection) updateSelection();
+ }
+
+ function _select_deselect(_, checkSelection:Bool = true) {
+ selection = new Selection();
+ if(checkSelection) updateSelection();
+ }
+
+ function _select_inverse(_, checkSelection:Bool = true) {
+ var oldSelection = selection;
+ _select_all(null, false);
+ for(sprite in oldSelection) {
+ selection.remove(sprite);
+ }
+ if(checkSelection) updateSelection();
+ }
+
+ var zoom(default, set):Float = 0;
+ var __camZoom(default, set):Float = 1;
+ function set_zoom(val:Float) {
+ return zoom = FlxMath.bound(val, -3.5, 1.75); // makes zooming not lag behind when continuing scrolling
+ }
+ function set___camZoom(val:Float) {
+ return __camZoom = FlxMath.bound(val, 0.1, 3);
+ }
+
+ function _view_zoomin(_) {
+ zoom += 0.25;
+ updateZoom();
+ }
+ function _view_zoomout(_) {
+ zoom -= 0.25;
+ updateZoom();
+ }
+ function _view_zoomreset(_) {
+ setZoom(stage.defaultZoom);
+ }
+
+ inline function updateZoom() {
+ __camZoom = Math.pow(2, zoom);
+ }
+
+ inline function calculateZoom(zoom:Float) {
+ return Math.log(zoom) / Math.log(2);
+ }
+
+ inline function setZoom(_zoom:Float) {
+ zoom = calculateZoom(__camZoom = _zoom);
+ }
+
+ function _view_focusdad(_) {
+ focusCharacter(charMap["dad"]);
+ }
+ function _view_focusgf(_) {
+ focusCharacter(charMap["girlfriend"]);
+ }
+ function _view_focusbf(_) {
+ focusCharacter(charMap["boyfriend"]);
+ }
+
+ function focusCharacter(char:Character) {
+ var point = char.getCameraPosition();
+ nextScroll.set(point.x - stageCamera.width / 2, point.y - stageCamera.height / 2);
+
+ setZoom(stage.defaultZoom);
+ }
+
+ function _editor_showCharacters(t) {
+ showCharacters = !showCharacters;
+ t.icon = showCharacters ? 1 : 0;
+ for(char in chars) {
+ var button:StageCharacterButton = char.extra.get(exID("button"));
+ button.isHidden = !showCharacters;
+ button.updateInfo();
+ }
+ }
+ #end
+
+ override function draw() {
+ super.draw();
+
+ if(dot == null) {
+ dot = new FlxSprite().loadGraphic(Paths.image("editors/stage/selectionDot"), true, 32, 32);
+ dot.antialiasing = true;
+ dot.animation.add("default", [0], 0, false);
+ dot.animation.add("hollow", [1], 0, false);
+ dot.animation.play("default");
+ //dot = new FlxSprite().makeGraphic(30, 30, FlxColor.WHITE);
+ dot.camera = stageCamera;
+ dot.forceIsOnScreen = true;
+
+ line = new FlxSprite().makeGraphic(30, 30, FlxColor.WHITE);
+ line.camera = stageCamera;
+ line.color = lineColor;
+ line.forceIsOnScreen = true;
+ }
+ for(sprite in selection) {
+ if(sprite is FunkinSprite) {
+ @:privateAccess if(sprite._frame == null) continue;
+ drawGuides(cast sprite);
+ }
+ }
+ }
+
+ var lineColor:FlxColor = 0xFFb794b6;
+ var circleColor:FlxColor = 0xFF99a8f2;
+ var hollowColor:FlxColor = 0xffb2beff;
+
+ function drawGuides(sprite:FlxSprite) {
+ var corners = calcSpriteBounds(sprite);
+ var funkinSprite = sprite is FunkinSprite ? cast(sprite, FunkinSprite) : null;
+
+ if(funkinSprite != null) {
+ if(funkinSprite.extra.exists(exID("buttonBoxes"))) {
+ var oldButtonBoxes:Array = cast funkinSprite.extra.get(exID("buttonBoxes"));
+ if(oldButtonBoxes != null) {
+ for(point in oldButtonBoxes) {
+ point.put();
+ }
+ }
+ }
+ }
+ var buttonBoxes = [];
+ if(funkinSprite != null) {
+ funkinSprite.extra.set(exID("buttonBoxes"), buttonBoxes);
+ }
+
+ drawLine(corners[0], corners[1]); // tl - tr
+ drawLine(corners[0], corners[2]); // tl - bl
+ drawLine(corners[1], corners[3]); // tr - br
+ drawLine(corners[2], corners[3]); // bl - br
+
+ drawLine(corners[7], corners[9], 0.3); // tc - ac // top center to angle center
+
+ // cross
+ //drawLine(corners[0], corners[3]); // tl - br
+ //drawLine(corners[1], corners[2]); // tr - bl
+
+ dot.color = circleColor;
+
+ final ANGLE_INDEX = StageEditorEdge.ROTATE_CIRCLE.toInt();
+ final CENTER_INDEX = StageEditorEdge.CENTER_CIRCLE.toInt();
+
+ dot.animation.play("default");
+
+ for(i in 0...corners.length) {
+ var corner = corners[i];
+ if(i != CENTER_INDEX) {
+ if(i == ANGLE_INDEX) {
+ dot.color = hollowColor;
+ drawDot(corner.x, corner.y);
+ dot.animation.play("hollow");
+ dot.color = circleColor;
+ drawDot(corner.x, corner.y);
+ } else {
+ drawDot(corner.x, corner.y);
+ }
+ buttonBoxes.push(corner);
+ }
+ else if(sprite.visible){
+ dot.color = circleColor;
+ drawDot(corner.x, corner.y, 0.7);
+ dot.animation.play("hollow");
+ buttonBoxes.push(corner);
+ }
+
+ }
+
+ if(funkinSprite == null) {
+ for(corner in buttonBoxes) {
+ corner.put();
+ }
+ }
+ }
+
+ function calcSpriteBounds(sprite:FlxSprite) {
+ var oldWidth = sprite.width;
+ var oldHeight = sprite.height;
+ var oldOffset = sprite.offset.clone(FlxPoint.weak());
+ var oldOrigin = sprite.origin.clone(FlxPoint.weak());
+ sprite.updateHitbox();
+ sprite.offset.copyFrom(oldOffset);
+ sprite.origin.copyFrom(oldOrigin);
+ @:privateAccess sprite.updateTrig();
+
+ var corners = sprite.getMatrixPosition([
+ // corners
+ FlxPoint.get(0, 0),
+ FlxPoint.get(1, 0),
+ FlxPoint.get(0, 1),
+ FlxPoint.get(1, 1),
+ // edges
+ FlxPoint.get(0, 0.5),
+ FlxPoint.get(1, 0.5),
+ FlxPoint.get(0.5, 1),
+ FlxPoint.get(0.5, 0),
+ // center
+ FlxPoint.get(0.5, 0.5),
+ //FlxPoint.get(0.5, -100/sprite.frameHeight) // angle
+ FlxPoint.get(0.5, -0.5), // angle
+ FlxPoint.get(1, 0) // rotate corner
+ ], sprite.camera, sprite.frameWidth, sprite.frameHeight);
+ @:privateAccess corners[corners.length - 1].add(
+ dotCheckSize * 0.35 * sprite._cosAngle + dotCheckSize * 0.35 * sprite._sinAngle,
+ -dotCheckSize * 0.35 * sprite._cosAngle + dotCheckSize * 0.35 * sprite._sinAngle
+ );
+
+ //Logs.trace("Guide at " + corners[0].x + ", " + corners[0].y + " sprite at " + sprite.x + ", " + sprite.y);
+
+ if(sprite is FunkinSprite) {
+ var sprite:FunkinSprite = cast sprite;
+ var maxX = Math.max(Math.max(corners[0].x, corners[1].x), Math.max(corners[2].x, corners[3].x));
+ var maxY = Math.max(Math.max(corners[0].y, corners[1].y), Math.max(corners[2].y, corners[3].y));
+ var minX = Math.min(Math.min(corners[0].x, corners[1].x), Math.min(corners[2].x, corners[3].x));
+ var minY = Math.min(Math.min(corners[0].y, corners[1].y), Math.min(corners[2].y, corners[3].y));
+
+ if(!sprite.extra.exists(exID("bounds"))) {
+ sprite.extra.set(exID("bounds"), new FlxRect());
+ }
+ cast(sprite.extra.get(exID("bounds")), FlxRect).set(minX, minY, maxX - minX, maxY - minY);
+ }
+
+ // reset hitbox to old values
+ sprite.width = oldWidth;
+ sprite.height = oldHeight;
+
+ return corners;
+ }
+
+ var edges:Array = [
+ //// corners
+ TOP_LEFT, //FlxPoint.get(0, 0),
+ TOP_RIGHT, //FlxPoint.get(1, 0),
+ BOTTOM_LEFT, //FlxPoint.get(0, 1),
+ BOTTOM_RIGHT, //FlxPoint.get(1, 1),
+ //// edges
+ MIDDLE_LEFT, //FlxPoint.get(0, 0.5),
+ MIDDLE_RIGHT, //FlxPoint.get(1, 0.5),
+ BOTTOM_MIDDLE, //FlxPoint.get(0.5, 1),
+ TOP_MIDDLE, //FlxPoint.get(0.5, 0),
+
+ CENTER_CIRCLE, //FlxPoint.get(0.5, -0.5)
+
+ ROTATE_CIRCLE, //FlxPoint.get(0.5, -0.5)
+ ROTATE_CORNER
+ ];
+
+ function tryUpdateHitbox(sprite:FunkinSprite) {
+ call("tryUpdateHitbox", [sprite]);
+ }
+
+ function handleSelection(sprite:FunkinSprite) {
+ if(!sprite.extra.exists(exID("buttonBoxes"))) return;
+ var buttonBoxes:Array = cast sprite.extra.get(exID("buttonBoxes"));
+
+ dotCheckSize = dot.frameWidth / 0.7/stageCamera.zoom; // basically adjust it to the zoom.
+
+ var prevMode = mouseMode;
+ if(FlxG.mouse.justPressed) {
+ for (i in StageEditorMouseMode.SKEW_TOP...(StageEditorMouseMode.SKEW_BOTTOM + 1)) {
+ var cappedI1 = Math.max(i - StageEditorMouseMode.SKEW_TOP - 1, 0);
+ var cappedI2 = Math.min(i - StageEditorMouseMode.SKEW_TOP + 1, 3);
+ var point1 = buttonBoxes[Std.int(cappedI1)];
+ var point2 = buttonBoxes[Std.int(cappedI2)];
+ if (checkLine(point1, point2, point2.x - point1.x, point2.y - point1.y)) {
+ mousePoint.copyTo(clickPoint);
+ storedPos.set(sprite.x, sprite.y);
+ storedSkew.copyFrom(sprite.skew);
+ storedScale.copyFrom(sprite.scale);
+ storedAngle = sprite.angle;
+ mouseMode = i;
+ }
+ }
+
+ for(i=>edge in edges) {
+ if(checkDot(buttonBoxes[i])) {
+ mouseMode = switch(edge) {
+ case TOP_LEFT: SCALE_TOP_LEFT;
+ case TOP_MIDDLE: SCALE_TOP;
+ case TOP_RIGHT: SCALE_TOP_RIGHT;
+ case MIDDLE_LEFT: SCALE_LEFT;
+ case MIDDLE_RIGHT: SCALE_RIGHT;
+ case BOTTOM_LEFT: SCALE_BOTTOM_LEFT;
+ case BOTTOM_MIDDLE: SCALE_BOTTOM;
+ case BOTTOM_RIGHT: SCALE_BOTTOM_RIGHT;
+ case CENTER_CIRCLE: MOVE_CENTER;
+ case ROTATE_CIRCLE:
+ angleOffset = 90;
+ ROTATE;
+ case ROTATE_CORNER:
+ angleOffset = Math.atan(sprite.height / sprite.width) * FlxAngle.TO_DEG;
+ ROTATE;
+ default: NONE;
+ }
+ Logs.trace("Clicked Dot: " + mouseMode.toString());
+ mousePoint.copyTo(clickPoint);
+ storedPos.set(sprite.x, sprite.y);
+ storedSkew.set(sprite.skew.x, sprite.skew.y);
+ storedScale.copyFrom(sprite.scale);
+ storedAngle = sprite.angle;
+ }
+
+ if(mouseMode == MOVE_CENTER){
+ trace(mouseMode);
+ storedPos.set(sprite.x, sprite.y);
+ }
+ }
+ }
+ for(i=>edge in edges) {
+ if(checkDot(buttonBoxes[i])) {
+ // TODO: make it show both sided arrows when resizing, unless its at minimum size then show only one
+ // TODO: make this rotate with the sprite
+ currentCursor = switch(edge) {
+ // RESIZE_NESW; //RESIZE_NS; //RESIZE_NWSE; //RESIZE_WE;
+ case TOP_LEFT: RESIZE_TL;
+ case TOP_MIDDLE: RESIZE_T;
+ case TOP_RIGHT: RESIZE_TR;
+ case MIDDLE_LEFT: RESIZE_L;
+ case CENTER_CIRCLE:
+ #if (mac) FlxG.mouse.pressed ? DRAG : DRAG_OPEN
+ #elseif (linux) FlxG.mouse.pressed ? DRAG : CLICK
+ #else MOVE #end;
+ case MIDDLE_RIGHT: RESIZE_R;
+ case BOTTOM_LEFT: RESIZE_BL;
+ case BOTTOM_MIDDLE: RESIZE_B;
+ case BOTTOM_RIGHT: RESIZE_BR;
+ //case TOP_LEFT | BOTTOM_RIGHT: MouseCursor.RESIZE_NWSE;
+ //case TOP_MIDDLE | BOTTOM_MIDDLE: MouseCursor.RESIZE_NS;
+ //case TOP_RIGHT | BOTTOM_LEFT: MouseCursor.RESIZE_NESW;
+ //case MIDDLE_LEFT | MIDDLE_RIGHT: MouseCursor.RESIZE_WE;
+
+ case ROTATE_CIRCLE | ROTATE_CORNER: FlxG.mouse.pressed ? DRAG : #if mac DRAG_OPEN #else CLICK #end;
+ default: ARROW;
+ }
+ break;
+ }
+ }
+
+ mouseMode = (FlxG.mouse.justReleased) ? NONE : mouseMode;
+
+ if (prevMode == NONE && mouseMode == NONE) return;
+
+ if (prevMode != NONE && mouseMode == NONE) {
+ undos.addToUndo(CTransform(sprite, {
+ x: storedPos.x,
+ y: storedPos.y,
+ scaleX: storedScale.x,
+ scaleY: storedScale.y,
+ skewX: storedSkew.x,
+ skewY: storedSkew.y,
+ angle: storedAngle
+ }, {
+ x: sprite.x,
+ y: sprite.y,
+ scaleX: sprite.scale.x,
+ scaleY: sprite.scale.y,
+ skewX: sprite.skew.x,
+ skewY: sprite.skew.y,
+ angle: sprite.angle
+ }));
+ }
+
+ if(mouseMode == NONE) return;
+
+ var relative = clickPoint.subtractNew(mousePoint);
+ call(mouseMode.toString(), [sprite, relative]);
+ cast(sprite.extra.get(exID("button")), StageElementButton).updateInfo();
+ relative.put();
+ }
+
+ var dotCheckSize:Float = 53;
+
+ function checkDot(point:FlxPoint):Bool {
+ if(point!=null){
+ var rect = new FlxRect(point.x - dotCheckSize/2, point.y - dotCheckSize/2, dotCheckSize, dotCheckSize);
+ return rect.containsPoint(mousePoint);
+ }
+ return false;
+ }
+
+ inline function drawDot(x:Float, y:Float, ?scale:Float = 1) {
+ dot.setPosition(x, y);
+ dot.scale.set(0.7/stageCamera.zoom * scale, 0.7/stageCamera.zoom * scale);
+ dot.x -= dot.width / 2;
+ dot.y -= dot.height / 2;
+ dot.draw();
+ }
+
+ function checkLine(point1:FlxPoint, point2:FlxPoint, dx:Float, dy:Float) {
+ var leftRegion = Math.min(point1.x, point2.x) - dotCheckSize * 0.2;
+ var rightRegion = Math.max(point1.x, point2.x) + dotCheckSize * 0.2;
+ var topRegion = Math.min(point1.y, point2.y) - dotCheckSize * 0.2;
+ var bottomRegion = Math.max(point1.y, point2.y) + dotCheckSize * 0.2;
+
+ if (dx == 0.0 || dy == 0.0)
+ return (mousePoint.x >= leftRegion && mousePoint.x <= rightRegion)
+ && (mousePoint.y >= topRegion && mousePoint.y <= bottomRegion);
+
+ var inc = dx * ((mousePoint.y - point1.y) / dy);
+ leftRegion += inc;
+ rightRegion += inc;
+
+ inc = dy * ((mousePoint.x - point1.x) / dx);
+ topRegion += inc;
+ bottomRegion += inc;
+
+ return (mousePoint.x >= leftRegion && mousePoint.x <= rightRegion)
+ && (mousePoint.y >= topRegion && mousePoint.y <= bottomRegion);
+ }
+
+ function drawLine(point1:FlxPoint, point2:FlxPoint, sizeModify:Float = 1) {
+ var dx:Float = point2.x - point1.x;
+ var dy:Float = point2.y - point1.y;
+
+ var angle:Float = Math.atan2(dy, dx);
+ var distance:Float = Math.sqrt(dx * dx + dy * dy);
+
+ line.setPosition(point1.x, point1.y);
+ line.angle = angle * FlxAngle.TO_DEG;
+ line.origin.set(0, line.frameHeight / 2);
+ line.scale.x = distance / line.frameWidth;
+ line.scale.y = 0.20/stageCamera.zoom * sizeModify;
+ //line.x -= line.width / 2;
+ line.y -= line.height / 2;
+ line.draw();
+ // Reset the angle and scale
+ line.angle = 0;
+ line.scale.x = line.scale.y = 1;
+ line.updateHitbox();
+ }
+
+ var dot:FlxSprite = null;
+ var line:FlxSprite = null;
+}
+
+typedef StageInfo = {
+ name:String,
+ folder:String,
+ startCamX:Float,
+ startCamY:Float,
+ zoom:Float,
+ attrib:Map
+};
+
+typedef StageSprInfo = {
+ x:Float,
+ y:Float,
+ scaleX:Float,
+ scaleY:Float,
+ skewX:Float,
+ skewY:Float, // i dont wanna be cloning a bunch of FlxPoints. Theres a pool for a reason.
+ angle:Float
+}
+
+enum StageChange {
+ CEditInfo(oldInfo:StageInfo, newInfo:StageInfo);
+ CTransform(sprite:FunkinSprite, oldInfo:StageSprInfo, newInfo:StageSprInfo);
+}
+
+@:forward abstract Selection(Array) from Array to Array {
+ public inline function new(?array:Array)
+ 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));
+ // }
+ //}
+}
+
+enum abstract StageEditorMouseMode(Int) from Int to Int {
+ var NONE;
+
+ var SCALE_LEFT;
+ var SCALE_BOTTOM;
+ var SCALE_TOP;
+ var SCALE_RIGHT;
+ var SCALE_TOP_LEFT;
+ var SCALE_TOP_RIGHT;
+ var SCALE_BOTTOM_LEFT;
+ var SCALE_BOTTOM_RIGHT;
+
+ var MOVE_CENTER;
+
+ var SKEW_TOP;
+ var SKEW_LEFT;
+ var SKEW_RIGHT;
+ var SKEW_BOTTOM;
+
+ var ROTATE;
+
+ public function toString():String {
+ return switch(cast this) {
+ case NONE: "NONE";
+ case SCALE_LEFT: "SCALE_LEFT";
+ case SCALE_BOTTOM: "SCALE_BOTTOM";
+ case SCALE_TOP: "SCALE_TOP";
+ case SCALE_RIGHT: "SCALE_RIGHT";
+ case SCALE_TOP_LEFT: "SCALE_TOP_LEFT";
+ case SCALE_TOP_RIGHT: "SCALE_TOP_RIGHT";
+ case SCALE_BOTTOM_LEFT: "SCALE_BOTTOM_LEFT";
+ case SCALE_BOTTOM_RIGHT: "SCALE_BOTTOM_RIGHT";
+ case MOVE_CENTER: "MOVE_CENTER";
+ case SKEW_LEFT: "SKEW_LEFT";
+ case SKEW_BOTTOM: "SKEW_BOTTOM";
+ case SKEW_TOP: "SKEW_TOP";
+ case SKEW_RIGHT: "SKEW_RIGHT";
+ case ROTATE: "ROTATE";
+ }
+ }
+}
+
+enum abstract StageEditorEdge(Int) {
+ var NONE = -1;
+
+ var TOP_LEFT = 0;
+ var TOP_MIDDLE;
+ var TOP_RIGHT;
+ var MIDDLE_LEFT;
+ //var MIDDLE_MIDDLE;
+ var MIDDLE_RIGHT;
+ var BOTTOM_LEFT;
+ var BOTTOM_MIDDLE;
+ var BOTTOM_RIGHT;
+ var CENTER_CIRCLE;
+
+ var ROTATE_CIRCLE;
+ var ROTATE_CORNER;
+
+ public function toString():String {
+ return switch(cast this) {
+ case NONE: "NONE";
+ case TOP_LEFT: "TOP_LEFT";
+ case TOP_MIDDLE: "TOP_MIDDLE";
+ case TOP_RIGHT: "TOP_RIGHT";
+ case MIDDLE_LEFT: "MIDDLE_LEFT";
+ //case MIDDLE_MIDDLE: "MIDDLE_MIDDLE";
+ case MIDDLE_RIGHT: "MIDDLE_RIGHT";
+ case BOTTOM_LEFT: "BOTTOM_LEFT";
+ case BOTTOM_MIDDLE: "BOTTOM_MIDDLE";
+ case BOTTOM_RIGHT: "BOTTOM_RIGHT";
+ case CENTER_CIRCLE: "CENTER_CIRCLE";
+ case ROTATE_CIRCLE: "ROTATE_CIRCLE";
+ case ROTATE_CORNER: "ROTATE_CORNER";
+ }
+ }
+
+ public function toInt():Int {
+ return switch(cast this) {
+ case NONE: -1;
+ case TOP_LEFT: 0;
+ case TOP_MIDDLE: 1;
+ case TOP_RIGHT: 2;
+ case MIDDLE_LEFT: 3;
+
+ case MIDDLE_RIGHT: 4;
+ case BOTTOM_LEFT: 5;
+ case BOTTOM_MIDDLE: 6;
+ case BOTTOM_RIGHT: 7;
+ case CENTER_CIRCLE: 8;
+ case ROTATE_CIRCLE: 9;
+ case ROTATE_CORNER: 10;
+ }
+ }
+}
+
+class StageXMLEditScreen extends UISoftcodedWindow {
+ public var xml:Access;
+ public var saveCallback:Void->Void;
+
+ public function new(xml:Access, saveCallback:Void->Void, type:String = "Unknown") {
+ this.xml = xml;
+ this.saveCallback = saveCallback;
+ super("layouts/stage/xmlEditScreen", [
+ "stage" => StageEditor.instance.stage,
+ "xml" => xml,
+ "exID" => StageEditor.exID,
+ "type" => type
+ ]);
+ }
+
+ override function saveData() {
+ super.saveData();
+ if(saveCallback != null) saveCallback();
+ }
+}
diff --git a/source/funkin/editors/stage/StageSelection.hx b/source/funkin/editors/stage/StageSelection.hx
new file mode 100644
index 00000000..0f0fc06c
--- /dev/null
+++ b/source/funkin/editors/stage/StageSelection.hx
@@ -0,0 +1,65 @@
+package funkin.editors.stage;
+
+import funkin.editors.stage.StageCreationScreen.StageCreationData;
+import funkin.game.Stage;
+import funkin.options.OptionsScreen;
+import funkin.options.type.NewOption;
+import funkin.options.type.OptionType;
+import funkin.options.type.TextOption;
+
+class StageSelection extends EditorTreeMenu
+{
+ public var stages:Array = [];
+
+ public override function create()
+ {
+ bgType = "charter";
+ super.create();
+
+ var modsList:Array = Stage.getList(true, true);
+
+ var list:Array = [
+ for (stage in (modsList.length == 0 ? Stage.getList(false, true) : modsList)) {
+ stages.push(stage);
+ new TextOption(stage, "Press ACCEPT to edit this stage.",
+ function() {
+ FlxG.switchState(new StageEditor(stage));
+ });
+ }
+ ];
+
+ list.insert(0, new NewOption("New Stage", "New Stage", function() {
+ FlxG.state.openSubState(new StageCreationScreen(saveStage));
+ }));
+
+ main = new OptionsScreen("Stage Editor", "Select a stage to edit. HSCRIPT only stages are not supported.", list);
+
+ //DiscordUtil.call("onEditorTreeLoaded", ["Stage Editor"]);
+ }
+
+ public function saveStage(creation:StageCreationData) {
+ if ([for (s in stages) s.toLowerCase()].contains(creation.name.toLowerCase())) {
+ openSubState(new UIWarningSubstate("Creating Stage: Error!", "The stage you are trying to create alreadly exists, if you would like to override it delete the stage first!", [
+ {label: "Ok", color: 0xFFFF0000, onClick: function(t) {}}
+ ]));
+ return;
+ }
+
+ #if sys
+ // Save File
+ CoolUtil.safeSaveFile('${Paths.getAssetsRoot()}/data/stages/${creation.name}.xml', '\n\n');
+ #end
+
+ // Add to List
+ var option = new TextOption(creation.name, "Press ACCEPT to edit this stage.", function() {
+ FlxG.switchState(new StageEditor(creation.name));
+ });
+ optionsTree.members[optionsTree.members.length-1].insert(1, option);
+ }
+
+ override function createPost() {
+ super.createPost();
+
+ main.changeSelection(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
new file mode 100644
index 00000000..d74e8834
--- /dev/null
+++ b/source/funkin/editors/stage/elements/StageCharacterButton.hx
@@ -0,0 +1,119 @@
+package funkin.editors.stage.elements;
+
+import funkin.editors.stage.StageEditor.StageXMLEditScreen;
+import funkin.game.Character;
+import funkin.game.Stage.StageCharPos;
+import haxe.xml.Access;
+
+class StageCharacterButton extends StageElementButton {
+ public var charPos:StageCharPos;
+ public var char:Character;
+
+ public function new(x:Float,y:Float, charPos:StageCharPos, xml:Access) {
+ this.charPos = charPos;
+ this.char = charPos.extra.get(StageEditor.exID("char"));
+ super(x,y, xml);
+
+ color = 0xff7aa8ff;
+
+ hasAdvancedEdit = true;
+
+ updateInfo();
+ }
+
+ public override function update(elapsed:Float) {
+ super.update(elapsed);
+ }
+
+ public override function updateInfo() {
+ char.visible = !isHidden;
+ char.alpha = 0.5 * charPos.alpha;
+ var charScale = (char.xml.has.scale ? Std.parseFloat(char.xml.att.scale).getDefault(1) : 1);
+ charPos.scale.x = char.scale.x / charScale;
+ charPos.scale.y = char.scale.y / charScale;
+ charPos.x = char.x;
+ charPos.y = char.y;
+ char.scale.copyFrom(charPos.scale);
+ super.updateInfo();
+ char.scale.scale(charScale);
+ }
+
+ public override function getSprite():FunkinSprite {
+ return char;
+ }
+
+ public override function onSelect() {
+ StageEditor.instance.selectSprite(char);
+ }
+
+ /*public override function onSelect() {
+ UIState.state.displayNotification(new UIBaseNotification("Selecting a character isnt implemented yet!", 2, BOTTOM_LEFT));
+ CoolUtil.playMenuSFX(WARNING, 0.45);
+ }*/
+
+ public override function onEdit() {
+ if(!FlxG.keys.pressed.SHIFT) {
+ FlxG.state.openSubState(new StageCharacterEditScreen(this));
+ } else {
+ FlxG.state.openSubState(new StageXMLEditScreen(this.xml, updateInfo, "Character"));
+ }
+ }
+
+ //public override function onEdit() {
+ // UIState.state.displayNotification(new UIBaseNotification("Editing a character isnt implemented yet!", 2, BOTTOM_LEFT));
+ // CoolUtil.playMenuSFX(WARNING, 0.45);
+ //}
+
+ public override function onVisiblityToggle() {
+ isHidden = !isHidden;
+ updateInfo();
+ }
+
+ public override function getName():String {
+ return charPos.name;
+ }
+
+ public override function getPos():FlxPoint {
+ return FlxPoint.get(charPos.x, charPos.y);
+ }
+
+ public override function updatePos() {
+ super.updatePos();
+ }
+}
+
+class StageCharacterEditScreen extends UISoftcodedWindow {
+ public var button:StageCharacterButton;
+ public var char:Character;
+ public var charPos:StageCharPos;
+
+ public function new(button:StageCharacterButton) {
+ this.button = button;
+ this.char = button.char;
+ this.charPos = button.charPos;
+ super("layouts/stage/characterEditScreen", [
+ "stage" => StageEditor.instance.stage,
+ "char" => char,
+ "charPos" => charPos,
+ "button" => button,
+ "xml" => button.xml,
+ "exID" => StageEditor.exID,
+ "getEx" => function(name:String):Dynamic {
+ return char.extra.get(StageEditor.exID(name));
+ },
+ "setEx" => function(name:String, value:Dynamic) {
+ char.extra.set(StageEditor.exID(name), value);
+ charPos.extra.set(StageEditor.exID(name), value);
+ },
+ ]);
+ }
+
+ public override function create() {
+ super.create();
+ }
+
+ public override function saveData() {
+ super.saveData();
+ button.updateInfo();
+ }
+}
\ No newline at end of file
diff --git a/source/funkin/editors/stage/elements/StageElementButton.hx b/source/funkin/editors/stage/elements/StageElementButton.hx
new file mode 100644
index 00000000..2dce7c63
--- /dev/null
+++ b/source/funkin/editors/stage/elements/StageElementButton.hx
@@ -0,0 +1,218 @@
+package funkin.editors.stage.elements;
+
+import flixel.util.FlxColor;
+import haxe.xml.Access;
+
+using flixel.util.FlxColorTransformUtil;
+
+class StageElementButton extends UIButton {
+ public var xml:Access;
+
+ public var editButton:UIButton;
+ public var editIcon:FlxSprite;
+
+ public var visibilityButton:UIButton;
+ public var visibilityIcon:FlxSprite;
+
+ public var deleteButton:UIButton;
+ public var deleteIcon:FlxSprite;
+
+ public var isHidden:Bool = false;
+ public var hasAdvancedEdit:Bool = false;
+
+ public var selected:Bool = false;
+
+ public var tagColor:UISliceSprite;
+
+ public function new(x:Float,y:Float, xml:Access) {
+ this.xml = xml;
+ super(x,y, getInfoText(), function () {
+ onSelect();
+ }, StageEditor.SPRITE_WINDOW_WIDTH, StageEditor.SPRITE_WINDOW_BUTTON_HEIGHT);
+ autoAlpha = false;
+
+ tagColor = new UISliceSprite(x, y, 10, StageEditor.SPRITE_WINDOW_BUTTON_HEIGHT, 'editors/ui/button');
+ tagColor.alpha = 1; // Make entire sprite transparent
+ tagColor.selectable = false;
+ tagColor.active = false;
+ //members.push(tagColor);
+
+ topAlpha = middleAlpha = bottomAlpha = 0.7;
+
+ field.alignment = LEFT;
+
+ visibilityButton = new UIButton(x+282+17, y, "", function () {
+ onVisiblityToggle();
+ }, 32);
+ visibilityButton.autoAlpha = false;
+ members.push(visibilityButton);
+
+ visibilityIcon = new FlxSprite(visibilityButton.x + 8, visibilityButton.y + 8).loadGraphic(Paths.image('editors/stage/visible-icon'), true, 16, 16);
+ visibilityIcon.animation.add("visible", [0]);
+ visibilityIcon.animation.add("invisible", [1]);
+ visibilityIcon.animation.play("visible");
+ visibilityIcon.antialiasing = false;
+ visibilityIcon.updateHitbox();
+ members.push(visibilityIcon);
+
+ editButton = new UIButton(visibilityButton.x+32+17, y, "", function () {
+ onEdit();
+ }, 32);
+ editButton.frames = Paths.getFrames("editors/ui/grayscale-button");
+ editButton.color = FlxColor.YELLOW;
+ editButton.autoAlpha = false;
+ members.push(editButton);
+
+ editIcon = new FlxSprite(editButton.x + 8, editButton.y + 8).loadGraphic(Paths.image('editors/character/edit-button'), true, 16, 16);
+ editIcon.animation.add("edit", [0]);
+ editIcon.animation.add("advanced", [1]);
+ editIcon.antialiasing = false;
+ members.push(editIcon);
+
+ deleteButton = new UIButton(editButton.x+32+17, y, "", function () {
+ onDelete();
+ }, 32);
+ deleteButton.color = FlxColor.RED;
+ deleteButton.autoAlpha = false;
+ members.push(deleteButton);
+
+ deleteIcon = new FlxSprite(deleteButton.x + 8, deleteButton.y + 8).loadGraphic(Paths.image('editors/stage/trash-icon'));
+ deleteIcon.antialiasing = false;
+ members.push(deleteIcon);
+
+ setEditNormal();
+ }
+
+ public function setEditNormal() {
+ editIcon.animation.play("edit");
+ editButton.color = FlxColor.YELLOW;
+ }
+
+ public function setEditAdvanced() {
+ editIcon.animation.play("advanced");
+ editButton.color = 0xFFFF5B0F;
+ }
+
+
+ var _lastSelected:Bool = false;
+
+ public override function update(elapsed:Float) {
+ editButton.selectable = visibilityButton.selectable = deleteButton.selectable = selectable;
+ editButton.shouldPress = visibilityButton.shouldPress = deleteButton.shouldPress = shouldPress;
+
+ hovered = !deleteButton.hovered;
+ updatePos();
+ super.update(elapsed);
+ field.x += 12;
+
+ if(hasAdvancedEdit) {
+ if(FlxG.keys.pressed.SHIFT) {
+ setEditAdvanced();
+ } else {
+ setEditNormal();
+ }
+ }
+
+ tagColor.color = color;
+ }
+
+ //private var _lastHovered:Bool = false;
+
+ public override function draw() {
+ //if(_lastHovered != (_lastHovered = hovered && !pressed)) {
+ if(_lastSelected != (_lastSelected = selected)) {
+ updateColorTransform();
+ }
+ super.draw();
+ }
+
+ /*public override function setFrameOffset() {
+ var _frameOffset = 0;
+ if(selected) _frameOffset = 9;
+ if(hovered && pressed) _frameOffset = 18;
+ framesOffset = _frameOffset;
+ }*/
+
+ public override function updateColorTransform() {
+ super.updateColorTransform();
+
+ if(selected) {
+ useColorTransform = true;
+ colorTransform.setOffsets(70, 70, 70, 0);
+ } else {
+ colorTransform.setOffsets(0, 0, 0, 0);
+ }
+ }
+
+ public function updateInfo() {
+ field.text = getInfoText();
+
+ visibilityIcon.animation.play(isHidden ? "invisible" : "visible");
+ visibilityIcon.alpha = !isHidden ? 1 : 0.5;
+ }
+
+ public function updatePos() {
+ // buttons
+ var spacing = 8;
+ var buttonY = y + (bHeight - 32) / 2;
+ deleteButton.x = bWidth - deleteButton.bWidth - spacing;
+ deleteButton.y = buttonY;
+ editButton.x = deleteButton.x - editButton.bWidth - spacing;
+ editButton.y = buttonY;
+ visibilityButton.x = editButton.x - visibilityButton.bWidth - spacing;
+ visibilityButton.y = buttonY;
+ //deleteButton.x = (editButton.x = (visibilityButton.x = (x+282+17))+32+17)+32+17;
+ //deleteButton.y = editButton.y = visibilityButton.y = y;
+ // icons
+ visibilityIcon.x = visibilityButton.x + 8; visibilityIcon.y = visibilityButton.y + 8;
+ editIcon.x = editButton.x + 8; editIcon.y = editButton.y + 8;
+ deleteIcon.x = deleteButton.x + 8; deleteIcon.y = deleteButton.y + 8;
+
+ tagColor.x = x;// + bWidth - tagColor.bWidth;
+ tagColor.y = y;
+ }
+
+ public function getSprite():FunkinSprite {
+ return null;
+ }
+
+ public function canRender() {
+ return true;
+ }
+
+ public function getName():String {
+ return "UNKNOWN";
+ }
+
+ public function onSelect() {
+ // TODO: implement
+ }
+
+ public function onVisiblityToggle() {
+ // TODO: implement
+ }
+
+ public function onEdit() {
+ // TODO: implement
+ }
+
+ public function onDelete() {
+ // TODO: implement
+ }
+
+ public function getPos():FlxPoint {
+ return FlxPoint.get(-1, -1);
+ }
+
+ public function getInfoText():String {
+ var pos = getPos();
+ var text = '${getName()} (${CoolUtil.quantize(pos.x, 100)}, ${CoolUtil.quantize(pos.y, 100)})';
+ var sprite = getSprite();
+ if(sprite != null) {
+ text += '\nScale: (${CoolUtil.quantize(sprite.scale.x, 100)}, ${CoolUtil.quantize(sprite.scale.y, 100)})';
+ text += '\nScroll: (${CoolUtil.quantize(sprite.scrollFactor.x, 100)}, ${CoolUtil.quantize(sprite.scrollFactor.y, 100)})';
+ }
+ pos.put();
+ return text;
+ }
+}
\ No newline at end of file
diff --git a/source/funkin/editors/stage/elements/StageSolidButton.hx b/source/funkin/editors/stage/elements/StageSolidButton.hx
new file mode 100644
index 00000000..6ded6d26
--- /dev/null
+++ b/source/funkin/editors/stage/elements/StageSolidButton.hx
@@ -0,0 +1,16 @@
+package funkin.editors.stage.elements;
+
+import flixel.util.FlxColor;
+import haxe.xml.Access;
+
+class StageSolidButton extends StageSpriteButton {
+
+ public function new(x:Float,y:Float, sprite:FunkinSprite, xml:Access) {
+ super(x,y, sprite, xml);
+ color = 0xFFD9FF50;
+ }
+
+ public override function onEdit() {
+ // TODO: implement
+ }
+}
\ No newline at end of file
diff --git a/source/funkin/editors/stage/elements/StageSpriteButton.hx b/source/funkin/editors/stage/elements/StageSpriteButton.hx
new file mode 100644
index 00000000..485eca9f
--- /dev/null
+++ b/source/funkin/editors/stage/elements/StageSpriteButton.hx
@@ -0,0 +1,114 @@
+package funkin.editors.stage.elements;
+
+import funkin.editors.stage.StageEditor.StageXMLEditScreen;
+import funkin.editors.stage.elements.StageElementButton;
+import funkin.editors.ui.UISoftcodedWindow;
+import haxe.xml.Access;
+
+class StageSpriteButton extends StageElementButton {
+ public var sprite:FunkinSprite;
+
+ public function new(x:Float,y:Float, sprite:FunkinSprite, xml:Access) {
+ this.sprite = sprite;
+ super(x,y, xml);
+
+ //color = 0xFFD9FF50;
+
+ hasAdvancedEdit = true;
+
+ updateInfo();
+ }
+
+ public override function update(elapsed:Float) {
+ super.update(elapsed);
+ }
+
+ public override function updateInfo() {
+ sprite.visible = !isHidden;
+ super.updateInfo();
+ }
+
+ public override function getSprite():FunkinSprite {
+ return sprite;
+ }
+
+ public override function onSelect() {
+ StageEditor.instance.selectSprite(sprite);
+ }
+
+ public override function onVisiblityToggle() {
+ isHidden = !isHidden;
+ updateInfo();
+ }
+
+ public override function onEdit() {
+ if(!FlxG.keys.pressed.SHIFT) {
+ FlxG.state.openSubState(new StageSpriteEditScreen(this));
+ } else {
+ FlxG.state.openSubState(new StageXMLEditScreen(this.xml, updateInfo, "Sprite"));
+ }
+ }
+
+ public override function onDelete() {
+ sprite.destroy();
+ xml.x.parent.removeChild(xml.x);
+ StageEditor.instance.stage.stageSprites.remove(sprite.name);
+ StageEditor.instance.xmlMap.remove(sprite);
+ StageEditor.instance.stageSpritesWindow.remove(this);
+ }
+
+ public override function getName():String {
+ return xml.att.name;
+ }
+
+ public override function getPos():FlxPoint {
+ return FlxPoint.get(sprite.x, sprite.y);
+ }
+
+ public override function updatePos() {
+ super.updatePos();
+ }
+}
+
+class StageSpriteEditScreen extends UISoftcodedWindow {
+ public var newSprite:Bool = false;
+ public var button:StageSpriteButton;
+ public var sprite:FunkinSprite;
+ var isSaving:Bool = false;
+
+ public function new(button:StageSpriteButton) {
+ this.button = button;
+ this.sprite = button.getSprite();
+ super("layouts/stage/spriteEditScreen", [
+ "stage" => StageEditor.instance.stage,
+ "sprite" => sprite,
+ "button" => button,
+ "xml" => button.xml,
+ "exID" => StageEditor.exID,
+ "getEx" => function(name:String):Dynamic {
+ return sprite.extra.get(StageEditor.exID(name));
+ },
+ "setEx" => function(name:String, value:Dynamic) {
+ sprite.extra.set(StageEditor.exID(name), value);
+ },
+ ]);
+ }
+
+ public override function create() {
+ super.create();
+ }
+
+ public override function saveData() {
+ isSaving = true;
+ super.saveData();
+ button.updateInfo();
+ }
+
+ public override function close() {
+ if (!isSaving && newSprite) {
+ button.onDelete();
+ trace("deleting sprite");
+ }
+ super.close();
+ }
+}
\ No newline at end of file
diff --git a/source/funkin/editors/stage/elements/StageUnknownButton.hx b/source/funkin/editors/stage/elements/StageUnknownButton.hx
new file mode 100644
index 00000000..12c8c5d3
--- /dev/null
+++ b/source/funkin/editors/stage/elements/StageUnknownButton.hx
@@ -0,0 +1,105 @@
+package funkin.editors.stage.elements;
+
+import funkin.game.Character;
+import funkin.game.Stage.StageCharPos;
+import haxe.xml.Access;
+
+class StageUnknownButton extends StageElementButton {
+ public var lowMemory:Bool = false;
+ public var highMemory:Bool = false;
+
+ public function new(x:Float,y:Float, xml:Access) {
+ super(x,y, xml);
+
+ if(xml.x.parent != null) {
+ lowMemory = xml.x.parent.nodeName == "low-memory";
+ highMemory = xml.x.parent.nodeName == "high-memory";
+ }
+
+ color = 0xff000000;
+
+ visibilityButton.exists = false;
+ members.remove(visibilityButton);
+
+ visibilityIcon.exists = false;
+ members.remove(visibilityIcon);
+
+ setEditAdvanced();
+
+ updateInfo();
+ }
+
+ public override function update(elapsed:Float) {
+ super.update(elapsed);
+ }
+
+ public override function updateInfo() {
+ super.updateInfo();
+ }
+
+ public override function getSprite():FunkinSprite {
+ var sprite = new FunkinSprite(); // to allow it to be saved
+ sprite.extra.set(StageEditor.exID("lowMemory"), lowMemory);
+ sprite.extra.set(StageEditor.exID("highMemory"), highMemory);
+ sprite.extra.set(StageEditor.exID("button"), this);
+ return sprite;
+ }
+
+ public override function canRender() {
+ return false;
+ }
+
+ public override function onSelect() {
+ }
+
+ public override function onEdit() {
+ FlxG.state.openSubState(new StageUnknownEditScreen(this));
+ }
+
+ //public override function onEdit() {
+ // UIState.state.displayNotification(new UIBaseNotification("Editing a character isnt implemented yet!", 2, BOTTOM_LEFT));
+ // CoolUtil.playMenuSFX(WARNING, 0.45);
+ //}
+
+ public override function onVisiblityToggle() {
+ }
+
+ public override function getName():String {
+ return xml.name;
+ }
+
+ public override function getPos():FlxPoint {
+ return FlxPoint.get(0, 0);
+ }
+
+ public override function getInfoText():String {
+ return 'Unknown\n<${getName()} />';
+ }
+
+ public override function updatePos() {
+ super.updatePos();
+ }
+}
+
+class StageUnknownEditScreen extends UISoftcodedWindow {
+ public var button:StageUnknownButton;
+
+ public function new(button:StageUnknownButton) {
+ this.button = button;
+ super("layouts/stage/unknownEditScreen", [
+ "stage" => StageEditor.instance.stage,
+ "button" => button,
+ "xml" => button.xml,
+ "exID" => StageEditor.exID
+ ]);
+ }
+
+ public override function create() {
+ super.create();
+ }
+
+ public override function saveData() {
+ super.saveData();
+ button.updateInfo();
+ }
+}
\ No newline at end of file
diff --git a/source/funkin/editors/ui/UIAudioPlayer.hx b/source/funkin/editors/ui/UIAudioPlayer.hx
index c0f49d90..7d688a69 100644
--- a/source/funkin/editors/ui/UIAudioPlayer.hx
+++ b/source/funkin/editors/ui/UIAudioPlayer.hx
@@ -57,7 +57,7 @@ class UIAudioPlayer extends UIButton {
members.push(timeBarPlayer);
timeBarSpr = cast new UISprite(timeBar.x, timeBar.y).makeSolid(timeBar.barWidth, timeBar.barHeight, 0x00FFFFFF);
- timeBarSpr.cursor = BUTTON;
+ timeBarSpr.cursor = CLICK;
members.push(timeBarSpr);
volumeBar = new FlxBar(timeBar.x + timeBar.barWidth - 56, y + 6, LEFT_TO_RIGHT, 56, 10, sound, "volume", 0, 1);
@@ -67,7 +67,7 @@ class UIAudioPlayer extends UIButton {
members.push(volumeBar);
volumeBarSpr = cast new UISprite(volumeBar.x, volumeBar.y).makeSolid(volumeBar.barWidth, volumeBar.barHeight, 0x00FFFFFF);
- volumeBarSpr.cursor = BUTTON;
+ volumeBarSpr.cursor = CLICK;
members.push(volumeBarSpr);
volumeIcon = new FlxSprite(volumeBar.x - 12 - 8, volumeBar.y-1).loadGraphic(Paths.image('editors/ui/audio-icon'));
diff --git a/source/funkin/editors/ui/UIButton.hx b/source/funkin/editors/ui/UIButton.hx
index c5522c5c..46bc30b5 100644
--- a/source/funkin/editors/ui/UIButton.hx
+++ b/source/funkin/editors/ui/UIButton.hx
@@ -13,7 +13,7 @@ class UIButton extends UISliceSprite {
field.alignment = CENTER;
field.fieldWidth = w;
- cursor = BUTTON;
+ cursor = CLICK;
}
public override function resize(w:Int, h:Int) {
@@ -38,7 +38,11 @@ class UIButton extends UISliceSprite {
}
public override function draw() {
- framesOffset = hovered ? (pressed ? 18 : 9) : 0;
+ setFrameOffset();
super.draw();
}
+
+ public function setFrameOffset() {
+ framesOffset = hovered ? (pressed ? 18 : 9) : 0;
+ }
}
\ No newline at end of file
diff --git a/source/funkin/editors/ui/UIButtonList.hx b/source/funkin/editors/ui/UIButtonList.hx
index ec8385e5..fffe3701 100644
--- a/source/funkin/editors/ui/UIButtonList.hx
+++ b/source/funkin/editors/ui/UIButtonList.hx
@@ -14,7 +14,7 @@ class UIButtonList extends UIWindow {
public var cameraSpacing = 30;
public var buttonSpacing:Float = 16;
- public var buttonSize:FlxPoint = FlxPoint.get();
+ public var buttonSize:FlxPoint = null;
public var buttonOffset:FlxPoint = FlxPoint.get();
public var dragging:Bool = false;
@@ -41,14 +41,14 @@ class UIButtonList extends UIWindow {
addIcon = new FlxSprite(addButton.x + addButton.bHeight / 2, addButton.y + (32/2) - 8).loadGraphic(Paths.image('editors/charter/add-button'));
addIcon.antialiasing = false;
addButton.members.push(addIcon);
- members.push(addButton);
+ content.add(addButton);
- members.push(buttons);
+ content.add(buttons);
nextscrollY = buttonCameras.scroll.y = -this.buttonSpacing;
}
public inline function add(button:T) {
- button.ID = buttons.members.length-1;
+ button.ID = buttons.members.length;
buttons.add(button);
curMovingInterval = 0;
nextscrollY += button.bHeight;
@@ -95,7 +95,8 @@ class UIButtonList extends UIWindow {
curMovingInterval = 0;
}
}
- addIcon.x = addButton.x + addButton.bWidth / 2 - addIcon.width / 2; addIcon.y = addButton.y + addButton.bHeight / 2 - addIcon.height / 2;
+ addIcon.x = addButton.x + addButton.bWidth / 2 - addIcon.width / 2;
+ addIcon.y = addButton.y + addButton.bHeight / 2 - addIcon.height / 2;
}
var nextscrollY:Float = 0;
public override function update(elapsed:Float) {
diff --git a/source/funkin/editors/ui/UICheckbox.hx b/source/funkin/editors/ui/UICheckbox.hx
index a5bcdef3..426857a5 100644
--- a/source/funkin/editors/ui/UICheckbox.hx
+++ b/source/funkin/editors/ui/UICheckbox.hx
@@ -10,19 +10,19 @@ class UICheckbox extends UISprite {
public function new(x:Float, y:Float, text:String, checked:Bool = false, w:Int = 0) {
super(x, y);
loadGraphic(Paths.image('editors/ui/checkbox'), true, 20, 20);
- for(frame=>name in ["normal", "hover", "pressed", "checkmark"])
+ for(frame=>name in ["normal", "hover", "pressed", "selected"])
animation.add(name, [frame], 0, false);
this.checked = checked;
field = new UIText(x, y, w, text);
check = new FlxSprite().loadGraphicFromSprite(this);
- check.animation.play("checkmark");
+ check.animation.play("selected");
members.push(check);
members.push(field);
- cursor = BUTTON;
+ cursor = CLICK;
}
public override function update(elapsed:Float) {
diff --git a/source/funkin/editors/ui/UIContextMenu.hx b/source/funkin/editors/ui/UIContextMenu.hx
index 8cc5bcb0..36db45ab 100644
--- a/source/funkin/editors/ui/UIContextMenu.hx
+++ b/source/funkin/editors/ui/UIContextMenu.hx
@@ -7,6 +7,7 @@ class UIContextMenu extends MusicBeatSubstate {
public var options:Array;
var x:Float;
var y:Float;
+ var w:Int = 100;
var contextCam:FlxCamera;
var bg:UISliceSprite;
@@ -21,11 +22,12 @@ class UIContextMenu extends MusicBeatSubstate {
__oobDeletion = false;
}
- public function new(options:Array, callback:UIContextMenuCallback, x:Float, y:Float) {
+ public function new(options:Array, callback:UIContextMenuCallback, x:Float, y:Float, ?w:Int = 100) {
super();
this.options = options.getDefault([]);
this.x = x;
this.y = y;
+ this.w = w;
this.callback = callback;
}
@@ -37,7 +39,7 @@ class UIContextMenu extends MusicBeatSubstate {
contextCam.scroll.set(0, 7.5);
FlxG.cameras.add(contextCam, false);
- bg = new UISliceSprite(x, y, 100, 100, 'editors/ui/context-bg');
+ bg = new UISliceSprite(x, y, w, 100, 'editors/ui/context-bg');
bg.cameras = [contextCam];
add(bg);
diff --git a/source/funkin/editors/ui/UIDropDown.hx b/source/funkin/editors/ui/UIDropDown.hx
index 1cb944b8..1cbcbaf3 100644
--- a/source/funkin/editors/ui/UIDropDown.hx
+++ b/source/funkin/editors/ui/UIDropDown.hx
@@ -1,34 +1,68 @@
package funkin.editors.ui;
+import flixel.util.typeLimit.OneOfTwo;
+
class UIDropDown extends UISliceSprite {
public var dropButton:UIButton;
public var label:UIText;
public var index:Int = 0;
public var options:Array;
+ public var items:Array;
+ public var key(get, never):String;
+ inline function get_key():String {
+ return items[index].label;
+ }
+ public var value(get, never):Dynamic;
+ inline function get_value():Dynamic {
+ return items[index].value;
+ }
public var onChange:Int->Void;
var curMenu:UIContextMenu = null;
- public function new(x:Float, y:Float, width:Int = 320, height:Int = 32, options:Array, index:Int = 0) {
+ public static function getItems(options:Array):Array {
+ return [for(i=>o in options) {label: o, value: i}];
+ }
+
+ public function new(x:Float, y:Float, width:Int = 320, height:Int = 32, _options:Array>, index:Int = 0) {
super(x, y, width - height, height, 'editors/ui/inputbox'); // using same sprite cause fuck you
- this.options = options;
+ var _items:Array = [];
+
+ if(_options.length > 0) {
+ if(_options[0] is String) {
+ _items = getItems(_options);
+ } else {
+ _items = cast _options;
+ }
+ }
+
+ this.items = _items;
+ this.options = [for(o in _items) o.label]; // backwards compatibility
this.index = index;
- cursor = BUTTON;
+ cursor = CLICK;
- label = new UIText(0, 0, width - height, options[index]);
+ label = new UIText(0, 0, width - height, items[index].label);
members.push(label);
dropButton = new UIButton(0, 0, "V", null, height, height);
members.push(dropButton);
}
+ public static function indexOfItemValue(items:Array, value:Dynamic):Int {
+ return [for(o in items) o.value].indexOf(value);
+ }
+
+ public static function indexOfItemLabel(items:Array, str:String):Int {
+ return [for(o in items) o.label].indexOf(str);
+ }
+
public function setOption(newIndex:Int) {
if (index != (index = newIndex)) {
- label.text = options[index];
+ label.text = items[index].label;
if (onChange != null)
onChange(index);
}
@@ -53,12 +87,17 @@ class UIDropDown extends UISliceSprite {
public function openContextMenu() {
var screenPos = getScreenPosition(null, __lastDrawCameras[0] == null ? FlxG.camera : __lastDrawCameras[0]);
curMenu = UIState.state.openContextMenu([
- for(k=>o in options) {
+ for(k=>o in items) {
icon: (k == index) ? 1 : 0,
- label: o
+ label: o.label
}
], function(_, i, _) {
setOption(i);
}, __lastDrawCameras[0].x + screenPos.x, __lastDrawCameras[0].y + screenPos.y + bHeight);
}
+}
+
+typedef DropDownItem = {
+ var label:String;
+ var value:Dynamic;
}
\ No newline at end of file
diff --git a/source/funkin/editors/ui/UIRadioButton.hx b/source/funkin/editors/ui/UIRadioButton.hx
new file mode 100644
index 00000000..ebff277a
--- /dev/null
+++ b/source/funkin/editors/ui/UIRadioButton.hx
@@ -0,0 +1,105 @@
+package funkin.editors.ui;
+
+import flixel.util.typeLimit.OneOfTwo;
+
+class UIRadioButton extends UISprite {
+ public var checked:Bool = false;
+ public var onChecked:Bool->Void = null;
+
+ public var field:UIText;
+ public var check:FlxSprite;
+
+ public var parent:OneOfTwo = null;
+ public var forID:String = null;
+
+ public function new(x:Float, y:Float, text:String, checked:Bool = false, forID:String = null, w:Int = 0) {
+ super(x, y);
+ this.forID = forID;
+ loadGraphic(Paths.image('editors/ui/radiobutton'), true, 20, 20);
+ for(frame=>name in ["normal", "hover", "pressed", "selected"])
+ animation.add(name, [frame], 0, false);
+
+ this.checked = checked;
+ this.targetScale = checked ? 1 : 0;
+
+ field = new UIText(x, y, w, text);
+ check = new FlxSprite().loadGraphicFromSprite(this);
+ check.animation.play("selected");
+ check.scale.set(targetScale, targetScale);
+
+ members.push(check);
+ members.push(field);
+
+ cursor = CLICK;
+
+ autoAlpha = false;
+ }
+
+ public var targetScale:Float = 1;
+
+ public override function update(elapsed:Float) {
+ // ANIMATION HANDLING
+ animation.play(hovered ? (pressed ? "pressed" : "hover") : "normal");
+
+ // CHECKMARK HANDLING
+ if(autoAlpha)
+ check.alpha = checked ? 1 : 0;
+ check.scale.x = CoolUtil.fpsLerp(check.scale.x, targetScale, 0.25);
+ check.scale.y = CoolUtil.fpsLerp(check.scale.y, targetScale, 0.25);
+
+ // POSITION HANDLING
+ updatePositions();
+
+ super.update(elapsed);
+ }
+
+ public inline function updatePositions() {
+ check.follow(this);
+ field.follow(this, 25, 0);
+ }
+
+ public override function draw() {
+ updatePositions();
+ super.draw();
+ }
+
+ public override function onHovered() {
+ super.onHovered();
+ if (FlxG.mouse.justReleased) {
+ // clicked
+ checked = !checked;
+ check.scale.set(1.25, 1.25);
+ targetScale = checked ? 1 : 0;
+
+ if (Options.editorSFX)
+ CoolUtil.playMenuSFX(checked ? CHECKED : UNCHECKED, 0.5);
+
+ if(parent != null) {
+ var members = (parent is UIWindow) ? (cast(parent, UIWindow)).members : (cast(parent, UISubstateWindow)).members;
+ var radios:Array = cast members.filter((o) -> o is UIRadioButton && o != this);
+ //trace(radios);
+ for(radio in radios) {
+ if(radio.forID != null && radio.forID == forID) {
+ radio.checked = false;
+ radio.targetScale = 0;
+ }
+ }
+ }
+
+ if (onChecked != null)
+ onChecked(checked);
+ }
+ }
+
+ public override function updateButton() {
+ __rect.x = x;
+ __rect.y = y;
+ __rect.width = field.width + 30;
+ __rect.height = field.height > height ? field.height : height;
+ UIState.state.updateRectButtonHandler(this, __rect, onHovered);
+ }
+
+ public override function toString() {
+ return "RadioButton(" + field.text + ", " + checked + ", " + forID + ")";
+ }
+}
\ No newline at end of file
diff --git a/source/funkin/editors/ui/UIScrollBar.hx b/source/funkin/editors/ui/UIScrollBar.hx
index c6ef73d2..570a0c56 100644
--- a/source/funkin/editors/ui/UIScrollBar.hx
+++ b/source/funkin/editors/ui/UIScrollBar.hx
@@ -24,7 +24,7 @@ class UIScrollBar extends UISprite {
this.length = length;
thumb = new UISliceSprite(0, 0, w, h, 'editors/ui/scrollbar');
- thumb.cursor = BUTTON;
+ thumb.cursor = CLICK;
members.push(thumb);
thumbIcon = new FlxSprite(0, 0, Paths.image('editors/ui/scrollbar-icon'));
diff --git a/source/funkin/editors/ui/UISliceSprite.hx b/source/funkin/editors/ui/UISliceSprite.hx
index 09226205..8cc6ab2d 100644
--- a/source/funkin/editors/ui/UISliceSprite.hx
+++ b/source/funkin/editors/ui/UISliceSprite.hx
@@ -5,7 +5,7 @@ import flixel.graphics.frames.FlxFrame;
class UISliceSprite extends UISprite {
public var bWidth:Int = 120;
public var bHeight:Int = 20;
- public var framesOffset:Int = 0;
+ public var framesOffset(default, set):Int = 0;
public var incorporeal:Bool = false;
@@ -14,6 +14,7 @@ class UISliceSprite extends UISprite {
frames = Paths.getFrames(path);
resize(w, h);
+ calculateFrames();
}
public override function updateButton() {
@@ -30,28 +31,71 @@ class UISliceSprite extends UISprite {
bHeight = h;
}
+ public var topAlpha:Null = null;
+ public var middleAlpha:Null = null;
+ public var bottomAlpha:Null = null;
+
public var drawTop:Bool = true;
public var drawMiddle:Bool = true;
public var drawBottom:Bool = true;
+ var topleft:FlxFrame = null;
+ var top:FlxFrame = null;
+ var topright:FlxFrame = null;
+ var middleleft:FlxFrame = null;
+ var middle:FlxFrame = null;
+ var middleright:FlxFrame = null;
+ var bottomleft:FlxFrame = null;
+ var bottom:FlxFrame = null;
+ var bottomright:FlxFrame = null;
+
+ public var topHeight:Int = 0;
+ public var bottomHeight:Int = 0;
+ public var leftWidth:Int = 0;
+ public var rightWidth:Int = 0;
+
+ override function set_frames(val) {
+ super.set_frames(val);
+ calculateFrames();
+ return val;
+ }
+
+ function set_framesOffset(value:Int) {
+ if(value != framesOffset) {
+ framesOffset = value;
+ calculateFrames();
+ }
+ return value;
+ }
+
+ function calculateFrames() {
+ if(frames == null) return;
+ topleft = frames.frames[framesOffset];
+ top = frames.frames[framesOffset + 1];
+ topright = frames.frames[framesOffset + 2];
+ middleleft = frames.frames[framesOffset + 3];
+ middle = frames.frames[framesOffset + 4];
+ middleright = frames.frames[framesOffset + 5];
+ bottomleft = frames.frames[framesOffset + 6];
+ bottom = frames.frames[framesOffset + 7];
+ bottomright = frames.frames[framesOffset + 8];
+
+ leftWidth = Std.int(Math.max(topleft.frame.width, Math.max(middleleft.frame.width, bottomleft.frame.width)));
+ rightWidth = Std.int(Math.max(topright.frame.width, Math.max(middleright.frame.width, bottomright.frame.width)));
+ topHeight = Std.int(Math.max(topleft.frame.height, Math.max(top.frame.height, topright.frame.height)));
+ bottomHeight = Std.int(Math.max(topleft.frame.height, Math.max(top.frame.height, topright.frame.height)));
+ }
+
public override function draw() @:privateAccess {
var x:Float = this.x;
var y:Float = this.y;
if (visible && !(bWidth == 0 || bHeight == 0)) {
- var topleft:FlxFrame = frames.frames[framesOffset];
- var top:FlxFrame = frames.frames[framesOffset + 1];
- var topright:FlxFrame = frames.frames[framesOffset + 2];
- var middleleft:FlxFrame = frames.frames[framesOffset + 3];
- var middle:FlxFrame = frames.frames[framesOffset + 4];
- var middleright:FlxFrame = frames.frames[framesOffset + 5];
- var bottomleft:FlxFrame = frames.frames[framesOffset + 6];
- var bottom:FlxFrame = frames.frames[framesOffset + 7];
- var bottomright:FlxFrame = frames.frames[framesOffset + 8];
-
+ var oldAlpha = alpha;
// TOP
if (drawTop) {
// TOP LEFT
+ if(topAlpha != null) alpha = topAlpha;
frame = topleft;
setPosition(x, y);
__setSize(
@@ -80,6 +124,7 @@ class UISliceSprite extends UISprite {
// MIDDLE
if (drawMiddle && bHeight > top.frame.height + bottom.frame.height) {
+ if(middleAlpha != null) alpha = middleAlpha;
var middleHeight:Float = bHeight - (topleft.frame.height * Math.min(bHeight/(topleft.frame.height*2), 1)) -
bottomleft.frame.height * Math.min(bHeight/(bottomleft.frame.height*2), 1);
@@ -106,6 +151,7 @@ class UISliceSprite extends UISprite {
// BOTTOM
if (drawBottom) {
+ if(bottomAlpha != null) alpha = bottomAlpha;
// BOTTOM LEFT
frame = bottomleft;
setPosition(x, y + bHeight - (bottomleft.frame.height * Math.min(bHeight/(bottomleft.frame.height*2), 1)));
@@ -134,7 +180,9 @@ class UISliceSprite extends UISprite {
bottomright.frame.height * Math.min(bHeight/(bottomright.frame.height*2), 1)
);
super.drawSuper();
+
}
+ alpha = oldAlpha;
}
setPosition(x, y);
diff --git a/source/funkin/editors/ui/UISlider.hx b/source/funkin/editors/ui/UISlider.hx
index d07bb915..b0d86d0a 100644
--- a/source/funkin/editors/ui/UISlider.hx
+++ b/source/funkin/editors/ui/UISlider.hx
@@ -45,7 +45,7 @@ class UISlider extends UISprite {
makeGraphic(barWidth, 12, 0x00000000, true);
this.drawRoundRect(0, 0, barWidth, 12, 8, 6, 0xFFC0C0C0);
this.drawRoundRect(1, 1, barWidth-2, 12-2, 8, 5, 0xFF140013);
- cursor = BUTTON;
+ cursor = CLICK;
progressbar = new UISprite(centered ? barWidth/2 : 0);
progressbar.makeGraphic(barWidth, 8, 0x00000000, true);
@@ -63,7 +63,7 @@ class UISlider extends UISprite {
var selectableBar:UISprite = new UISprite(x,y);
selectableBar.loadGraphic(Paths.image("editors/ui/slider"));
selectableBar.antialiasing = true;
- selectableBar.cursor = BUTTON;
+ selectableBar.cursor = CLICK;
members.push(selectableBar);
switch (i) {
@@ -74,7 +74,7 @@ class UISlider extends UISprite {
selectableHitbox = new UISprite(x,y);
selectableHitbox.makeSolid(barWidth, 18, -1);
- selectableHitbox.cursor = BUTTON;
+ selectableHitbox.cursor = CLICK;
selectableHitbox.alpha = 0;
members.push(selectableHitbox);
diff --git a/source/funkin/editors/ui/UISoftcodedWindow.hx b/source/funkin/editors/ui/UISoftcodedWindow.hx
new file mode 100644
index 00000000..e791f883
--- /dev/null
+++ b/source/funkin/editors/ui/UISoftcodedWindow.hx
@@ -0,0 +1,340 @@
+package funkin.editors.ui;
+
+import flixel.math.FlxPoint;
+import funkin.backend.chart.ChartData.ChartMetaData;
+import funkin.backend.scripting.HScript;
+import funkin.backend.scripting.Script;
+import funkin.editors.extra.PropertyButton;
+import funkin.editors.stage.elements.StageElementButton;
+import funkin.editors.ui.UIDropDown;
+import haxe.xml.Access;
+import hscript.*;
+import hscript.Expr.Error;
+import hscript.Interp;
+import hscript.Parser;
+import haxe.io.Path;
+
+using StringTools;
+
+class UISoftcodedWindow extends UISubstateWindow {
+ public var saveButton:UIButton;
+ public var closeButton:UIButton;
+
+ var elementMap:Map = [];
+
+ var xmlPath:String;
+ var filename:String;
+ var customVariables:Map;
+
+ public function new(xmlPath:String, customVariables:Map = null) {
+ super();
+ this.xmlPath = xmlPath;
+ this.filename = Path.withoutDirectory(xmlPath);
+ this.customVariables = customVariables;
+ }
+
+ 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);
+ }
+ function varExists(name:String):Bool {
+ return interp.variables.exists(name);
+ }
+
+ function execString(input:String):String {
+ if(input == null) return null;
+ if(input.indexOf("${") == -1) return input;
+ var text = "";
+ while(input.length > 0) {
+ var idx = input.indexOf("${");
+ if(idx == -1) {
+ text += input;
+ break;
+ }
+ text += input.substring(0, idx);
+ var ei = input.indexOf("}", idx);
+ if(ei == -1) {
+ text += input.substring(idx);
+ break;
+ }
+ var block = input.substring(idx+2, ei);
+ text += exec(block);
+ input = input.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);
+ }
+
+ function exec(code:String):Dynamic {
+ try {
+ if (code != null && code.trim() != "")
+ return interp.execute(parser.parseString(code, filename));
+ } catch(e:Error) {
+ _errorHandler(e);
+ } catch(e) {
+ _errorHandler(new Error(ECustom(e.toString()), 0, 0, filename, 0));
+ }
+ return 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);
+ }
+
+ public override function create() {
+ var layout = new Access(Xml.parse(Assets.getText(Paths.xml("editors/" + xmlPath))).firstElement());
+
+ for(k=>e in Script.getDefaultVariables()) {
+ set(k, e);
+ }
+ set("self", this);
+ set("hasSaveButton", true);
+ set("hasCloseButton", true);
+ set("getRadioButtons", function(forID:String) {
+ var radios:Array = cast members.filter((o) -> o is UIRadioButton);
+ return cast radios.filter((o) -> o.forID == forID);
+ });
+ for(k=>v in customVariables) {
+ set(k, v);
+ }
+ set("last", null);
+
+ if(varExists("winTitle")) winTitle = get("winTitle");
+ if(varExists("winWidth")) winWidth = get("winWidth");
+ if(varExists("winHeight")) winHeight = get("winHeight");
+
+ winTitle = execString(layout.getAtt("title").getDefault(winTitle));
+ winWidth = Std.parseInt(layout.getAtt("width")).getDefault(winWidth);
+ winHeight = Std.parseInt(layout.getAtt("height")).getDefault(winHeight);
+
+ super.create();
+
+ function addLabelOn(ui:UISprite, text:String)
+ add(new UIText(ui.x, ui.y - 24, 0, text));
+
+ function getArr(i:Iterator) {
+ return [for(e in i) e];
+ }
+
+ function addLayout(elements:Array) {
+ for(el in elements) {
+ var type = el.name;
+
+ switch(type) {
+ case "section":
+ if(execAtt(el, "if", true) != true)
+ continue;
+ if(execAtt(el, "unless", false) != false)
+ continue;
+ addLayout(getArr(el.elements));
+ continue;
+ case "set":
+ set(execString(el.getAtt("name")), execAtt(el, "value"));
+ continue;
+ case "exec":
+ exec(el.innerData);
+ continue;
+ }
+
+ var name:String = execString(el.getAtt("name"));
+ var label:String = execString(el.getAtt("label"));
+ var x:Float = execAtt(el, "x", 0);
+ var y:Float = execAtt(el, "y", 0);
+
+ var sprite:FlxSprite = switch(type) {
+ case "image":
+ var spr = XMLUtil.createSpriteFromXML(el, "", LOOP);
+ spr.x = x;
+ spr.y = y;
+ cast add(spr);
+ case "solid":
+ var spr = new FunkinSprite(x, y).makeSolid(
+ execAtt(el, "width", 100),
+ execAtt(el, "height", 100),
+ execAtt(el, "color", 0xFFFFFFFF)
+ );
+ cast add(spr);
+ case "textbox":
+ var text:String = execString(el.getAtt("text"));
+ text = Std.string(execAtt(el, "value", text));
+ var textBox = new UITextBox(x, y, text, execAtt(el, "width"), execAtt(el, "height"), execAtt(el, "multiline", false));//(el.getAtt("multiline").getDefault("false") == "true"));
+ add(textBox);
+ addLabelOn(textBox, label);
+ textBox;
+ case "title":
+ cast add(new UIText(x, y, execAtt(el, "width", 0), execString(el.getAtt("text")), execAtt(el, "size", 28)));
+ case "label":
+ cast add(new UIText(x, y - 24, execAtt(el, "width", 0), execString(el.getAtt("text")), execAtt(el, "size", 15)));
+ case "text":
+ cast add(new UIText(x, y, execAtt(el, "width", 0), execString(el.getAtt("text")), execAtt(el, "size", 15)));
+ 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"),
+ execAtt(el, "max"),
+ execAtt(el, "width"),
+ execAtt(el, "height")
+ );
+ add(stepper);
+ addLabelOn(stepper, label);
+ stepper;
+ case "checkbox":
+ var checkbox = new UICheckbox(x, y, execString(el.getAtt("text")), execAtt(el, "value", false));
+ add(checkbox);
+ addLabelOn(checkbox, label);
+ checkbox;
+ case "radio":
+ var radio = new UIRadioButton(x, y, execString(el.getAtt("text")), execAtt(el, "value", false), execString(el.getAtt("for")));
+ radio.parent = this;
+ add(radio);
+ addLabelOn(radio, label);
+ radio;
+ case "slider":
+ var slider = new UISlider(x, y, execAtt(el, "width"), execAtt(el, "value", 0), execAtt(el, "segments", []), execAtt(el, "centered", false));
+ add(slider);
+ addLabelOn(slider, label);
+ slider;
+ case "dropdown":
+ var items:Array = [];
+ for(i=>node in el.nodes.item)
+ items.push({label: execString(node.getAtt("label")), value: execAtt(node, "value", i)});
+ var curValue = execAtt(el, "value");
+ var index = UIDropDown.indexOfItemValue(items, curValue);
+ if (index == -1) index = 0;
+
+ var dropdown = new UIDropDown(x, y, execAtt(el, "width"), execAtt(el, "height"), items, index);
+ add(dropdown);
+ addLabelOn(dropdown, label);
+ dropdown;
+ case "buttonlist":
+ //x:Float, y:Float, width:Int, height:Int, windowName:String, buttonSize:FlxPoint, ?buttonOffset:FlxPoint, ?buttonSpacing:Float
+ var buttonlist = new UIButtonList(
+ x,
+ y,
+ execAtt(el, "width"),
+ execAtt(el, "height"),
+ execString(el.getAtt("title")).getDefault(""),
+ FlxPoint.get(execAtt(el, "buttonSizeX", 0), execAtt(el, "buttonSizeY", 0)),
+ FlxPoint.get(execAtt(el, "buttonOffsetX", 0), execAtt(el, "buttonOffsetY", 0)),
+ execAtt(el, "buttonSpacing")
+ );
+ var old = get("buttonlist");
+ set("buttonlist", buttonlist);
+ var texture = "editors/ui/inputbox";
+ var code = "";
+ for(node in getArr(el.elements)) {
+ switch(node.name) {
+ case "camSpacing": buttonlist.cameraSpacing = execAtt(node, "value", 0);
+ case "texture": texture = execString(node.getAtt("path")).getDefault(texture);
+ case "code": code = node.innerData;
+ }
+ }
+ buttonlist.frames = Paths.getFrames(texture);
+ if(code != "")
+ exec(code);
+ add(buttonlist);
+ addLabelOn(buttonlist, label);
+ set("buttonlist", old);
+ buttonlist;
+ // 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);
+ }
+ }
+
+ addLayout(getArr(layout.elements));
+
+ if(get("hasSaveButton")) {
+ 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;
+ add(saveButton);
+ }
+
+ if(get("hasCloseButton")) {
+ var x = saveButton != null ? saveButton.x - 20 : windowSpr.x + windowSpr.bWidth - 20;
+ var y = saveButton != null ? saveButton.y : windowSpr.y + windowSpr.bHeight - 20;
+ closeButton = new UIButton(x, y, "Close", function() {
+ close();
+ }, 125);
+ if(saveButton != null)
+ closeButton.color = 0xFFFF0000;
+ closeButton.x -= closeButton.bWidth;
+ if(saveButton == null)
+ closeButton.y -= closeButton.bHeight;
+ add(closeButton);
+ }
+ }
+
+ function getElement(name:String):UISprite {
+ return elementMap.get(name);
+ }
+
+ function callFunc(name:String, args:Array) {
+ 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;
+ }
+
+ override function update(elapsed:Float) {
+ super.update(elapsed);
+ callFunc("onUpdate", [elapsed]);
+ }
+
+ public function saveData() {
+ for (el in elementMap) {
+ if(el is UINumericStepper) {
+ var el:UINumericStepper = cast el;
+ @:privateAccess el.__onChange(el.label.text);
+ }
+ }
+
+ callFunc("onSave", []);
+ }
+}
\ No newline at end of file
diff --git a/source/funkin/editors/ui/UISprite.hx b/source/funkin/editors/ui/UISprite.hx
index 1246e6de..c8e38333 100644
--- a/source/funkin/editors/ui/UISprite.hx
+++ b/source/funkin/editors/ui/UISprite.hx
@@ -1,6 +1,7 @@
package funkin.editors.ui;
import flixel.math.FlxRect;
+import funkin.backend.utils.NativeAPI.CodeCursor;
import openfl.ui.MouseCursor;
@:allow(funkin.editors.ui.UIState)
@@ -21,7 +22,7 @@ class UISprite extends FlxSprite {
public var hoverCallback:Void->Void = null;
- public var cursor:MouseCursor = ARROW;
+ public var cursor:CodeCursor = ARROW;
public var focused(get, set):Bool;
public var selectable:Bool = true;
@@ -55,7 +56,8 @@ class UISprite extends FlxSprite {
FlxCamera._defaultCameras = cameras;
for(m in members)
- m.update(elapsed);
+ if(m.exists && m.active)
+ m.update(elapsed);
FlxCamera._defaultCameras = __oldDefCams;
}
@@ -114,4 +116,11 @@ class UISprite extends FlxSprite {
if (hoverCallback != null)
hoverCallback();
}
+
+ public function updateSpriteRect() {
+ __rect.x = x;
+ __rect.y = y;
+ __rect.width = width;
+ __rect.height = height;
+ }
}
\ No newline at end of file
diff --git a/source/funkin/editors/ui/UIState.hx b/source/funkin/editors/ui/UIState.hx
index 1c32fad6..e2e580af 100644
--- a/source/funkin/editors/ui/UIState.hx
+++ b/source/funkin/editors/ui/UIState.hx
@@ -1,8 +1,10 @@
package funkin.editors.ui;
+import funkin.editors.ui.notifications.UIBaseNotification;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import funkin.backend.system.framerate.Framerate;
+import funkin.backend.utils.NativeAPI.CodeCursor;
import funkin.editors.ui.UIContextMenu.UIContextMenuCallback;
import funkin.editors.ui.UIContextMenu.UIContextMenuOption;
import lime.ui.KeyCode;
@@ -19,7 +21,9 @@ class UIState extends MusicBeatState {
public var hoveredSprite:UISprite = null;
public var currentFocus:IUIFocusable = null;
- public var currentCursor:MouseCursor = ARROW;
+ public var currentCursor:CodeCursor = ARROW;
+
+ public var uiCameras:Array = [];
private var __rect:FlxRect;
private var __mousePos:FlxPoint;
@@ -59,28 +63,15 @@ class UIState extends MusicBeatState {
currentFocus.onTextEdit(str, start, end);
}
- public inline function updateSpriteRect(spr:UISprite) {
- spr.__rect.x = spr.x;
- spr.__rect.y = spr.y;
- spr.__rect.width = spr.width;
- spr.__rect.height = spr.height;
- }
-
public function updateButtonHandler(spr:UISprite, buttonHandler:Void->Void) {
- spr.__rect.x = spr.x;
- spr.__rect.y = spr.y;
- spr.__rect.width = spr.width;
- spr.__rect.height = spr.height;
+ spr.updateSpriteRect();
updateRectButtonHandler(spr, spr.__rect, buttonHandler);
}
public function isOverlapping(spr:UISprite, rect:FlxRect) {
for(camera in spr.__lastDrawCameras) {
var pos = FlxG.mouse.getScreenPosition(camera, FlxPoint.get());
- __rect.x = rect.x;
- __rect.y = rect.y;
- __rect.width = rect.width;
- __rect.height = rect.height;
+ __rect.copyFrom(rect);
__rect.x -= camera.scroll.x * spr.scrollFactor.x;
__rect.y -= camera.scroll.y * spr.scrollFactor.y;
@@ -117,12 +108,12 @@ class UIState extends MusicBeatState {
FlxG.sound.keysAllowed = currentFocus != null ? !(currentFocus is UITextBox) : true;
- if (hoveredSprite != null) {
- Mouse.cursor = hoveredSprite.cursor;
- hoveredSprite = null;
+ if (hoveredSprite != null && hoveredSprite.cursor != null) {
+ NativeAPI.setCursorIcon(hoveredSprite.cursor);
} else {
- Mouse.cursor = currentCursor;
+ NativeAPI.setCursorIcon(currentCursor);
}
+ hoveredSprite = null;
}
public override function destroy() {
@@ -145,7 +136,7 @@ class UIState extends MusicBeatState {
}
}
- public function openContextMenu(options:Array, ?callback:UIContextMenuCallback, ?x:Float, ?y:Float) {
+ public function openContextMenu(options:Array, ?callback:UIContextMenuCallback, ?x:Float, ?y:Float, ?w:Int) {
var state = FlxG.state;
while(state.subState != null && !(state._requestSubStateReset && state._requestedSubState == null))
state = state.subState;
@@ -153,7 +144,24 @@ class UIState extends MusicBeatState {
state.persistentDraw = true;
state.persistentUpdate = true;
- state.openSubState(curContextMenu = new UIContextMenu(options, callback, x.getDefault(__mousePos.x), y.getDefault(__mousePos.y)));
+ state.openSubState(curContextMenu = new UIContextMenu(options, callback, x.getDefault(__mousePos.x), y.getDefault(__mousePos.y), w));
return curContextMenu;
}
+
+ public function displayNotification(notification:UIBaseNotification) {
+ notification.cameras = uiCameras;
+ notification.onRemove = (notif) -> {
+ notification.onRemove = null;
+ remove(notif, true);
+ };
+ add(notification);
+ notification.update(0);
+ notification.appearAnimation();
+ // TODO: future tooltips
+ //notification.x = __mousePos.x;
+ //notification.y = __mousePos.y;
+ //notification.alpha = 0;
+ //notification.appearAnimation();
+ //FlxTween.tween(notification, {x: __mousePos.x, y: __mousePos.y, alpha: 1}, .3, {ease: FlxEase.circInOut});
+ }
}
\ No newline at end of file
diff --git a/source/funkin/editors/ui/UITopMenu.hx b/source/funkin/editors/ui/UITopMenu.hx
index ae78469d..12e1b15e 100644
--- a/source/funkin/editors/ui/UITopMenu.hx
+++ b/source/funkin/editors/ui/UITopMenu.hx
@@ -43,7 +43,7 @@ class UITopMenuButton extends UISliceSprite {
super(x, y, 0, 23, "editors/ui/menu-item");
this.contextMenu = contextMenu;
this.parent = parent;
- cursor = BUTTON;
+ cursor = CLICK;
this.label = new UIText(4, 0, 0, label);
this.label.alignment = CENTER;
diff --git a/source/funkin/editors/ui/UIUtil.hx b/source/funkin/editors/ui/UIUtil.hx
index 7f9de548..4a5a0ee4 100644
--- a/source/funkin/editors/ui/UIUtil.hx
+++ b/source/funkin/editors/ui/UIUtil.hx
@@ -1,5 +1,6 @@
package funkin.editors.ui;
+import flixel.input.FlxInput.FlxInputState;
import flixel.input.keyboard.FlxKey;
import funkin.editors.ui.UIContextMenu.UIContextMenuOption;
@@ -14,6 +15,19 @@ class UIUtil {
return contextMenu != null && UIState.state.curContextMenu == contextMenu;
}
+ @:noUsing public static function fixKey(key:FlxKey):FlxKey {
+ return switch(key) {
+ #if mac
+ case CONTROL: WINDOWS; // Remap control to the meta key
+ #end
+ default: key;
+ }
+ }
+
+ public static function getKeyState(key:FlxKey, Status:FlxInputState):Bool {
+ return FlxG.keys.checkStatus(fixKey(key), Status);
+ }
+
/**
* Process all options with shortcuts present in a `Array`. Also checks childrens.
* @param topMenuOptions
@@ -41,14 +55,7 @@ class UIUtil {
var shouldPress = Std.int(key) > 0;
if(!shouldPress) key = -key;
- var k = switch(key) {
- #if mac
- case CONTROL:
- WINDOWS;
- #end
- default:
- key;
- }
+ var k = fixKey(key);
if (FlxG.keys.checkStatus(k, shouldPress ? JUST_PRESSED : JUST_RELEASED)) {
justPressed = true;
} else if (!FlxG.keys.checkStatus(k, shouldPress ? PRESSED : RELEASED)) {
diff --git a/source/funkin/editors/ui/UIWindow.hx b/source/funkin/editors/ui/UIWindow.hx
index 6853ce06..29abb538 100644
--- a/source/funkin/editors/ui/UIWindow.hx
+++ b/source/funkin/editors/ui/UIWindow.hx
@@ -2,19 +2,48 @@ package funkin.editors.ui;
class UIWindow extends UISliceSprite {
public var titleSpr:UIText;
+ public var collapsable:Bool = false;
+ public var content:FlxTypedGroup;
public override function new(x:Float, y:Float, w:Int, h:Int, title:String) {
super(x, y, w, h, "editors/ui/normal-popup");
members.push(titleSpr = new UIText(x + 25, y, bWidth - 50, title, 15, -1));
titleSpr.y = y + ((30 - titleSpr.height) / 2);
+
+ content = new FlxTypedGroup();
+ members.push(content);
}
public override function update(elapsed:Float) {
super.update(elapsed);
- __rect.x = x; __rect.y = y+23;
- __rect.width = bWidth; __rect.height = bHeight-23;
+ cursor = ARROW;
+
+ if(collapsable) {
+ __rect.x = x; __rect.y = y;
+ __rect.width = bWidth; __rect.height = topHeight;
+
+ if(UIState.state.isOverlapping(this, __rect)) {
+ cursor = CLICK;
+ if(FlxG.mouse.justPressed) {
+ content.exists = !content.exists;
+ drawMiddle = !drawMiddle;
+ drawBottom = !drawBottom;
+ }
+ } else {
+ if(!content.exists) {
+ cursor = null;
+ }
+ }
+ }
+
+ __rect.x = x;
+ __rect.width = bWidth;
+ if(content.exists) {
+ __rect.y = y+topHeight;
+ __rect.height = bHeight-topHeight;
+ }
hovered = UIState.state.isOverlapping(this, __rect);
}
}
\ No newline at end of file
diff --git a/source/funkin/editors/ui/notifications/UIBaseNotification.hx b/source/funkin/editors/ui/notifications/UIBaseNotification.hx
new file mode 100644
index 00000000..b86ba3c7
--- /dev/null
+++ b/source/funkin/editors/ui/notifications/UIBaseNotification.hx
@@ -0,0 +1,166 @@
+package funkin.editors.ui.notifications;
+
+import flixel.group.FlxGroup;
+import flixel.math.FlxRect;
+import flixel.tweens.FlxTween;
+import flixel.util.FlxTimer;
+
+using flixel.util.FlxSpriteUtil;
+
+class UIBaseNotification extends UISliceSprite {
+ public var textField:UIText;
+ public var closeButton:UIButton;
+
+ public var onRemove:(notif:UIBaseNotification)->Void = null;
+
+ public var showedAnimation:Bool = false;
+ public var closed:Bool = false;
+
+ public function new(text:String, showTimer:Float = 5, corner:Corner = BOTTOM_LEFT) {
+ super(x, y, 340, 46, "editors/ui/inputbox");
+
+ closeButton = new UIButton(0, 0, "X", () -> {
+ if (closed) return;
+ closed = true;
+
+ (new FlxTimer()).start(0.2, (_) -> {disappearAnimation();});
+ }, 32, 32);
+ closeButton.hoverCallback = () -> {
+ closeButton.color = 0xFFAC3D3D;
+ closeButton.field.color = 0xFFffffff;
+ //closeButton.field.color = closeButton.hovered ? 0xFFffffff : 0xFF7f7f7f;
+ }
+ closeButton.autoAlpha = false;
+ closeButton.alpha = 0.0;
+
+ textField = new UIText(0, 0, 0, text, 14);
+ textField.alignment = LEFT;
+ textField.fieldWidth = bWidth - 32 - 8;
+ members.push(textField);
+
+ if(textField.height > bHeight)
+ bHeight = Std.int(textField.height);
+
+ alpha = 0;
+ textField.alpha = 0;
+
+ showingTimer = showTimer;
+
+ members.push(closeButton);
+
+ var margin = 20;
+
+ switch(corner) {
+ case TOP_LEFT:
+ x = margin;
+ y = margin;
+ case TOP_RIGHT:
+ x = FlxG.width - bWidth - margin;
+ y = margin;
+ case BOTTOM_LEFT:
+ x = margin;
+ y = FlxG.height - bHeight - margin;
+ case BOTTOM_RIGHT:
+ x = FlxG.width - bWidth - margin;
+ y = FlxG.height - bHeight - margin;
+ }
+
+ defaultX = x;
+ defaultY = y;
+
+ switch(corner) {
+ case TOP_LEFT | BOTTOM_LEFT:
+ x -= bWidth - margin - 20;
+ case TOP_RIGHT | BOTTOM_RIGHT:
+ x += bWidth - margin - 20;
+ }
+
+ closingX = x;
+ closingY = y;
+
+ alpha = 0;
+ }
+
+ public var defaultX:Float = 0;
+ public var defaultY:Float = 0;
+
+ public var closingX:Float = 0;
+ public var closingY:Float = 0;
+
+ public var showingTimer:Float = 5;
+ public var timerActive:Bool = false;
+
+ public var progress:Float = 0;
+ public override function update(elapsed:Float) {
+ closeButton.color = 0xFFFFFFFF;
+ closeButton.field.color = 0xFF7f7f7f;
+ super.update(elapsed);
+
+ if(timerActive) {
+ showingTimer -= elapsed;
+ if(showingTimer <= 0) {
+ timerActive = false;
+ idleDisappearAnimation();
+ }
+ }
+
+ textField.follow(this, 5, bHeight / 2 - textField.height / 2);
+ closeButton.follow(this, bWidth-closeButton.bWidth - 7, bHeight / 2 - closeButton.bHeight / 2);
+ closeButton.field.alpha = alpha * .7;
+ //closeButton.alpha = alpha;
+ textField.alpha = alpha;
+ }
+
+ public function appearAnimation() {
+ x = closingX; alpha=0; FlxTween.cancelTweensOf(this, ["x", "alpha"]);
+ FlxTween.tween(this, {x: defaultX}, .4, {ease: FlxEase.circInOut});
+ FlxTween.tween(this, {alpha: 1}, .3, {ease: FlxEase.sineOut, startDelay: .1, onComplete: (_) -> {
+ if(showingTimer > 0) timerActive = true;
+ }});
+
+ showedAnimation = true;
+ }
+
+ public var isClosing:Bool = false;
+
+ public function disappearAnimation() {
+ if (isClosing) return;
+ isClosing = true;
+
+ // TODO: make it so you can click through the notification
+ closeButton.active = false;
+ closeButton.selectable = false;
+ closeButton.canBeHovered = false;
+ selectable = false;
+ canBeHovered = false;
+
+ x = defaultX; alpha = 1; FlxTween.cancelTweensOf(this, ["x", "alpha"]);
+ FlxTween.tween(this, {alpha: 0}, .3, {ease: FlxEase.sineOut, onComplete: (_) -> {
+ if(onRemove != null) onRemove(this);
+ destroy();
+ }});
+
+ showedAnimation = false;
+ }
+
+ public function idleDisappearAnimation() {
+ if (isClosing) return;
+ isClosing = true;
+
+ x = defaultX; alpha = 1; FlxTween.cancelTweensOf(this, ["x", "alpha"]);
+ FlxTween.tween(this, {alpha: 0}, .3, {ease: FlxEase.sineOut});
+ FlxTween.tween(this, {x: closingX}, .4, {ease: FlxEase.circInOut, onComplete: (_) -> {
+ if(onRemove != null) onRemove(this);
+ destroy();
+ }});
+
+ showedAnimation = false;
+ }
+}
+
+enum abstract Corner(Int) {
+ var TOP_LEFT = 0;
+ var TOP_RIGHT = 1;
+ var BOTTOM_LEFT = 2;
+ var BOTTOM_RIGHT = 3;
+}
\ No newline at end of file
diff --git a/source/funkin/game/Character.hx b/source/funkin/game/Character.hx
index 40ddcd74..1a5ad6c6 100644
--- a/source/funkin/game/Character.hx
+++ b/source/funkin/game/Character.hx
@@ -78,7 +78,7 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset
playAnim(event.animName, event.force, event.context, event.reversed, event.frame);
}
- public function new(x:Float, y:Float, ?character:String = "bf", isPlayer:Bool = false, switchAnims:Bool = true)
+ public function new(x:Float, y:Float, ?character:String = "bf", isPlayer:Bool = false, switchAnims:Bool = true, disableScripts:Bool = false)
{
super(x, y);
@@ -114,7 +114,12 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset
continue;
}
// Loads the script
- script = Script.create(Paths.script(Path.withoutExtension(xmlPath), null, true));
+ if(!disableScripts) {
+ script = Script.create(Paths.script(Path.withoutExtension(xmlPath), null, true));
+ } else {
+ script = new DummyScript(curCharacter);
+ }
+
script.setParent(this);
script.load();
applyXML(xml);
@@ -539,7 +544,7 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset
public static function getList(?mods:Bool = false):Array {
return [
for (path in Paths.getFolderContent('data/characters/', true, mods ? MODS : BOTH))
- if (Path.extension(path) == "xml") Path.withoutDirectory(Path.withoutExtension(path))
+ if (Path.extension(path) == "xml") CoolUtil.getFilename(path)
];
}
}
diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx
index 5b601944..158dc746 100644
--- a/source/funkin/game/PlayState.hx
+++ b/source/funkin/game/PlayState.hx
@@ -587,7 +587,7 @@ class PlayState extends MusicBeatState
for (event in SONG.events) songEvents.pushOnce(event.name);
for (file in Paths.getFolderContent('data/events/', true, fromMods ? MODS : BOTH)) {
- var fileName:String = Path.withoutExtension(Path.withoutDirectory(file));
+ var fileName:String = CoolUtil.getFilename(file);
if (EventsData.eventsList.contains(fileName) && songEvents.contains(fileName)) {
addScript(file);
}
diff --git a/source/funkin/game/SplashGroup.hx b/source/funkin/game/SplashGroup.hx
index 75008a29..ffa9f135 100644
--- a/source/funkin/game/SplashGroup.hx
+++ b/source/funkin/game/SplashGroup.hx
@@ -37,6 +37,7 @@ class SplashGroup extends FlxTypedGroup {
// immediatly draw once and put image in GPU to prevent freezes
// TODO: change to graphics cache
+ @:privateAccess
splash.drawComplex(FlxG.camera);
} catch(e:Dynamic) {
Logs.trace('Couldn\'t parse splash data for "${path}": ${e.toString()}', ERROR);
diff --git a/source/funkin/game/Stage.hx b/source/funkin/game/Stage.hx
index f42cc90e..f4f13d70 100644
--- a/source/funkin/game/Stage.hx
+++ b/source/funkin/game/Stage.hx
@@ -1,5 +1,6 @@
package funkin.game;
+import flixel.util.typeLimit.OneOfTwo;
import flixel.FlxState;
import flixel.math.FlxPoint;
import funkin.backend.scripting.Script;
@@ -12,65 +13,103 @@ import haxe.xml.Access;
using StringTools;
class Stage extends FlxBasic implements IBeatReceiver {
+ public var extra:Map = [];
+
public var stageXML:Access;
public var stagePath:String;
+ public var stageFile:String;
+ public var stageName:String;
public var stageSprites:Map = [];
public var stageScript:Script;
public var state:FlxState;
public var characterPoses:Map = [];
- private var spritesParentFolder = "";
+ public var defaultZoom:Float = 1.05;
+ public var startCam = new FlxPoint();
- public function getSprite(name:String) {
+ public var onXMLLoaded:(Access, Array)->Array = null;
+ public var onNodeLoaded:(Access, Dynamic)->Dynamic = null;
+ public var onNodeFinished:(Access, Dynamic)->Void = null;
+ public var onAddSprite:(FlxObject)->Void = null;
+
+ public var spritesParentFolder = "";
+
+ public inline function getSprite(name:String) {
return stageSprites[name];
}
- public function new(stage:String, ?state:FlxState) {
+ public function new(stage:String, ?state:FlxState, autoLoad:Bool = true) {
super();
if (state == null) state = PlayState.instance;
if (state == null) state = FlxG.state;
this.state = state;
- stagePath = Paths.xml('stages/$stage');
+ stageFile = stage;
+
+ stagePath = Paths.xml('stages/$stageFile');
try {
if (Assets.exists(stagePath)) stageXML = new Access(Xml.parse(Assets.getText(stagePath)).firstElement());
} catch(e) {
- Logs.trace('Couldn\'t load stage "$stage": ${e.message}', ERROR);
+ Logs.trace('Couldn\'t load stage "$stageFile": ${e.message}', ERROR);
}
- if (PlayState.instance != null) {
- stageScript = Script.create(Paths.script('data/stages/$stage'));
+ if (autoLoad) loadXml(stageXML);
+ }
+
+ public static var DEFAULT_ATTRIBUTES:Array = ["name", "startCamPosX", "startCamPosY", "zoom", "folder"];
+
+ public function loadXml(xml:Access, forceLoadAll:Bool = false) {
+ if (PlayState.instance == state) {
+ stageScript = Script.create(Paths.script('data/stages/$stageFile'));
PlayState.instance.scripts.add(stageScript);
stageScript.load();
}
- if (stageXML != null) {
- if (PlayState.instance != null) {
- var parsed:Null;
- if(stageXML.has.startCamPosX && (parsed = Std.parseFloat(stageXML.att.startCamPosX)) != null) PlayState.instance.camFollow.x = parsed;
- if(stageXML.has.startCamPosY && (parsed = Std.parseFloat(stageXML.att.startCamPosY)) != null) PlayState.instance.camFollow.y = parsed;
- if(stageXML.has.zoom && (parsed = Std.parseFloat(stageXML.att.zoom)) != null) PlayState.instance.defaultCamZoom = parsed;
- PlayState.instance.curStage = stageXML.has.name ? stageXML.att.name : stage;
+ if (xml != null) {
+ var parsed:Null;
+ if((parsed = Std.parseFloat(xml.getAtt("startCamPosX"))).isNotNull()) startCam.x = parsed;
+ if((parsed = Std.parseFloat(xml.getAtt("startCamPosY"))).isNotNull()) startCam.y = parsed;
+ if((parsed = Std.parseFloat(xml.getAtt("zoom"))).isNotNull()) defaultZoom = parsed;
+
+ stageName = xml.getAtt("name").getDefault(stageFile);
+
+ if (PlayState.instance == state) {
+ if(xml.has.startCamPosX) PlayState.instance.camFollow.x = startCam.x;
+ if(xml.has.startCamPosY) PlayState.instance.camFollow.y = startCam.y;
+ if(xml.has.zoom) PlayState.instance.defaultCamZoom = defaultZoom;
+ PlayState.instance.curStage = stageName;
}
- if (stageXML.has.folder) {
- spritesParentFolder = stageXML.att.folder;
+ if (xml.has.folder) {
+ spritesParentFolder = xml.att.folder;
if (!spritesParentFolder.endsWith("/")) spritesParentFolder += "/";
}
- var elems = [];
- for(node in stageXML.elements) {
- if (node.name == "high-memory" && !Options.lowMemoryMode)
+ // Load custom attributes
+ for(att in xml.x.attributes())
+ if(!DEFAULT_ATTRIBUTES.contains(att))
+ extra.set(att, xml.x.get(att));
+
+ var elems:Array = [];
+ // some way to tag that the sprites are from the group
+ for(node in xml.elements) {
+ if (node.name == "high-memory" && (!Options.lowMemoryMode || forceLoadAll))
+ for(e in node.elements)
+ elems.push(e);
+ else if (node.name == "low-memory" && (Options.lowMemoryMode || forceLoadAll))
for(e in node.elements)
elems.push(e);
else
elems.push(node);
}
- if (PlayState.instance != null) {
- var event = PlayState.instance.scripts.event("onStageXMLParsed", EventManager.get(StageXMLEvent).recycle(this, stageXML, elems));
+ if (PlayState.instance == state) {
+ var event = PlayState.instance.scripts.event("onStageXMLParsed", EventManager.get(StageXMLEvent).recycle(this, xml, elems));
elems = event.elems;
}
+ if(onXMLLoaded != null) {
+ elems = onXMLLoaded(xml, elems);
+ }
for(node in elems) {
var sprite:Dynamic = switch(node.name) {
@@ -79,19 +118,16 @@ class Stage extends FlxBasic implements IBeatReceiver {
var spr = XMLUtil.createSpriteFromXML(node, spritesParentFolder, LOOP);
- if (!node.has.zoomfactor && PlayState.instance != null)
- spr.initialZoom = PlayState.instance.defaultCamZoom;
-
stageSprites.set(spr.name, spr);
- state.add(spr);
- spr;
+ addSprite(spr);
case "box" | "solid":
if ( !node.has.name || !node.has.width || !node.has.height) continue;
- var spr = new FlxSprite(
+ var spr = new FunkinSprite(
(node.has.x) ? Std.parseFloat(node.att.x).getDefault(0) : 0,
(node.has.y) ? Std.parseFloat(node.att.y).getDefault(0) : 0
);
+ spr.name = node.getAtt("name");
(node.name == "solid" ? spr.makeSolid : spr.makeGraphic)(
Std.parseInt(node.att.width),
@@ -99,35 +135,19 @@ class Stage extends FlxBasic implements IBeatReceiver {
(node.has.color) ? CoolUtil.getColorFromDynamic(node.att.color) : -1
);
- stageSprites.set(node.getAtt("name"), spr);
- state.add(spr);
- spr;
+ stageSprites.set(spr.name, spr);
+ addSprite(spr);
case "boyfriend" | "bf" | "player":
- addCharPos("boyfriend", node, {
- x: 770,
- y: 100,
- scroll: 1,
- flip: true
- });
+ addCharPos("boyfriend", node, getDefaultPos("boyfriend"));
case "girlfriend" | "gf":
- addCharPos("girlfriend", node, {
- x: 400,
- y: 130,
- scroll: 0.95,
- flip: false
- });
+ addCharPos("girlfriend", node, getDefaultPos("girlfriend"));
case "dad" | "opponent":
- addCharPos("dad", node, {
- x: 100,
- y: 100,
- scroll: 1,
- flip: false
- });
+ addCharPos("dad", node, getDefaultPos("dad"));
case "character" | "char":
if (!node.has.name) continue;
addCharPos(node.att.name, node);
case "ratings" | "combo":
- if (PlayState.instance == null) continue;
+ if (PlayState.instance != state) continue;
PlayState.instance.comboGroup.setPosition(
Std.parseFloat(node.getAtt("x")).getDefault(PlayState.instance.comboGroup.x),
Std.parseFloat(node.getAtt("y")).getDefault(PlayState.instance.comboGroup.y)
@@ -137,50 +157,72 @@ class Stage extends FlxBasic implements IBeatReceiver {
default: null;
}
- if(PlayState.instance != null) {
+ if(PlayState.instance == state) {
sprite = PlayState.instance.scripts.event("onStageNodeParsed", EventManager.get(StageNodeEvent).recycle(this, node, sprite, node.name)).sprite;
}
+ if(onNodeLoaded != null) {
+ sprite = onNodeLoaded(node, sprite);
+ }
if (sprite != null) {
for(e in node.nodes.property)
XMLUtil.applyXMLProperty(sprite, e);
}
+
+ if(onNodeFinished != null) {
+ onNodeFinished(node, sprite);
+ }
}
}
if (characterPoses["girlfriend"] == null)
- addCharPos("girlfriend", null, {
- x: 400,
- y: 130,
- scroll: 0.95,
- flip: false
- });
+ addCharPos("girlfriend", null, getDefaultPos("girlfriend"));
if (characterPoses["dad"] == null)
- addCharPos("dad", null, {
- x: 100,
- y: 100,
- scroll: 1,
- flip: false
- });
+ addCharPos("dad", null, getDefaultPos("dad"));
if (characterPoses["boyfriend"] == null)
- addCharPos("boyfriend", null, {
+ addCharPos("boyfriend", null, getDefaultPos("boyfriend"));
+
+ if (PlayState.instance != state) return;
+ for(k=>e in stageSprites) {
+ stageScript.set(k, e);
+ }
+ }
+
+ public static function getDefaultPos(name:String):StageCharPosInfo {
+ return switch(name) {
+ case "boyfriend" | "bf" | "player": {
x: 770,
y: 100,
scroll: 1,
flip: true
- });
-
- if (PlayState.instance == null) return;
- for(k=>e in stageSprites) {
- stageScript.set(k, e);
+ };
+ case "girlfriend" | "gf": {
+ x: 400,
+ y: 130,
+ scroll: 0.95,
+ flip: false
+ };
+ case "dad" | "opponent": {
+ x: 100,
+ y: 100,
+ scroll: 1,
+ flip: false
+ };
+ default: {
+ x: 0,
+ y: 0,
+ scroll: 1,
+ flip: false
+ };
}
}
public function addCharPos(name:String, node:Access, ?nonXMLInfo:StageCharPosInfo):StageCharPos {
var charPos = new StageCharPos();
charPos.visible = charPos.active = false;
+ charPos.name = name;
if (nonXMLInfo != null) {
charPos.setPosition(nonXMLInfo.x, nonXMLInfo.y);
@@ -191,33 +233,51 @@ class Stage extends FlxBasic implements IBeatReceiver {
if (node != null) {
charPos.x = Std.parseFloat(node.getAtt("x")).getDefault(charPos.x);
charPos.y = Std.parseFloat(node.getAtt("y")).getDefault(charPos.y);
+ charPos.charSpacingX = Std.parseFloat(node.getAtt("spacingx")).getDefault(charPos.charSpacingX);
+ charPos.charSpacingY = Std.parseFloat(node.getAtt("spacingy")).getDefault(charPos.charSpacingY);
charPos.camxoffset = Std.parseFloat(node.getAtt("camxoffset")).getDefault(charPos.camxoffset);
charPos.camyoffset = Std.parseFloat(node.getAtt("camyoffset")).getDefault(charPos.camyoffset);
charPos.skewX = Std.parseFloat(node.getAtt("skewx")).getDefault(charPos.skewX);
charPos.skewY = Std.parseFloat(node.getAtt("skewy")).getDefault(charPos.skewY);
charPos.alpha = Std.parseFloat(node.getAtt("alpha")).getDefault(charPos.alpha);
+ charPos.angle = Std.parseFloat(node.getAtt("angle")).getDefault(charPos.angle);
charPos.flipX = (node.has.flip || node.has.flipX) ? (node.getAtt("flip") == "true" || node.getAtt("flipX") == "true") : charPos.flipX;
+ charPos.zoomFactor = Std.parseFloat(node.getAtt("zoomfactor")).getDefault(charPos.zoomFactor);
- var scale = Std.parseFloat(node.getAtt("scale")).getDefault(charPos.scale.x);
- charPos.scale.set(scale, scale);
+ if (node.has.scale) {
+ var scale:Null = Std.parseFloat(node.att.scale);
+ if (scale.isNotNull()) charPos.scale.set(scale, scale);
+ }
+ if (node.has.scalex) {
+ var scale:Null = Std.parseFloat(node.att.scalex);
+ if (scale.isNotNull()) charPos.scale.x = scale;
+ }
+ if (node.has.scaley) {
+ var scale:Null = Std.parseFloat(node.att.scaley);
+ if (scale.isNotNull()) charPos.scale.y = scale;
+ }
if (node.has.scroll) {
var scroll:Null = Std.parseFloat(node.att.scroll);
if (scroll != null) charPos.scrollFactor.set(scroll, scroll);
- } else {
- if (node.has.scrollx) {
- var scroll:Null = Std.parseFloat(node.att.scrollx);
- if (scroll != null) charPos.scrollFactor.x = scroll;
- }
- if (node.has.scrolly) {
- var scroll:Null = Std.parseFloat(node.att.scrolly);
- if (scroll != null) charPos.scrollFactor.y = scroll;
- }
+ }
+ if (node.has.scrollx) {
+ var scroll:Null = Std.parseFloat(node.att.scrollx);
+ if (scroll != null) charPos.scrollFactor.x = scroll;
+ }
+ if (node.has.scrolly) {
+ var scroll:Null = Std.parseFloat(node.att.scrolly);
+ if (scroll != null) charPos.scrollFactor.y = scroll;
}
}
- state.add(charPos);
- return characterPoses[name] = charPos;
+ return addSprite(characterPoses[name] = charPos);
+ }
+
+ function addSprite(sprite:T):T {
+ state.add(sprite);
+ if(onAddSprite != null) onAddSprite(sprite);
+ return sprite;
}
public inline function isCharFlipped(posName:String, def:Bool = false)
@@ -239,19 +299,29 @@ class Stage extends FlxBasic implements IBeatReceiver {
public function measureHit(curMeasure:Int) {}
- public static function getList(?mods:Bool = false):Array {
+ public static function getList(?mods:Bool = false, ?xmlOnly:Bool = false):Array {
var list:Array = [];
- for (path in Paths.getFolderContent('data/stages/', true, mods ? MODS : BOTH))
- if (Path.extension(path) == "xml" || Path.extension(path) == "hx") {
- var file:String = Path.withoutDirectory(Path.withoutExtension(path));
- list.pushOnce(file);
+ var extensions:Array = ["xml"];
+ if (!xmlOnly) extensions.push("hx");
+
+ for (path in Paths.getFolderContent("data/stages/", false, mods ? MODS : BOTH)) {
+ var extension = Path.extension(path);
+ if (extensions.contains(extension)) {
+ list.pushOnce("test");
+ list.pushOnce(Path.withoutExtension(path));
}
+ }
+
+ trace(list);
return list;
}
}
class StageCharPos extends FlxObject {
+ public var extra:Map = [];
+
+ public var name:String;
public var charSpacingX:Float = 20;
public var charSpacingY:Float = 0;
public var camxoffset:Float = 0;
@@ -261,6 +331,7 @@ class StageCharPos extends FlxObject {
public var alpha:Float = 1;
public var flipX:Bool = false;
public var scale:FlxPoint = FlxPoint.get(1, 1);
+ public var zoomFactor:Float = 1;
public function new() {
super();
@@ -273,13 +344,51 @@ class StageCharPos extends FlxObject {
super.destroy();
}
+ private var _id:Float = -1;
+
+ private var oldInfo:OldCharInfo = null;
+
public function prepareCharacter(char:Character, id:Float = 0) {
+ _id = id;
+ oldInfo = getOldInfo(char);
char.setPosition(x + (id * charSpacingX), y + (id * charSpacingY));
char.scrollFactor.set(scrollFactor.x, scrollFactor.y);
char.scale.x *= scale.x; char.scale.y *= scale.y;
char.cameraOffset += FlxPoint.weak(camxoffset, camyoffset);
char.skew.x += skewX; char.skew.y += skewY;
char.alpha *= alpha;
+ char.angle += angle;
+ char.zoomFactor *= zoomFactor;
+ }
+
+ public function getOldInfo(char:Character) {
+ return {
+ x: char.x, y: char.y,
+ scrollX: char.scrollFactor.x, scrollY: char.scrollFactor.y,
+ scaleX: char.scale.x, scaleY: char.scale.y,
+ camxoffset: char.cameraOffset.x, camyoffset: char.cameraOffset.y,
+ skewX: char.skew.x, skewY: char.skew.y,
+ alpha: char.alpha, zoomFactor: char.zoomFactor,
+ angle: char.angle
+ }
+ }
+
+ public function revertCharacter(char:Character) {
+ if(oldInfo == null) return;
+ for(field in Reflect.fields(oldInfo)) {
+ switch(field) {
+ case "scrollX": char.scrollFactor.x = oldInfo.scrollX;
+ case "scrollY": char.scrollFactor.y = oldInfo.scrollY;
+ case "scaleX": char.scale.x = oldInfo.scaleX;
+ case "scaleY": char.scale.y = oldInfo.scaleY;
+ case "camxoffset": char.cameraOffset.x = oldInfo.camxoffset;
+ case "camyoffset": char.cameraOffset.y = oldInfo.camyoffset;
+ case "skewX": char.skew.x = oldInfo.skewX;
+ case "skewY": char.skew.y = oldInfo.skewY;
+ default: Reflect.setProperty(char, field, Reflect.field(oldInfo, field));
+ }
+ }
+ oldInfo = null;
}
}
typedef StageCharPosInfo = {
@@ -287,4 +396,20 @@ typedef StageCharPosInfo = {
var y:Float;
var flip:Bool;
var scroll:Float;
+}
+
+typedef OldCharInfo = {
+ var x:Float;
+ var y:Float;
+ var scrollX:Float;
+ var scrollY:Float;
+ var scaleX:Float;
+ var scaleY:Float;
+ var camxoffset:Float;
+ var camyoffset:Float;
+ var skewX:Float;
+ var skewY:Float;
+ var alpha:Float;
+ var zoomFactor:Float;
+ var angle:Float;
}
\ No newline at end of file