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); + } + + + + + <section> + <set name="curY" value="title.y + TO" /> + + <stepper name="xStepper" label="X" value="charPos.x" x="col(0)" y="curY" step="1" width="100" /> + <stepper name="yStepper" label="Y" value="charPos.y" x="last.x + last.bWidth + XOFFSET - 20" y="curY" step="1" width="100" /> + + <stepper name="scrollXStepper" label="Scroll X" value="charPos.scrollFactor.x" x="col(1)" y="curY" precision="2" step="0.01" width="100" /> + <stepper name="scrollYStepper" label="Scroll Y" value="charPos.scrollFactor.y" x="last.x + last.bWidth + XOFFSET - 20" y="curY" precision="2" step="0.01" width="100" /> + + <stepper name="scaleXStepper" label="Scale X" value="charPos.scale.x" x="col(2)" y="curY" precision="2" step="0.01" width="100" /> + <stepper name="scaleYStepper" label="Scale Y" value="charPos.scale.y" x="last.x + last.bWidth + XOFFSET - 20" y="curY" precision="2" step="0.01" width="100" /> + </section> + + <section> + <set name="curY" value="xStepper.y + xStepper.bHeight + YOFFSET" /> + + <stepper name="camxStepper" label="Cam X" value="charPos.camxoffset" x="col(0)" y="curY" step="1" width="100" /> + <stepper name="camyStepper" label="Cam Y" value="charPos.camyoffset" x="last.x + last.bWidth + XOFFSET - 20" y="curY" step="1" width="100" /> + + <label name="spacingLabel" text="Spacing (X, Y)" x="col(1)" y="curY" size="15" /> + <stepper name="spacingXStepper" value="charPos.charSpacingX" x="col(1)" y="curY" precision="2" step="0.01" width="100" /> + <stepper name="spacingYStepper" value="charPos.charSpacingY" x="last.x + last.bWidth + XOFFSET - 20" y="curY" precision="2" step="0.01" width="100" /> + + <stepper name="skewXStepper" label="Skew X" value="charPos.skewX" x="col(2)" y="curY" precision="2" step="0.01" width="100" /> + <stepper name="skewYStepper" label="Skew Y" value="charPos.skewY" x="last.x + last.bWidth + XOFFSET - 20" y="curY" precision="2" step="0.01" width="100" /> + </section> + + <section> + <set name="curY" value="camxStepper.y + camxStepper.bHeight + YOFFSET" /> + + <stepper name="alphaStepper" label="Alpha" value="charPos.alpha" x="col(0)" y="curY" precision="2" step="0.01" width="100" /> + <stepper name="angleStepper" label="Angle" value="charPos.angle" x="last.x + last.bWidth + XOFFSET - 20" y="curY" precision="2" step="0.01" width="100" /> + + <stepper name="zoomFactorStepper" label="Zoom Factor" value="charPos.zoomFactor" x="col(1)" y="curY" precision="2" step="0.01" width="100" /> + + <section if="charPos.extra.exists('extraPos')"> + <textbox name="charName" label="Position Name" value="charPos.name" x="col(2)" y="curY" width="100"/> + </section> + </section> + + <section> + <set name="curY" value="alphaStepper.y + alphaStepper.bHeight + YOFFSET - 20" /> + + <checkbox name="flipXCheckbox" text="Flip X" value="charPos.flipX" x="col(0)" y="curY" /> + + <radio for="memoryCheck" name="highMemoryRadio" text="High Memory Only" value="getEx('highMemory')" x="last.field.x + last.field.width + XOFFSET * 2" y="curY" /> + <radio for="memoryCheck" name="lowMemoryRadio" text="Low Memory Only" value="getEx('lowMemory')" x="last.field.x + last.field.width + XOFFSET * 2" y="curY" /> + </section> + + <section if="previewSprite != null"> + <title name="previewTitle" x="20" y="self.winHeight - 220" text="Preview Transform:" size="15" /> + </section> + <section if="previewSprite == null"> + <exec> + self.windowSpr.bHeight -= 170; + </exec> + </section> + + <exec> + 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); + } + </exec> +</window> \ 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 @@ +<window width="1056" height="520" title="Editing Sprite ${button.getName()}"> + <set name="MARGIN" value="20" /> + <set name="XOFFSET" value="50" /> + <set name="YOFFSET" value="40" /> + <set name="TO" value="64" /> + + <exec> + 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); + } + </exec> + + <title name="title" x="20" y="30 + 16" text="Sprite Data" /> + + <section> <!-- ROW 1 --> + <set name="curY" value="title.y + TO" /> + + <textbox name="nameTextBox" label="Name Identifier" value="button.getName()" x="col(0)" y="curY" width="200" height="32" /> + <textbox name="spriteTextBox" value="getEx('imageFile')" x="last.x + last.bWidth + XOFFSET" y="curY" width="200" height="32" /> + <stepper name="alphaStepper" label="Alpha" value="sprite.alpha" x="spriteTextBox.x + spriteTextBox.bWidth + XOFFSET" y="curY" precision="2" step="0.01" width="100" /> + <label name="imageFile" text="Image File (WIP: needs reload) In (${stage.spritesParentFolder})" x="spriteTextBox.x" y="spriteTextBox.y - 14" size="15" /> + </section> + + <section> <!-- ROW 2 --> + <set name="curY" value="nameTextBox.y + nameTextBox.bHeight + YOFFSET" /> + + <stepper name="xStepper" label="X" value="sprite.x" x="col(0)" y="curY" step="1" width="100" /> + <stepper name="yStepper" label="Y" value="sprite.y" x="last.x + last.bWidth + XOFFSET - 20" y="curY" step="1" width="100" /> + + <stepper name="scrollXStepper" label="Scroll X" value="sprite.scrollFactor.x" x="spriteTextBox.x" y="curY" precision="2" step="0.01" width="100" /> + <stepper name="scrollYStepper" label="Scroll Y" value="sprite.scrollFactor.y" x="last.x + last.bWidth + XOFFSET - 20" y="curY" precision="2" step="0.01" width="100" /> + + <stepper name="scaleXStepper" label="Scale X" value="sprite.scale.x" x="col(2)" y="curY" precision="2" step="0.01" width="100" /> + <stepper name="scaleYStepper" label="Scale Y" value="sprite.scale.y" x="last.x + last.bWidth + XOFFSET - 20" y="curY" precision="2" step="0.01" width="100" /> + </section> + + <section> <!-- ROW 3 --> + <set name="curY" value="xStepper.y + xStepper.bHeight + YOFFSET" /> + + <stepper name="angleStepper" label="Angle" value="sprite.angle" x="col(0)" y="curY" precision="2" step="0.01" width="100" /> + <stepper name="zoomFactorStepper" label="Zoom Factor" value="sprite.zoomFactor" x="last.x + last.bWidth + XOFFSET - 20" y="curY" precision="2" step="0.01" width="100" /> + + <!-- TODO: Simplify this --> + <dropdown name="animType" label="Animation Type" value="sprite.spriteAnimType" x="col(1)" y="curY" width="150" height="32"> + <item label="NONE" value="0" /> + <item label="BEAT" value="1" /> + <item label="LOOP" value="2" /> + </dropdown> + + <stepper name="skewXStepper" label="Skew X" value="sprite.skew.x" x="col(2)" y="curY" precision="2" step="0.01" width="100" /> + <stepper name="skewYStepper" label="Skew Y" value="sprite.skew.y" x="last.x + last.bWidth + XOFFSET - 20" y="curY" precision="2" step="0.01" width="100" /> + </section> + + <section> <!-- ROW 4 --> + <set name="curY" value="angleStepper.y + angleStepper.bHeight + YOFFSET" /> + + <checkbox name="antialiasingCheckbox" text="Antialiasing" value="sprite.antialiasing" x="col(0)" y="curY" /> + <radio for="memoryCheck" name="highMemoryRadio" text="High Memory Only" value="getEx('highMemory')" x="col(1)" y="curY" /> + <radio for="memoryCheck" name="lowMemoryRadio" text="Low Memory Only" value="getEx('lowMemory')" x="col(2)" y="curY" /> + + <!-- TODO: Animation list --> + </section> + + <section> <!-- RIGHT SIDE --> + <set name="curY" value="title.y + TO" /> + <set name="listWidth" value="250" /> + + <buttonlist name="animations" label="Animations (TODO)" x="col(3)" y="curY" width="listWidth" height="330" buttonSizeX="listWidth-10" buttonSizeY="35" buttonSpacing="5"> + <texture path="editors/ui/inputbox" /> + <camSpacing value="0" /> + + <code> + buttonlist.addButton.exists = false; + </code> + + <!-- <code> + 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)); + </code> --> + </buttonlist> + + <text text="Tip: To access custom properties, hold shift when clicking edit" x="col(2) - 60" y="self.winHeight - 50" size="15" /> + </section> + + + <!-- Preview Transform --> + + <section if="previewSprite != null"> + <title name="previewTitle" x="20" y="self.winHeight - 150" text="Preview Transform:" size="15" /> + </section> + + <exec> + 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); + } + </exec> +</window> \ 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 @@ +<window width="470" height="620" title="Editing Stage Info '${stage.stageName}'"> + <set name="MARGIN" value="20" /> + <set name="XOFFSET" value="50" /> + <set name="YOFFSET" value="40" /> + <set name="TO" value="64" /> + <set name="storedProps" value="[]" /> + + <exec> + 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); + } + } + </exec> + + <title name="title" x="MARGIN" y="30 + 16" text="Stage Info" /> + + <section> <!-- ROW 1 --> + <set name="curY" value="title.y + TO" /> + + <textbox name="nameTextBox" label="Stage Name" value="stage.stageName" x="col(0)" y="curY" width="200" height="32" /> + <textbox name="spritePathTextBox" value="stage.spritesParentFolder" x="col(1)" y="curY" width="200" height="32" /> + <label text="Sprite Path (Needs Refresh)" x="spritePathTextBox.x" y="spritePathTextBox.y - 14" size="15" /> + </section> + + <section> <!-- ROW 2 --> + <set name="curY" value="nameTextBox.y + nameTextBox.bHeight + YOFFSET" /> + + <label text="Start Cam Pos (X, Y)" x="col(0)" y="curY" /> + <stepper name="camxStepper" value="stage.startCam.x" x="col(0)" y="curY" step="1" width="120" /> + <stepper name="camyStepper" value="stage.startCam.y" x="last.x + last.bWidth + XOFFSET - 30" y="curY" step="1" width="120" /> + + <stepper name="zoomStepper" label="Zoom" value="stage.defaultZoom" x="col(1)" y="curY" precision="2" step="0.01" width="100" /> + </section> + + <section> + <set name="curY" value="camxStepper.y + camxStepper.bHeight + YOFFSET" /> + <set name="listWidth" value="self.winWidth - MARGIN * 2" /> + + <buttonlist name="attributesList" label="Custom Attributes" x="col(0)" y="curY" width="listWidth" height="290" buttonSizeX="listWidth-10" buttonSizeY="35" buttonSpacing="5"> + <texture path="editors/ui/inputbox" /> + <camSpacing value="0" /> + + <code> + 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)); + </code> + </buttonlist> + </section> + + <exec> + 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)); + } + </exec> +</window> \ 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 @@ +<window width="350" height="570" title="Editing XML <${button.getName()} />"> + <set name="MARGIN" value="20" /> + <set name="XOFFSET" value="30" /> + <set name="YOFFSET" value="40" /> + <set name="TO" value="64" /> + + <exec> + function col(c:Int) { + return MARGIN + (150 + XOFFSET) * c; + } + </exec> + + <title name="title" x="MARGIN" y="30 + 16" text="Xml Data" /> + + <section> + <set name="curY" value="title.y + TO" /> + + <textbox name="nodeNameTextBox" label="Element Name" value="button.getName()" x="col(0)" y="curY" width="280" height="32" /> + </section> + <section> + <set name="curY" value="nodeNameTextBox.y + nodeNameTextBox.bHeight + YOFFSET - 30" /> + + <radio for="memoryCheck" name="highMemoryRadio" text="High Memory Only" value="button.highMemory" x="col(0)" y="curY" /> + <radio for="memoryCheck" name="lowMemoryRadio" text="Low Memory Only" value="button.lowMemory" x="col(0)" y="last.y + last.check.height + YOFFSET - 30" /> + </section> + + <section> + <set name="curY" value="lowMemoryRadio.y + lowMemoryRadio.check.height + YOFFSET" /> + <set name="listWidth" value="self.winWidth - MARGIN * 2" /> + + <buttonlist name="attributesList" label="Attributes" x="col(0)" y="curY" width="listWidth" height="260" buttonSizeX="listWidth-10" buttonSizeY="35" buttonSpacing="5"> + <texture path="editors/ui/inputbox" /> + <camSpacing value="0" /> + + <code> + 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)); + </code> + </buttonlist> + </section> + + <exec> + 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); + } + </exec> +</window> \ 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 @@ +<window width="450" height="570" title="Editing XML ${type}"> + <!-- BASED ON unknownEditScreen.xml --> + <set name="MARGIN" value="20" /> + <set name="XOFFSET" value="30" /> + <set name="YOFFSET" value="40" /> + <set name="TO" value="64" /> + + <exec> + function col(c:Int) { + return MARGIN + (150 + XOFFSET) * c; + } + </exec> + + <title name="title" x="MARGIN" y="30 + 16" text="Xml Data" /> + + <section> + <set name="curY" value="title.y + TO" /> + <set name="listWidth" value="self.winWidth - MARGIN * 2" /> + + <buttonlist name="attributesList" label="Attributes" x="col(0)" y="curY" width="listWidth" height="400" buttonSizeX="listWidth-10" buttonSizeY="35" buttonSpacing="5"> + <texture path="editors/ui/inputbox" /> + <camSpacing value="0" /> + + <code> + 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)); + </code> + </buttonlist> + </section> + + <exec> + 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); + } + </exec> +</window> \ 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 @@ +<!-- praying to god we remove this file later on --> + +<stage zoom="0.9" name="stage" folder="stages/default/" startCamPosY="600" startCamPosX="1000" example="value"> + <sprite name="normBG" sprite="stagefront" x="-500" y="100" scale="0.75" updateHitbox="true" /> + <sprite name="noUpdateHitbox" sprite="stagefront" x="-500" y="300" scale="0.75" /> + <sprite name="ro-tat-ay" sprite="stagefront" x="-500" y="700" scale="0.75" updateHitbox="true" angle="37" /> + <box name="box" x="0" y="0" width="200" height="200" color="0xFF0000" /> + <solid name="box2" x="0" y="300" width="200" height="200" color="0x00FF00" /> + <unknown-sprite /> + + <unknown-sprite-1-attr name="test" /> +</stage> \ 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<FlxPoint> = 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 <optional arguments>", + "", + "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<String>) { - __build(args, ["test", getBuildTarget(), "-DTEST_BUILD"]); + __runLime(args, ["test", getBuildTarget(), "-DTEST_BUILD"]); } public static function build(args:Array<String>) { - __build(args, ["build", getBuildTarget(), "-DTEST_BUILD"]); + __runLime(args, ["build", getBuildTarget(), "-DTEST_BUILD"]); } public static function release(args:Array<String>) { - __build(args, ["build", getBuildTarget()]); + __runLime(args, ["build", getBuildTarget()]); } public static function testRelease(args:Array<String>) { - __build(args, ["test", getBuildTarget()]); + __runLime(args, ["test", getBuildTarget()]); } - private static function __build(args:Array<String>, arg:Array<String>) { + public static function run(args:Array<String>) { + __runLime(args, ["run", getBuildTarget()]); + } + + private static function __runLime(args:Array<String>, arg:Array<String>) { 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 @@ <git name="flixel-addons" url="https://github.com/FNF-CNE-Devs/flixel-addons" /> <!-- Other Libraries --> - <git name="hscript-improved" url="https://github.com/FNF-CNE-Devs/hscript-improved" ref="custom-classes" /> + <git name="hscript-improved" url="https://github.com/FNF-CNE-Devs/hscript-improved" /> <git name="flxanimate" url="https://github.com/FNF-CNE-Devs/flxanimate" /> <git name="hxdiscord_rpc" url="https://github.com/FNF-CNE-Devs/hxdiscord_rpc" /> <lib name="hxvlc" /> 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<Field> + { + 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xml> + <pragma once="true" /> + + <files id="haxe"> + <compilerflag value="-I${this_dir}/include/mac" if="macos" /> + </files> + + <files id="__main__"> + <compilerflag value="-I${this_dir}/include/mac" if="macos" /> + </files> + + <files id="external-code"> + <section if="macos"> + <compilerflag value="-I${this_dir}/include/mac" /> + + <file name="${this_dir}/src/mac/CursorHelper.mm" /> + <file name="${this_dir}/src/mac/Mac.mm" /> + </section> + </files> + + <target id="haxe"> + <section if="macos"> + <vflag name="-framework" value="IOKit" /> + <vflag name="-framework" value="AppKit" /> + <vflag name="-framework" value="Cocoa" /> + <vflag name="-framework" value="Foundation" /> + </section> + + <files id="external-code" /> + </target> +</xml> 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 <Foundation/Foundation.h> + +@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 <Cocoa/Cocoa.h> +#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 <unordered_map> + +#import <Cocoa/Cocoa.h> +#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<const char *, NSCursor *> 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<BeatAnim> = []; public var name:String; public var zoomFactor:Float = 1; - public var initialZoom:Float = 1; public var debugMode:Bool = false; public var animDatas:Map<String, AnimData> = []; + 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<Float>, indices:DrawData<Int>, uvtData:DrawData<Float>, ?colors:DrawData<Int>, ?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<String> = ["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<String>, ?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<FlxPoint, Array<FlxPoint>>, ?camera:FlxCamera, _width:Float = 1, _height:Float = 1):Array<FlxPoint> + { + //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<FlxPoint> = 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 <sys/sysctl.h>") -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<StageCreationData> -> 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<UIContextMenuOption>; + public var topMenuSpr:UITopMenu; + + public var uiGroup:FlxTypedGroup<FlxSprite> = new FlxTypedGroup<FlxSprite>(); + public var stageSpritesWindow:UIButtonList<StageElementButton>; + + public var xmlMap:Map<FlxObject, Access> = new Map<FlxObject, Access>(); + + public var chars:Array<Character> = []; + public var charMap:Map<String, Character> = []; + + 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<StageChange> = new UndoList<StageChange>(); + + 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<Dynamic> = []; + var orderNodes:Array<Access> = []; + stage = new Stage(__stage, this, false); + stage.onXMLLoaded = function(xml:Access, elems:Array<Access>) { + 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<StageElementButton>(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 "<!DOCTYPE codename-engine-stage>\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<FlxPoint> = 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<StageEditorEdge> = [ + //// 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<FlxPoint> = 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<String, String> +}; + +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<FunkinSprite>) from Array<FunkinSprite> to Array<FunkinSprite> { + public inline function new(?array:Array<FunkinSprite>) + 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<String> = []; + + public override function create() + { + bgType = "charter"; + super.create(); + + var modsList:Array<String> = Stage.getList(true, true); + + var list:Array<OptionType> = [ + 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', '<!DOCTYPE codename-engine-stage>\n<stage folder="${creation.path}">\n</stage>'); + #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<T:UIButton> 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<T:UIButton> 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<T:UIButton> 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<UIContextMenuOption>; 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<UIContextMenuOption>, callback:UIContextMenuCallback, x:Float, y:Float) { + public function new(options:Array<UIContextMenuOption>, 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<String>; + public var items:Array<DropDownItem>; + 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<String>, index:Int = 0) { + public static function getItems(options:Array<String>):Array<DropDownItem> { + 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<OneOfTwo<DropDownItem, String>>, index:Int = 0) { super(x, y, width - height, height, 'editors/ui/inputbox'); // using same sprite cause fuck you - this.options = options; + var _items:Array<DropDownItem> = []; + + 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<DropDownItem>, value:Dynamic):Int { + return [for(o in items) o.value].indexOf(value); + } + + public static function indexOfItemLabel(items:Array<DropDownItem>, 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<UIWindow, UISubstateWindow> = 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<UIRadioButton> = 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<Float> = null; + public var middleAlpha:Null<Float> = null; + public var bottomAlpha:Null<Float> = 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<String, UISprite> = []; + + var xmlPath:String; + var filename:String; + var customVariables:Map<String, Dynamic>; + + public function new(xmlPath:String, customVariables:Map<String, Dynamic> = 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<UIRadioButton> = 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<Access>) { + return [for(e in i) e]; + } + + function addLayout(elements:Array<Access>) { + 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<DropDownItem> = []; + 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<UIButton>( + 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<Dynamic>) { + if (interp == null) return null; + if (!interp.variables.exists(name)) return null; + + var func = interp.variables.get(name); + if (func != null && Reflect.isFunction(func)) + return Reflect.callMethod(null, func, args); + + return null; + } + + 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<FlxCamera> = []; 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<UIContextMenuOption>, ?callback:UIContextMenuCallback, ?x:Float, ?y:Float) { + public function openContextMenu(options:Array<UIContextMenuOption>, ?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<UIContextMenuOption>`. 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<FlxBasic>; 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<FlxBasic>(); + 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<String> { 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<FunkinSprite> { // 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<String, Dynamic> = []; + public var stageXML:Access; public var stagePath:String; + public var stageFile:String; + public var stageName:String; public var stageSprites:Map<String, FlxSprite> = []; public var stageScript:Script; public var state:FlxState; public var characterPoses:Map<String, StageCharPos> = []; - private var spritesParentFolder = ""; + public var defaultZoom:Float = 1.05; + public var startCam = new FlxPoint(); - public function getSprite(name:String) { + public var onXMLLoaded:(Access, Array<Access>)->Array<Access> = 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<String> = ["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<Float>; - 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<Float>; + 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<Access> = []; + // 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<Float> = Std.parseFloat(node.att.scale); + if (scale.isNotNull()) charPos.scale.set(scale, scale); + } + if (node.has.scalex) { + var scale:Null<Float> = Std.parseFloat(node.att.scalex); + if (scale.isNotNull()) charPos.scale.x = scale; + } + if (node.has.scaley) { + var scale:Null<Float> = Std.parseFloat(node.att.scaley); + if (scale.isNotNull()) charPos.scale.y = scale; + } if (node.has.scroll) { var scroll:Null<Float> = Std.parseFloat(node.att.scroll); if (scroll != null) charPos.scrollFactor.set(scroll, scroll); - } else { - if (node.has.scrollx) { - var scroll:Null<Float> = Std.parseFloat(node.att.scrollx); - if (scroll != null) charPos.scrollFactor.x = scroll; - } - if (node.has.scrolly) { - var scroll:Null<Float> = Std.parseFloat(node.att.scrolly); - if (scroll != null) charPos.scrollFactor.y = scroll; - } + } + if (node.has.scrollx) { + var scroll:Null<Float> = Std.parseFloat(node.att.scrollx); + if (scroll != null) charPos.scrollFactor.x = scroll; + } + if (node.has.scrolly) { + var scroll:Null<Float> = 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<T:FlxObject>(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<String> { + public static function getList(?mods:Bool = false, ?xmlOnly:Bool = false):Array<String> { var list:Array<String> = []; - 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<String> = ["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<String, Dynamic> = []; + + 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