From f992b4f65d3f63c515019aeb4c0862f9e07d1015 Mon Sep 17 00:00:00 2001 From: Ali <75845687+aliwahabdev@users.noreply.github.com> Date: Sun, 11 Jun 2023 19:40:09 +0200 Subject: [PATCH] Character Editor (#141) * Character Editor * Update Character.hx * Updated more stuff * Update CharacterEditor.hx * Update DeleteAnimScreen.hx * Fixed indices * Update UpdateAnimScreen.hx * Update Character.hx * Update Character.hx * . * MoOOOre stuff --------- Co-authored-by: Ali <75845687+execoded@users.noreply.github.com> Co-authored-by: NeeEoo --- source/funkin/backend/assets/Paths.hx | 4 + source/funkin/editors/EditorPicker.hx | 2 +- .../funkin/editors/character/AddAnimScreen.hx | 113 +++ .../editors/character/CharacterConfig.hx | 169 +++++ .../editors/character/CharacterEditor.hx | 700 ++++++++++++++++++ .../editors/character/CharacterProperties.hx | 123 +++ .../editors/character/CharacterSelection.hx | 87 +++ .../editors/character/DeleteAnimScreen.hx | 73 ++ .../funkin/editors/character/HelpControls.hx | 27 + .../editors/character/SelectAnimUpdate.hx | 62 ++ .../editors/character/UpdateAnimScreen.hx | 125 ++++ source/funkin/game/Character.hx | 194 +++-- 12 files changed, 1621 insertions(+), 58 deletions(-) create mode 100644 source/funkin/editors/character/AddAnimScreen.hx create mode 100644 source/funkin/editors/character/CharacterConfig.hx create mode 100644 source/funkin/editors/character/CharacterEditor.hx create mode 100644 source/funkin/editors/character/CharacterProperties.hx create mode 100644 source/funkin/editors/character/CharacterSelection.hx create mode 100644 source/funkin/editors/character/DeleteAnimScreen.hx create mode 100644 source/funkin/editors/character/HelpControls.hx create mode 100644 source/funkin/editors/character/SelectAnimUpdate.hx create mode 100644 source/funkin/editors/character/UpdateAnimScreen.hx diff --git a/source/funkin/backend/assets/Paths.hx b/source/funkin/backend/assets/Paths.hx index daf52620..531a0ca1 100644 --- a/source/funkin/backend/assets/Paths.hx +++ b/source/funkin/backend/assets/Paths.hx @@ -120,6 +120,10 @@ class Paths return getPath('songs/$song/charts/$difficulty.json', null); } + static public function character(character:String):String { + return getPath('data/characters/$character.xml', null); + } + inline static public function font(key:String) { return getPath('fonts/$key'); diff --git a/source/funkin/editors/EditorPicker.hx b/source/funkin/editors/EditorPicker.hx index d6402c87..188756b3 100644 --- a/source/funkin/editors/EditorPicker.hx +++ b/source/funkin/editors/EditorPicker.hx @@ -16,7 +16,7 @@ class EditorPicker extends MusicBeatSubstate { { name: "Character Editor", iconID: 1, - state: null + state: funkin.editors.character.CharacterSelection }, { name: "Stage Editor", diff --git a/source/funkin/editors/character/AddAnimScreen.hx b/source/funkin/editors/character/AddAnimScreen.hx new file mode 100644 index 00000000..c01ad927 --- /dev/null +++ b/source/funkin/editors/character/AddAnimScreen.hx @@ -0,0 +1,113 @@ +package funkin.editors.character; + +import flixel.math.FlxPoint; +import funkin.backend.utils.XMLUtil.AnimData; +import funkin.game.Character; + +class AddAnimScreen extends UISubstateWindow +{ + public var char:Character; + public var ghostChar:Character; + + public var addButton:UIButton; + public var cancelButton:UIButton; + public var editor:CharacterEditor; + + public var animName:UITextBox; + public var animPrefix:UITextBox; + public var fps:UITextBox; + public var loop:UICheckbox; + public var indices:UITextBox; + + public function new(char:Character, ghostChar, editor:CharacterEditor) + { + super(); + this.char = char; + this.ghostChar = ghostChar; + } + + public override function create() + { + winTitle = "Add new Animation"; + winWidth = 960; + + super.create(); + + // Animation Properties + var y_ofs = 60; + var y_text_ofs = 20; + + animName = new UITextBox(windowSpr.x + 50, windowSpr.y + y_ofs, ""); + add(animName); + add(new UIText(animName.x, animName.y - y_text_ofs, -1, "Animation Name: ")); + + animPrefix = new UITextBox(animName.x, animName.y + y_ofs, ""); + add(animPrefix); + add(new UIText(animPrefix.x, animPrefix.y - y_text_ofs, -1, "Animation on XML/Atlas file: ")); + + fps = new UITextBox(animPrefix.x, animPrefix.y + y_ofs, "24"); + add(fps); + add(new UIText(fps.x, fps.y - y_text_ofs, -1, "Framerate: ")); + + loop = new UICheckbox(fps.x, fps.y + y_ofs, "Looped", false); + add(loop); + + indices = new UITextBox(loop.x, loop.y + y_ofs, ""); + add(indices); + add(new UIText(indices.x, indices.y - y_text_ofs, -1, "Indices (0, 1, 2, 3): ")); + + // Add / Cancel Buttons + addButton = new UIButton(windowSpr.x + 1, windowSpr.y + 31, "", function() + { + newAnimation(animName.label.text, animPrefix.label.text, Std.parseInt(fps.label.text), loop.checked, indices.label.text); + close(); + }); + addButton.bWidth = addButton.bHeight = 30; + add(addButton); + + var addButtonIcon = new FlxSprite(addButton.x, addButton.y, Paths.image('editors/charter/add-button')); + addButtonIcon.x += (30 - addButtonIcon.width) / 2; + addButtonIcon.y += (30 - addButtonIcon.height) / 2; + add(addButtonIcon); + + cancelButton = new UIButton(addButton.x - 10, addButton.y, "Cancel", function() + { + close(); + }); + cancelButton.x -= cancelButton.bWidth; + add(cancelButton); + } + + public function newAnimation(name:String, anim:String, ?fps:Int = 24, ?looped:Bool = false, ?indices:String) + { + var animType:XMLAnimType = NONE; + + var animData:AnimData = { + name: name, + anim: anim, + fps: fps, + loop: looped, + animType: animType, + indices: [], + x: 0, + y: 0 + }; + + if (indices != "" && indices != null) + { + var indicesSplit = indices.split(","); + + for (indice in indicesSplit) + { + var i = Std.parseInt(indice.trim()); + if (i != null) + animData.indices.push(i); + } + } + + XMLUtil.addAnimToSprite(char, animData); + XMLUtil.addAnimToSprite(ghostChar, animData); + char.characterData.animations.push(animData); + ghostChar.characterData.animations.push(animData); + } +} diff --git a/source/funkin/editors/character/CharacterConfig.hx b/source/funkin/editors/character/CharacterConfig.hx new file mode 100644 index 00000000..0c93959e --- /dev/null +++ b/source/funkin/editors/character/CharacterConfig.hx @@ -0,0 +1,169 @@ +package funkin.editors.character; + +import haxe.xml.Printer; +import sys.io.File; +import sys.FileSystem; +import flixel.group.FlxGroup; +import haxe.xml.Access; +import haxe.Exception; +import flixel.util.FlxColor; +import funkin.game.Character.CharacterData; + +class CharacterConfig extends FlxTypedGroup +{ + public static var DEFAULT_CHARACTER = "bf"; + + public static function loadCharacterData(character:String):CharacterData + { + var xml:Access; + + var charData:CharacterData = { + offsetY: 0, + offsetX: 0, + camOffsetY: 0, + camOffsetX: 0, + quickDancer: false, + icon: character, + iconColor: FlxColor.fromRGB(94, 94, 94), + scale: 1, + isPlayer: false, + isGF: false, + holdTime: 4, + flipX: false, + antialiasing: true, + sprite: "bf", + name: character, + animations: [] + } + + var xmlPath = Paths.xml('characters/$character'); + + var plainXML = Assets.getText(xmlPath); + try + { + var charXML = Xml.parse(plainXML).firstElement(); + if (charXML == null) + throw new Exception("Missing \"character\" node in XML."); + xml = new Access(charXML); + } + + charData = xmlToData(charData, xml); + + return charData; + } + + public static function xmlToData(charData:CharacterData, xml:Access):CharacterData + { + if (xml.has.isPlayer) + charData.isPlayer = (xml.att.isPlayer == "true"); + if (xml.has.isGF) + charData.isGF = (xml.att.isGF == "true"); + if (xml.has.x) + charData.offsetX = Std.parseFloat(xml.att.x); + if (xml.has.y) + charData.offsetY = Std.parseFloat(xml.att.y); + if (xml.has.gameOverChar) + charData.gameOverChar = xml.att.gameOverChar; + if (xml.has.camx) + charData.camOffsetX = Std.parseFloat(xml.att.camx); + if (xml.has.camy) + charData.camOffsetY = Std.parseFloat(xml.att.camy); + if (xml.has.holdTime) + charData.holdTime = CoolUtil.getDefault(Std.parseFloat(xml.att.holdTime), 4); + if (xml.has.flipX) + charData.flipX = (xml.att.flipX == "true"); + if (xml.has.icon) + charData.icon = xml.att.icon; + if (xml.has.iconColor) + charData.iconColor = stringToRGB(xml.att.iconColor); + if (xml.has.scale) + { + var scale = Std.parseFloat(xml.att.scale).getDefault(1); + charData.scale = scale; + } + if (xml.has.antialiasing) + charData.antialiasing = (xml.att.antialiasing == "true"); + if (xml.has.sprite) + charData.sprite = xml.att.sprite; + else + charData.sprite = charData.name; + + for (anim in xml.nodes.anim) + { + var animType:XMLAnimType = NONE; + var animloop = false; + if (anim.has.loop) + animloop = (anim.att.loop == "true"); + charData.animations.push(XMLUtil.extractAnimFromXML(anim, animType, animloop)); + } + return charData; + } + + public static function dataToXml(charData:CharacterData):Access + { + var root = Xml.createElement("character"); + root.set("x", Std.string(Math.floor(charData.offsetX))); + root.set("y", Std.string(Math.floor(charData.offsetY))); + root.set("gameOverChar", Std.string(charData.gameOverChar)); + root.set("camx", Std.string(Math.floor(charData.camOffsetX))); + root.set("camy", Std.string(Math.floor(charData.camOffsetY))); + root.set("holdTime", Std.string(charData.holdTime)); + root.set("flipX", Std.string(charData.flipX)); + root.set("icon", charData.icon); + root.set("iconColor", Std.string(charData.iconColor)); + root.set("scale", Std.string(charData.scale)); + root.set("antialiasing", Std.string(charData.antialiasing)); + root.set("sprite", Std.string(charData.sprite)); + root.set("isPlayer", Std.string(charData.isPlayer)); + root.set("isGF", Std.string(charData.isGF)); + + for (anim in charData.animations) + { + var child:Xml = Xml.createElement('anim'); + child.set("name", Std.string(anim.name)); + child.set("anim", Std.string(anim.anim)); + child.set("loop", Std.string(anim.loop)); + child.set("fps", Std.string(anim.fps)); + child.set("x", Std.string(anim.x)); + child.set("y", Std.string(anim.y)); + child.set("indices", Std.string(anim.indices)); + root.addChild(child); + } + var xml = new Access(Xml.parse('${root.toString()}')); + + return xml; + } + + public static function stringToRGB(rgbColor:String):FlxColor + { + var colorSplit = rgbColor.split(","); + if (colorSplit[0] != null && colorSplit[1] != null && colorSplit[2] != null) + return FlxColor.fromRGB(Std.parseInt(colorSplit[0]), Std.parseInt(colorSplit[1]), Std.parseInt(colorSplit[2])); + else + return FlxColor.fromRGB(90,90,90); + } + + public static function rgbToString(color:FlxColor):String + return '${color.red}, ${color.green}, ${color.blue}'; + /** + * @return Saves the character to the specific song folder path. + */ + public static function save(character:String, xml:String):String + { + var charPath = ''; + var data = '$xml'; + #if sys + var charPath = '${character}'; + + File.saveContent(charPath, data); + + // idk how null reacts to it so better be sure + #end + return xml; + } + + public static function generateCharXML(xml:Xml):String + { + return '\n${Printer.print(xml, true)}'; + } +} diff --git a/source/funkin/editors/character/CharacterEditor.hx b/source/funkin/editors/character/CharacterEditor.hx new file mode 100644 index 00000000..e2515529 --- /dev/null +++ b/source/funkin/editors/character/CharacterEditor.hx @@ -0,0 +1,700 @@ +package funkin.editors.character; + +import funkin.backend.system.framerate.Framerate; +import funkin.backend.FlxAnimate; +import flixel.group.FlxSpriteGroup; +import flixel.graphics.FlxGraphic; +import funkin.backend.utils.XMLUtil.AnimData; +import flixel.math.FlxPoint; +import funkin.editors.ui.UIContextMenu.UIContextMenuOption; +import funkin.game.Character; +import flixel.system.debug.interaction.tools.Pointer.GraphicCursorCross; +import flixel.util.FlxColor; + +class CharacterEditor extends UIState +{ + public var blockedInputs = []; + public var blockedStuff = []; + + public var curChar:String; + + public var char:Character; + public var ghostChar:Character; + public var charLayer:FlxTypedSpriteGroup = new FlxTypedSpriteGroup(0, 0); + public var animAtlas:FlxAnimate; + + public var cameraPointer:FlxSprite; + + public var camFollow:FlxObject; + public var editorCamera:FlxCamera; + public var uiCamera:FlxCamera; + + public var topMenu:Array; + public var topMenuSpr:UITopMenu; + + public var uiGroup:FlxTypedGroup = new FlxTypedGroup(); + public var stage:FlxTypedSpriteGroup = new FlxTypedSpriteGroup(0, 0); + + public var curAnim:Int = 0; + + public var playingAnim:UIText; + public var movingCamText:UIText; + + public var camZoom(default, set):Float = 1; + + public function new(charName:String) + { + super(); + this.curChar = charName; + } + + override function create() + { + super.create(); + + // Camera setup + camFollow = new FlxObject(0, 0, 2, 2); + camFollow.screenCenter(); + add(camFollow); + editorCamera = FlxG.camera; + uiCamera = new FlxCamera(); + uiCamera.bgColor = 0; + FlxG.cameras.add(uiCamera); + FlxG.camera.follow(camFollow); + FlxCamera.defaultCameras = [editorCamera]; + + // Add Stage and Centered Cross + add(stage); + add(ofs_cross); + loadCross(); + + // Indicators + var pointer:FlxGraphic = FlxGraphic.fromClass(GraphicCursorCross); + cameraPointer = new FlxSprite().loadGraphic(pointer); + cameraPointer.setGraphicSize(40, 40); + cameraPointer.updateHitbox(); + cameraPointer.color = FlxColor.WHITE; + + // Character + add(charLayer); + add(cameraPointer); + loadChar(!CharacterConfig.loadCharacterData(curChar).isPlayer); + + // Reload Stage + reloadStage(); + + // UI + loadUI(); + + playingAnim = new UIText((FlxG.width / 2), 30, -1, 'Animation:\n${char.characterData.animations[curAnim].name}', 24); + playingAnim.cameras = [uiCamera]; + + movingCamText = new UIText((FlxG.width / 4) - 310, (FlxG.height / 2), -1, + "Press the left mouse button anywhere on this stage to set the character's camera position.", 24); + movingCamText.cameras = [uiCamera]; + movingCamText.visible = false; + + // Top Menu + add(playingAnim); + add(movingCamText); + add(topMenuSpr); + add(uiGroup); + } + + override function update(elapsed:Float) + { + // Control + controlCamera(elapsed); + controlCharacter(); + changeAnim(); + moveCharPos(); + detectCamClick(); + + ghostChar.setPosition(char.x, char.y); + super.update(elapsed); + } + + public function searchAnim(name:String, ?array:Array):AnimData + { + if (array == null) + array = char.characterData.animations; + for (i in 0...array.length - 1) + if (array[i].name == name) + return array[i]; + return array[0]; + } + + public function getAnimIndex(name:String, ?array:Array):Int + { + if (array == null) + array = char.characterData.animations; + for (i in 0...array.length - 1) + if (array[i].name == name) + return i; + return 0; + } + + function resortAnims(animations:Array) + { + if (getAnimIndex("idle") != 0) + { + var oldanims = animations; + animations.clear(); + animations.push(searchAnim("idle", oldanims)); + for (anim in oldanims) + if (anim != searchAnim("idle", oldanims)) + animations.push(anim); + } + } + + public function reloadChar(isOpponent:Bool, charData:CharacterData) + { + var i:Int = charLayer.members.length - 1; + while (i >= 0) + { + var memb:Character = charLayer.members[i]; + if (memb != null) + { + memb.kill(); + charLayer.remove(memb); + memb.destroy(); + } + --i; + } + charLayer.clear(); + ghostChar = new Character(0, 0, curChar, !isOpponent); + ghostChar.characterData = charData; + ghostChar.debugMode = true; + ghostChar.alpha = 0.6; + + char = new Character(0, 0, curChar, !isOpponent); + char.characterData = charData; + + var charData = char.characterData; + resortAnims(charData.animations); + var availableAnim = searchAnim("idle"); + curAnim = getAnimIndex("idle"); + ghostChar.characterData.animations = char.characterData.animations; + char.playAnim(availableAnim.name, true); + ghostChar.playAnim(availableAnim.name, true); + + char.debugMode = true; + char.setPosition(char.characterData.offsetX + OFFSET_X + 100, char.characterData.offsetY); + + charLayer.add(ghostChar); + charLayer.add(char); + + animAtlas = (char.animateAtlas != null ? char.animateAtlas : null); + + updateCharPos(charData.offsetX, charData.offsetY); + updatePointerPos(); + mixDataToChar(char, charData); + mixDataToChar(ghostChar, charData); + } + + function mixDataToChar(target:Character, data:CharacterData) + { + var sprite = (target.xml.has.sprite ? target.xml.att.sprite : target.curCharacter); + if (data.sprite != sprite) + target.loadSprite(Paths.image('characters/${data.sprite}')); + target.icon = data.icon; + target.flipX = !!data.flipX; + target.antialiasing = data.antialiasing; + target.isPlayer = data.isPlayer; + target.playerOffsets = data.isPlayer; + target.isGF = data.isGF; + target.scale.set(data.scale, data.scale); + target.holdTime = data.holdTime; + target.fixChar(true); + target.updateHitbox(); + } + + function loadChar(isOpponent:Bool) + { + var i:Int = charLayer.members.length - 1; + while (i >= 0) + { + var memb:Character = charLayer.members[i]; + if (memb != null) + { + memb.kill(); + charLayer.remove(memb); + memb.destroy(); + } + --i; + } + charLayer.clear(); + ghostChar = new Character(0, 0, curChar, !isOpponent); + ghostChar.debugMode = true; + ghostChar.alpha = 0.6; + + char = new Character(0, 0, curChar, !isOpponent); + char.debugMode = true; + char.setPosition(char.characterData.offsetX + OFFSET_X + 100, char.characterData.offsetY); + + charLayer.add(ghostChar); + charLayer.add(char); + + var charData = char.characterData; + animAtlas = (char.animateAtlas != null ? char.animateAtlas : null); + updateCharPos(charData.offsetX, charData.offsetY); + resortAnims(char.characterData.animations); + curAnim = getAnimIndex("idle"); + char.playAnim(charData.animations[getAnimIndex("idle")].name, true); + ghostChar.characterData.animations = char.characterData.animations; + + char.updateHitbox(); + ghostChar.updateHitbox(); + updatePointerPos(); + } + + function loadUI() + { + topMenu = [ + { + label: "File", + childs: [ + { + label: "New" + }, + null, + { + label: "Save", + keybind: [CONTROL, S], + onSelect: _file_save, + }, + { + label: "Save As...", + keybind: [CONTROL, SHIFT, S], + onSelect: _file_saveas, + }, + null, + { + label: "Exit", + keybind: [ESCAPE], + onSelect: _file_exit + } + ] + }, + { + label: "Edit", + childs: [ + { + label: "Camera Position", + onSelect: moveCameraPos + }, + { + label: "Properties", + onSelect: edit_properties_window, + } + ] + }, + { + label: "Animation", + childs: [ + { + label: "New Animation", + keybind: [CONTROL, N], + onSelect: add_anim_window, + }, + null, + { + label: "Update Animation", + keybind: [CONTROL, U], + onSelect: update_anim_window, + }, + null, + { + label: "Remove Animation", + keybind: [CONTROL, D], + onSelect: remove_anim_window, + }, + ] + }, + { + label: "Help", + childs: [ + { + label: "Controls", + onSelect: help_controls_window + } + ] + } + ]; + + topMenuSpr = new UITopMenu(topMenu); + topMenuSpr.cameras = uiGroup.cameras = [uiCamera]; + + Framerate.fpsCounter.alpha = 0.4; + Framerate.memoryCounter.alpha = 0.4; + Framerate.codenameBuildField.alpha = 0.4; + } + + var ofs_cross:FlxSpriteGroup = new FlxSpriteGroup(0, 0); + + public function loadCross() + { + var i:Int = ofs_cross.members.length - 1; + while (i >= 0) + { + var memb:FlxSprite = ofs_cross.members[i]; + if (memb != null) + { + memb.kill(); + ofs_cross.remove(memb); + memb.destroy(); + } + --i; + } + ofs_cross.clear(); + + var horizontal_cross = new FlxSprite(0, 0).makeGraphic(2000, 5); + var vertical_cross = new FlxSprite(0, 0).makeGraphic(5, 2000); + horizontal_cross.y += (vertical_cross.height / 2); + vertical_cross.x += (horizontal_cross.width / 2); + ofs_cross.add(horizontal_cross); + ofs_cross.add(vertical_cross); + horizontal_cross.scrollFactor.set(1, 1); + vertical_cross.scrollFactor.set(1, 1); + // ofs_cross.screenCenter(); + ofs_cross.y = -200; + ofs_cross.scrollFactor.set(1, 1); + ofs_cross.alpha = 0.6; + } + + var OFFSET_X:Float = 150; + + public function reloadStage() + { + var i:Int = stage.members.length - 1; + while (i >= 0) + { + var memb:FunkinSprite = stage.members[i]; + if (memb != null) + { + memb.kill(); + stage.remove(memb); + memb.destroy(); + } + --i; + } + stage.clear(); + var playerXDifference = 0; + if (char.isPlayer) + playerXDifference = 670; + + var bg = new FunkinSprite(-600 + OFFSET_X - playerXDifference, -200, Paths.image("stages/default/stageback")); + stage.add(bg); + bg.scrollFactor.set(0.9, 0.9); + + var stageFront = new FunkinSprite(-600 + OFFSET_X - playerXDifference, 600, Paths.image("stages/default/stagefront")); + stageFront.setGraphicSize(Std.int(stageFront.width * 1.1)); + stageFront.updateHitbox(); + stage.add(stageFront); + stageFront.scrollFactor.set(0.9, 0.9); + + stage.setPosition(0, 0); + } + + function hasAtlas():Bool + return (animAtlas != null); + + var movingCam:Bool = false; + var movingCamDefaultPos:FlxPoint = null; + var movingCamDefaultPosCam:FlxPoint = null; + + public function moveCameraPos(_) + { + movingCam = true; + movingCamText.visible = true; + updatePointerPos(); + } + + public function detectCamClick() + { + var charData = char.characterData; + + if (movingCam) + { + if (FlxG.mouse.justPressed) + { + var midx = char.getMidpoint().x; + var midy = char.getMidpoint().y; + + var pos = FlxG.mouse.getWorldPosition(editorCamera); + var placedPos:FlxPoint = new FlxPoint((!char.isPlayer ? pos.x + -(midx) : pos.x + -(midx)), -(pos.y - midy)); + + movingCam = false; + movingCamText.visible = false; + charData.camOffsetX = placedPos.x; + charData.camOffsetY = placedPos.y; + updatePointerPos(); + } + } + } + + function updatePointerPos() + { + var x:Float = char.getMidpoint().x; + var y:Float = char.getMidpoint().y; + /*if (!char.isPlayer) + { */ + x += char.characterData.camOffsetX; + /*} + else + { + x -= char.characterData.camOffsetX; + }*/ + y -= char.characterData.camOffsetY; + + x -= cameraPointer.width / 2; + y -= cameraPointer.height / 2; + cameraPointer.setPosition(x, y); + } + + var movingOffset:Bool = false; + var dragX:Float = -1; + var dragY:Float = -1; + + public function moveCharPos() + { + var charSprite = (hasAtlas() ? char.animateAtlas : char); + var charData = char.characterData; + if (FlxG.mouse.justPressed && FlxG.mouse.overlaps(charSprite)) + { + var pos = FlxG.mouse.getWorldPosition(FlxG.camera); + dragX = charData.offsetX - pos.x; + dragY = charData.offsetY - pos.y; + movingOffset = true; + } + else if (!FlxG.mouse.overlaps(charSprite)) + movingOffset = false; + + if (movingOffset && FlxG.mouse.pressed) + { + var pos = FlxG.mouse.getWorldPosition(editorCamera); + updateCharPos((pos.x + dragX), (pos.y + dragY)); + } + } + + public function updateCharPos(x:Float, y:Float) + { + char.characterData.offsetX = x; + char.characterData.offsetY = y; + char.x = x; + char.y = y; + updatePointerPos(); + } + + public function reloadGhost() + { + ghostChar.loadSprite(char.characterData.sprite); + for (i in 0...char.characterData.animations.length - 1) + { + XMLUtil.addAnimToSprite(ghostChar, char.characterData.animations[i]); + } + + char.alpha = 0.85; + ghostChar.visible = true; + /*if (ghostDropDown.selectedLabel == '') + { + ghostChar.visible = false; + char.alpha = 1; + }*/ + ghostChar.color = 0xFF666688; + ghostChar.antialiasing = char.antialiasing; + ghostChar.playAnim(char.animation.curAnim.name, true); + } + + function controlCharacter() + { + var controlArray:Array = [ + FlxG.keys.justPressed.LEFT, + FlxG.keys.justPressed.RIGHT, + FlxG.keys.justPressed.UP, + FlxG.keys.justPressed.DOWN + ]; + + for (i in 0...controlArray.length) + { + if (controlArray[i]) + { + var holdShift = FlxG.keys.pressed.SHIFT; + var multiplier = 1; + if (holdShift) + multiplier = 10; + + var negaMult:Int = 1; + if (i % 2 == 1) + negaMult = -1; + + // For Player Offsets + if (i == 1 && char.playerOffsets) + negaMult = -1; + if (i == 0 && char.playerOffsets) + negaMult = 1; + + if (i > 1) + { + char.characterData.animations[curAnim].y += negaMult * multiplier; + ghostChar.characterData.animations[curAnim].y += negaMult * multiplier; + } + else + { + char.characterData.animations[curAnim].x += negaMult * multiplier; + ghostChar.characterData.animations[curAnim].x += negaMult * multiplier; + } + + char.animOffsets.set(char.characterData.animations[curAnim].name, + new FlxPoint(char.characterData.animations[curAnim].x, char.characterData.animations[curAnim].y)); + ghostChar.animOffsets.set(char.characterData.animations[curAnim].name, + new FlxPoint(char.characterData.animations[curAnim].x, char.characterData.animations[curAnim].y)); + + char.addOffset(char.characterData.animations[curAnim].anim, char.characterData.animations[curAnim].x, + char.characterData.animations[curAnim].y); + ghostChar.addOffset(char.characterData.animations[curAnim].anim, char.characterData.animations[curAnim].x, + char.characterData.animations[curAnim].y); + /*ghostChar.animOffsets.set(char.animation.curAnim.name, + new FlxPoint(char.characterData.animations[curAnim].x, char.characterData.animations[curAnim].y)); */ + + char.playAnim(char.characterData.animations[curAnim].name, false); + if (ghostChar.animation.curAnim != null + && char.animation.curAnim != null + && char.animation.curAnim.name == ghostChar.animation.curAnim.name) + { + ghostChar.playAnim(char.animation.curAnim.name, false); + } + } + } + } + + function controlCamera(elapsed:Float) + { + var ofs:Float = 500 * elapsed; + var shift:Bool = (FlxG.keys.pressed.SHIFT); + var cam_up:Bool = (FlxG.keys.pressed.W); + var cam_down:Bool = (FlxG.keys.pressed.S); + var cam_left:Bool = (FlxG.keys.pressed.A); + var cam_right:Bool = (FlxG.keys.pressed.D); + var cam_zoomers:Array = [FlxG.keys.pressed.I, FlxG.keys.pressed.O]; + + if (shift) + ofs *= 4; + + if (cam_up) + camFollow.y -= ofs; + if (cam_down) + camFollow.y += ofs; + if (cam_left) + camFollow.x -= ofs; + if (cam_right) + camFollow.x += ofs; + + var zoom_ofs = FlxG.camera.zoom * elapsed; + if (cam_zoomers[0] && camZoom < 3) + { + camZoom += zoom_ofs; + if (camZoom > 3) + camZoom = 3; + } + if (cam_zoomers[1] && camZoom > 0.1) + { + camZoom -= zoom_ofs; + if (camZoom < 0.1) + camZoom = 0.1; + } + } + + function changeAnim() + { + var space = (FlxG.keys.justPressed.SPACE); + var forward = (FlxG.keys.justPressed.L); + var backwards = (FlxG.keys.justPressed.K); + + var changelimit = char.characterData.animations.length; + + if (forward || backwards) + { + if (forward) + curAnim += 1; + else if (backwards) + curAnim -= 1; + + if (curAnim >= changelimit) + curAnim = 0; + else if (curAnim < 0) + curAnim = changelimit - 1; + + char.playAnim(char.characterData.animations[curAnim].name, false); + playingAnim.text = 'Animation:\n${char.characterData.animations[curAnim].name}'; + } + + if (space) + char.playAnim(char.characterData.animations[curAnim].name, true); + + char.updateHitbox(); + ghostChar.updateHitbox(); + } + + // Animation Windows + function add_anim_window(_) + openSubState(new AddAnimScreen(char, ghostChar, this)); + + function update_anim_window(_) + openSubState(new SelectAnimUpdate(char, ghostChar, this)); + + function remove_anim_window(_) + openSubState(new DeleteAnimScreen(char, ghostChar, this)); + + function help_controls_window(_) + openSubState(new HelpControls()); + + function _file_exit(_) + { + FlxG.switchState(new CharacterSelection()); + } + + override function destroy() { + Framerate.fpsCounter.alpha = 1; + Framerate.memoryCounter.alpha = 1; + Framerate.codenameBuildField.alpha = 1; + super.destroy(); + } + + function edit_properties_window(_) + FlxG.state.openSubState(new CharacterProperties(char, ghostChar, this)); + + function _file_save(_) + { + #if sys + var assetPath = Paths.character(char.curCharacter); + var path = Assets.getPath(assetPath); + saveTo(path); + return; + #end + _file_saveas(_); + } + + function _file_saveas(_) + { + var xml = CharacterConfig.dataToXml(char.characterData); + openSubState(new SaveSubstate(CharacterConfig.generateCharXML(xml.x), { + defaultSaveFile: '${char.curCharacter}.xml' + })); + } + + #if sys + function saveTo(path:String) + { + var xml = CharacterConfig.dataToXml(char.characterData); + CharacterConfig.save(path, CharacterConfig.generateCharXML(xml.x)); + } + #end + + function set_camZoom(value:Float):Float + { + editorCamera.zoom = value; + camZoom = value; + return value; + } +} diff --git a/source/funkin/editors/character/CharacterProperties.hx b/source/funkin/editors/character/CharacterProperties.hx new file mode 100644 index 00000000..2c167c9c --- /dev/null +++ b/source/funkin/editors/character/CharacterProperties.hx @@ -0,0 +1,123 @@ +package funkin.editors.character; + +import flixel.util.FlxColor; +import flixel.math.FlxPoint; +import funkin.backend.utils.XMLUtil.AnimData; +import funkin.game.Character; + +class CharacterProperties extends UISubstateWindow +{ + public var char:Character; + public var ghostChar:Character; + public var charData:CharacterData; + + public var applyButton:UIButton; + public var cancelButton:UIButton; + public var editor:CharacterEditor; + + public var curAnimData:AnimData; + + public var spriteFile:UITextBox; + public var gameOverChar:UITextBox; + public var healthIcon:UITextBox; + public var iconColor:UITextBox; + public var flipX:UICheckbox; + public var isPlayer:UICheckbox; + public var isGF:UICheckbox; + public var antialiasing:UICheckbox; + public var scale:UITextBox; + public var holdTime:UITextBox; + + public function new(char:Character, ghostChar, editor:CharacterEditor) + { + super(); + this.char = char; + this.ghostChar = ghostChar; + this.charData = char.characterData; + this.editor = editor; + } + + public override function create() + { + winTitle = 'Character Properties'; + winWidth = 960; + + super.create(); + + // Offsets + var y_ofs = 60; + var x_ofs = 60; + var y_text_ofs = 20; + + // Character Properties + spriteFile = new UITextBox(windowSpr.x + 50, windowSpr.y + 10 + y_ofs, charData.sprite); + add(spriteFile); + add(new UIText(spriteFile.x, spriteFile.y - y_text_ofs, -1, "Image/Atlas file: ")); + + gameOverChar = new UITextBox(spriteFile.x + spriteFile.bWidth + x_ofs, spriteFile.y, charData.gameOverChar); + add(gameOverChar); + add(new UIText(gameOverChar.x, gameOverChar.y - y_text_ofs, -1, "Game Over character: ")); + + healthIcon = new UITextBox(spriteFile.x, spriteFile.y + y_ofs, charData.icon); + add(healthIcon); + add(new UIText(healthIcon.x, healthIcon.y - y_text_ofs, -1, "Health Icon: ")); + + iconColor = new UITextBox(healthIcon.x + healthIcon.bWidth + x_ofs, healthIcon.y, CharacterConfig.rgbToString(charData.iconColor)); + add(iconColor); + add(new UIText(iconColor.x, iconColor.y - y_text_ofs, -1, "Icon color (RGB): ")); + + flipX = new UICheckbox(healthIcon.x, healthIcon.y + y_ofs, "Flip X", charData.flipX); + add(flipX); + + antialiasing = new UICheckbox(flipX.x + flipX.field.width + x_ofs, flipX.y, "Antialiasing", charData.antialiasing); + add(antialiasing); + + isPlayer = new UICheckbox(flipX.x, flipX.y + y_ofs, "Playable Character", charData.isPlayer); + add(isPlayer); + + isGF = new UICheckbox(isPlayer.x + isPlayer.field.width + x_ofs, isPlayer.y, "GF Character", charData.isGF); + add(isGF); + + scale = new UITextBox(isPlayer.x, isPlayer.y + y_ofs, Std.string(charData.scale)); + add(scale); + add(new UIText(scale.x, scale.y - y_text_ofs, -1, "Scale: ")); + + holdTime = new UITextBox(scale.x + scale.bWidth + y_ofs, scale.y, Std.string(charData.holdTime)); + add(holdTime); + add(new UIText(holdTime.x, holdTime.y - y_text_ofs, -1, "Hold time: ")); + + // Apply / Cancel buttons + applyButton = new UIButton(windowSpr.x - x_ofs - 70, windowSpr.y, "Apply", function() + { + updateProperties(spriteFile.label.text, healthIcon.label.text, CharacterConfig.stringToRGB(healthIcon.label.text), flipX.checked, + antialiasing.checked, isPlayer.checked, isGF.checked, Std.parseFloat(scale.label.text), Std.parseFloat(holdTime.label.text), gameOverChar.label.text); + }); + add(applyButton); + + cancelButton = new UIButton(applyButton.x, applyButton.y + y_ofs, "Cancel", function() + { + close(); + }); + add(cancelButton); + } + + public function updateProperties(sprite:String, healthIcon:String, iconColor:FlxColor, flipX:Bool, antialiasing:Bool, isPlayer:Bool, isGF:Bool, + scale:Float, holdTime:Float, gameOverChar:String) + { + // Character Data + charData.sprite = sprite; + charData.icon = healthIcon; + charData.iconColor = iconColor; + charData.flipX = flipX; + charData.antialiasing = antialiasing; + charData.isPlayer = isPlayer; + charData.isGF = isGF; + charData.scale = scale; + charData.holdTime = holdTime; + charData.gameOverChar = gameOverChar; + + editor.reloadChar(!char.isPlayer, charData); + editor.reloadStage(); + editor.loadCross(); + } +} diff --git a/source/funkin/editors/character/CharacterSelection.hx b/source/funkin/editors/character/CharacterSelection.hx new file mode 100644 index 00000000..fdc7de44 --- /dev/null +++ b/source/funkin/editors/character/CharacterSelection.hx @@ -0,0 +1,87 @@ +package funkin.editors.character; + +import flixel.util.FlxColor; +import funkin.game.Character; +import funkin.backend.chart.Chart; +import funkin.game.Character.CharacterData; +import funkin.options.type.TextOption; +import funkin.options.type.IconOption; +import funkin.options.OptionsScreen; +import sys.FileSystem; + +class CharacterSelection extends EditorTreeMenu +{ + public var characterList:CharactersList; + + public override function create() + { + bgType = "charter"; + + super.create(); + + characterList = CharactersList.get(); + + main = new OptionsScreen("Character Editor", "Select a character to edit", [ + for (s in characterList.characters) + new IconOption(s.name, "Press ACCEPT to edit this character.", s.icon, function() + { + FlxG.switchState(new CharacterEditor(s.name)); + }) + ]); + } + + public override function update(elapsed:Float) + { + super.update(elapsed); + } + + public override function onMenuChange() { + super.onMenuChange(); + if (optionsTree.members.length > 1) { // selected a song + // small flashbang + var color:FlxColor = characterList.characters[main.curSelected].iconColor.getDefault(0xFFFFFFFF); + + bg.colorTransform.redOffset = 0.25 * color.red; + bg.colorTransform.greenOffset = 0.25 * color.green; + bg.colorTransform.blueOffset = 0.25 * color.blue; + bg.colorTransform.redMultiplier = FlxMath.lerp(1, color.redFloat, 0.25); + bg.colorTransform.greenMultiplier = FlxMath.lerp(1, color.greenFloat, 0.25); + bg.colorTransform.blueMultiplier = FlxMath.lerp(1, color.blueFloat, 0.25); + } + } +} + +class CharactersList +{ + public var characters:Array = []; + + public function new() + { + } + + public function getCharactersFromSource(source:funkin.backend.assets.AssetsLibraryList.AssetSource) + { + var charactersFound:Array = []; + + charactersFound = Paths.getFolderContent('data/characters', false, source); + + if (charactersFound.length > 0) + { + for (s in charactersFound) + if (s.endsWith(".xml")) + characters.push(CharacterConfig.loadCharacterData(s.replace(".xml", ""))); + return false; + } + return true; + } + + public static function get() + { + var characterList = new CharactersList(); + + if (characterList.getCharactersFromSource(MODS)) + characterList.getCharactersFromSource(SOURCE); + + return characterList; + } +} diff --git a/source/funkin/editors/character/DeleteAnimScreen.hx b/source/funkin/editors/character/DeleteAnimScreen.hx new file mode 100644 index 00000000..83da3351 --- /dev/null +++ b/source/funkin/editors/character/DeleteAnimScreen.hx @@ -0,0 +1,73 @@ +package funkin.editors.character; + +import flixel.math.FlxRandom; +import cpp.Random; +import flixel.math.FlxPoint; +import funkin.backend.utils.XMLUtil.AnimData; +import funkin.game.Character; + +class DeleteAnimScreen extends UISubstateWindow +{ + public var char:Character; + public var ghostChar:Character; + + public var editor:CharacterEditor; + + public var animButtons:FlxTypedSpriteGroup = new FlxTypedSpriteGroup(365, -10); + + public var closeButton:UIButton; + + public function new(char:Character, ghostChar:Character, editor:CharacterEditor) + { + super(); + this.char = char; + this.ghostChar = ghostChar; + this.editor = editor; + } + + public override function create() + { + winTitle = "Delete Animation"; + winWidth = 480; + winHeight = 700; + + super.create(); + + add(animButtons); + + var y_adder = 60; + for (anim in char.characterData.animations) + { + var newbutton = new UIButton(animButtons.x - 550, animButtons.y + y_adder, anim.name, function() + { + removeAnimation(anim); + close(); + }); + animButtons.add(newbutton); + y_adder += 60; + } + + closeButton = new UIButton(windowSpr.x - 120, windowSpr.y + 20, "Close", function() { + close(); + }); + add(closeButton); + } + + public function removeAnimation(anim:AnimData) + { + var refreshAnim:Bool = false; + if (char.animation.curAnim != null && anim.name == char.animation.curAnim.name) + refreshAnim = true; + + if (char.animation.getByName(anim.name) != null) + char.animation.remove(anim.name); + if (char.animOffsets.exists(anim.name)) + char.animOffsets.remove(anim.name); + char.characterData.animations.remove(anim); + + if (refreshAnim && char.characterData.animations.length > 0) + char.playAnim(char.characterData.animations[0].name, true); + + editor.reloadGhost(); + } +} diff --git a/source/funkin/editors/character/HelpControls.hx b/source/funkin/editors/character/HelpControls.hx new file mode 100644 index 00000000..8340c49f --- /dev/null +++ b/source/funkin/editors/character/HelpControls.hx @@ -0,0 +1,27 @@ +package funkin.editors.character; + +class HelpControls extends UISubstateWindow +{ + public function new() + { + super(); + } + + public override function create() + { + winTitle = 'Help: Controls'; + winWidth = 960; + + super.create(); + + var helpText = new UIText(windowSpr.x + 25, windowSpr.y + 240, -1, "Left/Right/Up/Down Arrow: To change the offset of the current\nplaying animation.\n + Left mouse button on the character: Change character's position.", 24); + add(helpText); + + var closeButton = new UIButton(helpText.x, helpText.y + 160, "Close", function() + { + close(); + }); + add(closeButton); + } +} diff --git a/source/funkin/editors/character/SelectAnimUpdate.hx b/source/funkin/editors/character/SelectAnimUpdate.hx new file mode 100644 index 00000000..c097b922 --- /dev/null +++ b/source/funkin/editors/character/SelectAnimUpdate.hx @@ -0,0 +1,62 @@ +package funkin.editors.character; + +import flixel.math.FlxRandom; +import cpp.Random; +import flixel.math.FlxPoint; +import funkin.backend.utils.XMLUtil.AnimData; +import funkin.game.Character; + +class SelectAnimUpdate extends UISubstateWindow +{ + public var cancelButton:UIButton; + + public var char:Character; + public var ghostChar:Character; + + public var editor:CharacterEditor; + + public var animButtons:FlxTypedSpriteGroup = new FlxTypedSpriteGroup(365, -10); + + public function new(char:Character, ghostChar:Character, editor:CharacterEditor) + { + super(); + this.char = char; + this.ghostChar = ghostChar; + this.editor = editor; + } + + public override function create() + { + winTitle = "Select an animation to update:"; + winWidth = 480; + winHeight = 700; + + super.create(); + + add(animButtons); + + var y_ofs = 60; + + for (anim in char.characterData.animations) + { + var newbutton = new UIButton(animButtons.x - 550, animButtons.y + y_ofs, anim.name, function() + { + open_update_window(anim); + }); + animButtons.add(newbutton); + y_ofs += 60; + } + + cancelButton = new UIButton(windowSpr.x - 10, windowSpr.y + 20, "Cancel", function() + { + close(); + }); + cancelButton.x -= cancelButton.bWidth; + add(cancelButton); + } + + public function open_update_window(anim:AnimData) + { + openSubState(new UpdateAnimScreen(this.char, this.ghostChar, anim, this.editor)); + } +} diff --git a/source/funkin/editors/character/UpdateAnimScreen.hx b/source/funkin/editors/character/UpdateAnimScreen.hx new file mode 100644 index 00000000..39736b2c --- /dev/null +++ b/source/funkin/editors/character/UpdateAnimScreen.hx @@ -0,0 +1,125 @@ +package funkin.editors.character; + +import flixel.math.FlxPoint; +import funkin.backend.utils.XMLUtil.AnimData; +import funkin.game.Character; + +class UpdateAnimScreen extends UISubstateWindow +{ + public var char:Character; + public var ghostChar:Character; + + public var updateButton:UIButton; + public var cancelButton:UIButton; + public var editor:CharacterEditor; + + public var curAnimData:AnimData; + + public var animName:UITextBox; + public var animPrefix:UITextBox; + public var fps:UITextBox; + public var loop:UICheckbox; + public var indices:UITextBox; + + public function new(char:Character, ghostChar, animData:AnimData, editor:CharacterEditor) + { + super(); + this.char = char; + this.ghostChar = ghostChar; + this.curAnimData = animData; + this.editor = editor; + } + + public override function create() + { + winTitle = 'Update Animation: ${curAnimData.name}'; + winWidth = 960; + + super.create(); + + // Animation Properties + var y_ofs = 60; + var y_text_ofs = 20; + + animName = new UITextBox(windowSpr.x + 50, windowSpr.y + y_ofs, curAnimData.name); + add(animName); + add(new UIText(animName.x, animName.y - y_text_ofs, -1, "Animation Name: ")); + + animPrefix = new UITextBox(animName.x, animName.y + y_ofs, curAnimData.anim); + add(animPrefix); + add(new UIText(animPrefix.x, animPrefix.y - y_text_ofs, -1, "Animation on XML/Atlas file: ")); + + fps = new UITextBox(animPrefix.x, animPrefix.y + y_ofs, Std.string(curAnimData.fps)); + add(fps); + add(new UIText(fps.x, fps.y - y_text_ofs, -1, "Framerate: ")); + + loop = new UICheckbox(fps.x, fps.y + y_ofs, "Looped", curAnimData.loop); + add(loop); + + indices = new UITextBox(loop.x, loop.y + y_ofs, Std.string(curAnimData.indices).replace("[", "").replace("]", "")); + add(indices); + add(new UIText(indices.x, indices.y - y_text_ofs, -1, "Indices (0, 1, 2, 3): ")); + + // Add / Cancel Buttons + updateButton = new UIButton(windowSpr.x - 10, windowSpr.y + 31, "Update", function() + { + updateAnimation(animName.label.text, animPrefix.label.text, Std.parseInt(fps.label.text), loop.checked, indices.label.text); + close(); + }); + updateButton.x -= updateButton.bWidth; + add(updateButton); + + cancelButton = new UIButton(updateButton.x, updateButton.y + y_ofs, "Cancel", function() + { + close(); + }); + add(cancelButton); + } + + public function updateAnimation(name:String, ?anim:String, ?fps:Int = 24, ?looped:Bool = false, ?indices:String) + { + var animType:XMLAnimType = NONE; + + var selectedAnim:AnimData = null; + var selectedAnimIndex = 0; + + for (i in 0...char.characterData.animations.length) + { + if (char.characterData.animations[i].name == name) + { + selectedAnim = char.characterData.animations[i]; + selectedAnimIndex = i; + } + } + + if (selectedAnim == null) + return; + + var animData:AnimData = { + name: name, + anim: anim, + fps: fps, + loop: looped, + animType: animType, + indices: [], + x: selectedAnim.x, + y: selectedAnim.y + }; + + if (indices != "" && indices != null) + { + var indicesSplit = indices.split(","); + + for (indice in indicesSplit) + { + var i = Std.parseInt(indice.trim()); + if (i != null) + animData.indices.push(i); + } + } + + XMLUtil.addAnimToSprite(char, animData); + char.characterData.animations[selectedAnimIndex] = animData; + char.playAnim(name, true); + } +} diff --git a/source/funkin/game/Character.hx b/source/funkin/game/Character.hx index 43317cb2..1df53e7e 100644 --- a/source/funkin/game/Character.hx +++ b/source/funkin/game/Character.hx @@ -1,29 +1,49 @@ package funkin.game; +import funkin.editors.character.CharacterConfig; +import flixel.util.FlxColor; import funkin.backend.FunkinSprite; import flixel.graphics.frames.FlxFrame; import flixel.math.FlxPoint; import funkin.backend.system.interfaces.IBeatReceiver; import funkin.backend.system.interfaces.IOffsetCompatible; import funkin.backend.utils.XMLUtil; - import flixel.animation.FlxBaseAnimation; import flixel.graphics.frames.FlxAtlasFrames; import flixel.math.FlxRect; - import haxe.xml.Access; import haxe.Exception; import haxe.io.Path; import funkin.backend.system.Conductor; - import openfl.geom.ColorTransform; - import funkin.backend.scripting.DummyScript; import funkin.backend.scripting.Script; import funkin.backend.scripting.events.*; import funkin.backend.scripting.events.PlayAnimEvent.PlayAnimContext; + using StringTools; +typedef CharacterData = +{ + var offsetX:Float; + var offsetY:Float; + var camOffsetX:Float; + var camOffsetY:Float; + var quickDancer:Bool; + var holdTime:Float; + var isPlayer:Bool; + var flipX:Bool; + var antialiasing:Bool; + var sprite:String; + var name:String; + var animations:Array; + @:optional var isGF:Bool; + @:optional var icon:String; + @:optional var iconColor:FlxColor; + @:optional var scale:Float; + @:optional var gameOverChar:String; +} + @:allow(funkin.desktop.editors.CharacterEditor) @:allow(funkin.game.StrumLine) @:allow(funkin.game.PlayState) @@ -33,9 +53,11 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset private var __stunnedTime:Float = 0; private var __lockAnimThisFrame:Bool = false; + public var stunned(default, set):Bool = false; - private function set_stunned(b:Bool) { + private function set_stunned(b:Bool) + { __stunnedTime = 0; return stunned = b; } @@ -60,26 +82,35 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset public var script:Script; public var xml:Access; + public var characterData:CharacterData; + public var shadowFrame:CharacterShadowFrame; - - public inline function getCameraPosition() { + public inline function getCameraPosition() + { var midpoint = getMidpoint(); - var event = EventManager.get(PointEvent).recycle( - midpoint.x + (isPlayer ? -100 : 150) + globalOffset.x + cameraOffset.x, - midpoint.y - 100 + globalOffset.y + cameraOffset.y); + var event = EventManager.get(PointEvent) + .recycle(midpoint.x + + (isPlayer ? -100 : 150) + + globalOffset.x + + cameraOffset.x, midpoint.y + - 100 + + globalOffset.y + + cameraOffset.y); script.call("onGetCamPos", [event]); midpoint.put(); // this event cannot be cancelled return new FlxPoint(event.x, event.y); } - public function playSingAnim(direction:Int, suffix:String = "", Context:PlayAnimContext = SING, Force:Bool = true, Reversed:Bool = false, Frame:Int = 0) { + public function playSingAnim(direction:Int, suffix:String = "", Context:PlayAnimContext = SING, Force:Bool = true, Reversed:Bool = false, Frame:Int = 0) + { var anims = ["singLEFT", "singDOWN", "singUP", "singRIGHT"]; var event = EventManager.get(DirectionAnimEvent).recycle('${anims[direction]}$suffix', direction, suffix, Context, Reversed, Frame, Force); script.call("onPlaySingAnim", [event]); - if (!event.cancelled) playAnim(event.animName, event.force, Context, event.reversed, event.frame); + if (!event.cancelled) + playAnim(event.animName, event.force, Context, event.reversed, event.frame); } public function new(x:Float, y:Float, ?character:String = "bf", isPlayer:Bool = false, switchAnims:Bool = true) @@ -92,53 +123,76 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset antialiasing = true; - while(true) { + while (true) + { switch (curCharacter) { // case 'your-char': // To hardcode characters default: // load xml var xmlPath = Paths.xml('characters/$curCharacter'); - if (!Assets.exists(xmlPath)) { + if (!Assets.exists(xmlPath)) + { curCharacter = "bf"; continue; } var plainXML = Assets.getText(xmlPath); - try { + try + { var charXML = Xml.parse(plainXML).firstElement(); - if (charXML == null) throw new Exception("Missing \"character\" node in XML."); + if (charXML == null) + throw new Exception("Missing \"character\" node in XML."); xml = new Access(charXML); - } catch(e) { + } + catch (e) + { Logs.trace('Error while loading character ${curCharacter}: ${e}', ERROR); curCharacter = "bf"; continue; } var sprite:String = curCharacter; - if (xml.has.isPlayer) playerOffsets = (xml.att.isPlayer == "true"); - if (xml.has.isGF) isGF = (xml.att.isGF == "true"); - if (xml.has.x) globalOffset.x = Std.parseFloat(xml.att.x); - if (xml.has.y) globalOffset.y = Std.parseFloat(xml.att.y); - if (xml.has.gameOverChar) gameOverCharacter = xml.att.gameOverChar; - if (xml.has.camx) cameraOffset.x = Std.parseFloat(xml.att.camx); - if (xml.has.camy) cameraOffset.y = Std.parseFloat(xml.att.camy); - if (xml.has.holdTime) holdTime = CoolUtil.getDefault(Std.parseFloat(xml.att.holdTime), 4); - if (xml.has.flipX) flipX = (xml.att.flipX == "true"); - if (xml.has.icon) icon = xml.att.icon; - if (xml.has.scale) { + if (xml.has.isPlayer) + playerOffsets = (xml.att.isPlayer == "true"); + if (xml.has.isGF) + isGF = (xml.att.isGF == "true"); + if (xml.has.x) + globalOffset.x = Std.parseFloat(xml.att.x); + if (xml.has.y) + globalOffset.y = Std.parseFloat(xml.att.y); + if (xml.has.gameOverChar) + gameOverCharacter = xml.att.gameOverChar; + if (xml.has.camx) + cameraOffset.x = Std.parseFloat(xml.att.camx); + if (xml.has.camy) + cameraOffset.y = Std.parseFloat(xml.att.camy); + if (xml.has.holdTime) + holdTime = CoolUtil.getDefault(Std.parseFloat(xml.att.holdTime), 4); + if (xml.has.flipX) + flipX = (xml.att.flipX == "true"); + if (xml.has.icon) + icon = xml.att.icon; + if (xml.has.scale) + { var scale = Std.parseFloat(xml.att.scale).getDefault(1); this.scale.set(scale, scale); updateHitbox(); } - if (xml.has.antialiasing) antialiasing = (xml.att.antialiasing == "true"); - if (xml.has.sprite) sprite = xml.att.sprite; + if (xml.has.antialiasing) + antialiasing = (xml.att.antialiasing == "true"); + if (xml.has.sprite) + sprite = xml.att.sprite; loadSprite(Paths.image('characters/$sprite')); - for(anim in xml.nodes.anim) { + + for (anim in xml.nodes.anim) + { XMLUtil.addXMLAnimation(this, anim); } + characterData = CharacterConfig.loadCharacterData(character); + // Loads the script and calls it's "create" function script = Script.create(Paths.script(Path.withoutExtension(xmlPath), null, true)); script.setParent(this); @@ -147,7 +201,8 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset } break; } - if (script == null) script = new DummyScript(curCharacter); + if (script == null) + script = new DummyScript(curCharacter); /** NON CONVERTED CHARACTERS - DO NOT REMOVE @@ -170,7 +225,14 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset // antialiasing = false; // flipX = true; + fixChar(true); + dance(); + script.call("postCreate"); + } + + public function fixChar(switchAnims:Bool = false) + { isDanceLeftDanceRight = (hasAnimation("danceLeft") && hasAnimation("danceRight")); // alternative to xor operator @@ -189,10 +251,9 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset switchOffset('singLEFT', 'singRIGHT'); switchOffset('singLEFTmiss', 'singRIGHTmiss'); } - if (isPlayer) flipX = !flipX; + if (isPlayer) + flipX = !flipX; __baseFlipped = flipX; - dance(); - script.call("postCreate"); } var __baseFlipped:Bool = false; @@ -202,7 +263,8 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset { super.update(elapsed); script.call("update", [elapsed]); - if (stunned) { + if (stunned) + { __stunnedTime += elapsed; if (__stunnedTime > 5 / 60) stunned = false; @@ -221,22 +283,27 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset { var event = EventManager.get(DanceEvent).recycle(danced); script.call("onDance", [event]); - if (event.cancelled) return; + if (event.cancelled) + return; switch (curCharacter) { // hardcode custom dance animations here default: - if (isDanceLeftDanceRight) { + if (isDanceLeftDanceRight) + { playAnim((danced = !danced) ? 'danceLeft' : 'danceRight', DANCE); - } else + } + else playAnim('idle', DANCE); } } } - public function tryDance() { - switch(lastAnimContext) { + public function tryDance() + { + switch (lastAnimContext) + { case SING | MISS: if (lastHit + (Conductor.stepCrochet * holdTime) < Conductor.songPosition) dance(); @@ -261,24 +328,30 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset */ public var danceInterval:Int = 1; - public override function beatHit(curBeat:Int) { + public override function beatHit(curBeat:Int) + { script.call("beatHit", [curBeat]); if (danceInterval < 1) danceInterval = 1; - if (danceOnBeat && curBeat % danceInterval == 0 && !__lockAnimThisFrame) { + if (danceOnBeat && curBeat % danceInterval == 0 && !__lockAnimThisFrame) + { tryDance(); } } - public override function stepHit(curStep:Int) { + public override function stepHit(curStep:Int) + { script.call("stepHit", [curStep]); // nothing } var __reverseDrawProcedure:Bool = false; - public override function getScreenBounds(?newRect:FlxRect, ?camera:FlxCamera):FlxRect { - if (__reverseDrawProcedure) { + + public override function getScreenBounds(?newRect:FlxRect, ?camera:FlxCamera):FlxRect + { + if (__reverseDrawProcedure) + { scale.x *= -1; var bounds = super.getScreenBounds(newRect, camera); scale.x *= -1; @@ -289,8 +362,11 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset var __drawingShadowFrame = false; var __oldColorTransform = new ColorTransform(); - public override function draw() { - if (!__drawingShadowFrame && shadowFrame != null) { + + public override function draw() + { + if (!__drawingShadowFrame && shadowFrame != null) + { __drawingShadowFrame = true; var oldFrame = _frame; @@ -322,7 +398,8 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset __drawingShadowFrame = false; } - if ((isPlayer != playerOffsets) != (flipX != __baseFlipped)) { + if ((isPlayer != playerOffsets) != (flipX != __baseFlipped)) + { __reverseDrawProcedure = true; flipX = !flipX; @@ -332,7 +409,8 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset scale.x *= -1; __reverseDrawProcedure = false; - } else + } + else super.draw(); } @@ -340,7 +418,8 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset { var event = EventManager.get(PlayAnimEvent).recycle(AnimName, Force, Reversed, Frame, Context); script.call("onPlayAnim", [event]); - if (event.cancelled) return; + if (event.cancelled) + return; super.playAnim(event.animName, event.force, event.context, event.reverse, event.startingFrame); @@ -349,16 +428,16 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset lastHit = Conductor.songPosition; } - - - public override function destroy() { + public override function destroy() + { super.destroy(); cameraOffset.put(); globalOffset.put(); } - public inline function getIcon() { + public inline function getIcon() + { return (icon != null) ? icon : curCharacter; } @@ -397,7 +476,8 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset } } -typedef CharacterShadowFrame = { +typedef CharacterShadowFrame = +{ var anim:String; var frame:FlxFrame; -} \ No newline at end of file +}