the stage editor shit

This commit is contained in:
Kolo 2024-09-29 19:07:09 +02:00 committed by Cameron Taylor
parent b2647fe09f
commit 0a5419d7fc
24 changed files with 4025 additions and 3 deletions

View file

@ -20,6 +20,10 @@ class StageData
@:optional
public var cameraZoom:Null<Float>;
@:default("shared")
@:optional
public var directory:Null<String>;
public function new()
{
this.version = StageRegistry.STAGE_DATA_VERSION;
@ -198,6 +202,32 @@ typedef StageDataProp =
@:default("sparrow")
@:optional
var animType:String;
/**
* The angle of the prop, as a float.
* @default 1.0
*/
@:optional
@:default(0.0)
var angle:Float;
/**
* The blend mode of the prop, as a string.
* Just like in photoshop.
* @default Nothing.
*/
@:default("")
@:optional
var blend:String;
/**
* The color of the prop overlay, as a hex string.
* White overlays, or the ones with the value #FFFFFF, do not appear.
* @default `#FFFFFF`
*/
@:default("#FFFFFF")
@:optional
var color:String;
};
typedef StageDataCharacter =

View file

@ -256,6 +256,10 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
propSprite.scrollFactor.x = dataProp.scroll[0];
propSprite.scrollFactor.y = dataProp.scroll[1];
propSprite.angle = dataProp.angle;
propSprite.color = FlxColor.fromString(dataProp.color);
@:privateAccess if (!isSolidColor) propSprite.blend = BlendMode.fromString(dataProp.blend);
propSprite.zIndex = dataProp.zIndex;
propSprite.flipX = dataProp.flipX;

View file

@ -10,6 +10,7 @@ import funkin.save.migrator.SaveDataMigrator;
import funkin.save.migrator.SaveDataMigrator;
import funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle;
import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme;
import funkin.ui.debug.stageeditor.StageEditorState.StageEditorTheme;
import funkin.util.SerializerUtil;
import thx.semver.Version;
import thx.semver.Version;
@ -146,6 +147,14 @@ class Save
hitsoundVolumeOpponent: 1.0,
themeMusic: true
},
optionsStageEditor:
{
previousFiles: [],
moveStep: "1px",
angleStep: 5,
theme: StageEditorTheme.Light
}
};
}
@ -428,6 +437,91 @@ class Save
return data.unlocks.oldChar;
}
public var stageEditorPreviousFiles(get, set):Array<String>;
function get_stageEditorPreviousFiles():Array<String>
{
if (data.optionsStageEditor.previousFiles == null) data.optionsStageEditor.previousFiles = [];
return data.optionsStageEditor.previousFiles;
}
function set_stageEditorPreviousFiles(value:Array<String>):Array<String>
{
// Set and apply.
data.optionsStageEditor.previousFiles = value;
flush();
return data.optionsStageEditor.previousFiles;
}
public var stageEditorHasBackup(get, set):Bool;
function get_stageEditorHasBackup():Bool
{
if (data.optionsStageEditor.hasBackup == null) data.optionsStageEditor.hasBackup = false;
return data.optionsStageEditor.hasBackup;
}
function set_stageEditorHasBackup(value:Bool):Bool
{
// Set and apply.
data.optionsStageEditor.hasBackup = value;
flush();
return data.optionsStageEditor.hasBackup;
}
public var stageEditorMoveStep(get, set):String;
function get_stageEditorMoveStep():String
{
if (data.optionsStageEditor.moveStep == null) data.optionsStageEditor.moveStep = "1px";
return data.optionsStageEditor.moveStep;
}
function set_stageEditorMoveStep(value:String):String
{
// Set and apply.
data.optionsStageEditor.moveStep = value;
flush();
return data.optionsStageEditor.moveStep;
}
public var stageEditorAngleStep(get, set):Float;
function get_stageEditorAngleStep():Float
{
if (data.optionsStageEditor.angleStep == null) data.optionsStageEditor.angleStep = 5;
return data.optionsStageEditor.angleStep;
}
function set_stageEditorAngleStep(value:Float):Float
{
// Set and apply.
data.optionsStageEditor.angleStep = value;
flush();
return data.optionsStageEditor.angleStep;
}
public var stageEditorTheme(get, set):StageEditorTheme;
function get_stageEditorTheme():StageEditorTheme
{
if (data.optionsStageEditor.theme == null) data.optionsStageEditor.theme = StageEditorTheme.Light;
return data.optionsStageEditor.theme;
}
function set_stageEditorTheme(value:StageEditorTheme):StageEditorTheme
{
// Set and apply.
data.optionsStageEditor.theme = value;
flush();
return data.optionsStageEditor.theme;
}
/**
* When we've seen a character unlock, add it to the list of characters seen.
* @param character
@ -1068,6 +1162,11 @@ typedef RawSaveData =
* The user's preferences specific to the Chart Editor.
*/
var optionsChartEditor:SaveDataChartEditorOptions;
/**
* The user's preferences specific to the Stage Editor.
*/
var optionsStageEditor:SaveDataStageEditorOptions;
};
typedef SaveApiData =
@ -1441,3 +1540,39 @@ typedef SaveDataChartEditorOptions =
*/
var ?playbackSpeed:Float;
};
typedef SaveDataStageEditorOptions =
{
// a lot of these things were copied from savedatacharteditoroptions
/**
* Whether the Stage Editor created a backup the last time it closed.
* Prompt the user to load it, then set this back to `false`.
* @default `false`
*/
var ?hasBackup:Bool;
/**
* Previous files opened in the Stage Editor.
* @default `[]`
*/
var ?previousFiles:Array<String>;
/**
* The Step at which an Object or Character is moved.
* @default `1px`
*/
var ?moveStep:String;
/**
* The Step at which an Object is rotated.
* @default `5`
*/
var ?angleStep:Float;
/**
* Theme in the Stage Editor.
* @default `StageEditorTheme.Light`
*/
var ?theme:StageEditorTheme;
};

View file

@ -60,7 +60,7 @@ class DebugMenuSubState extends MusicBeatSubState
// createItem("Input Offset Testing", openInputOffsetTesting);
createItem("CHARACTER SELECT", openCharSelect, true);
createItem("ANIMATION EDITOR", openAnimationEditor);
// createItem("STAGE EDITOR", openStageEditor);
createItem("STAGE EDITOR", openStageEditor);
// createItem("TEST STICKERS", testStickers);
#if sys
createItem("OPEN CRASH LOG FOLDER", openLogFolder);
@ -125,6 +125,7 @@ class DebugMenuSubState extends MusicBeatSubState
function openStageEditor()
{
trace('Stage Editor');
FlxG.switchState(() -> new funkin.ui.debug.stageeditor.StageEditorState());
}
#if sys

View file

@ -0,0 +1,119 @@
package funkin.ui.debug.stageeditor;
import funkin.data.animation.AnimationData;
import funkin.graphics.FunkinSprite;
import funkin.modding.events.ScriptEvent;
/**
* Contains all the Logic needed for Stage Editor. Only for Stage Editor, as in the gameplay StageProps and Boppers will be used.
*/
class StageEditorObject extends FunkinSprite
{
/**
* The internal Name of the Object.
*/
public var name:String = "Unnamed";
/**
* What animation to play upon starting.
*/
public var startingAnimation:String = "";
public var animDatas:Map<String, AnimationData> = [];
override public function new()
{
super();
}
/**
* Whether the Object is currently being modified in the Stage Editor.
*/
public var isDebugged(default, set):Bool = true;
function set_isDebugged(value:Bool)
{
this.isDebugged = value;
if (value == false) // plays upon starting yippee!!!
playAnim(startingAnimation, true);
else
{
if (animation.curAnim != null)
{
animation.stop();
offset.set();
updateHitbox();
}
}
return value;
}
public function playAnim(name:String, restart:Bool = false, reversed:Bool = false)
{
if (!animation.getNameList().contains(name)) return;
animation.play(name, restart, reversed, 0);
if (animDatas.exists(name)) offset.set(animDatas[name].offsets[0], animDatas[name].offsets[1]);
else
offset.set();
}
/**
* On which beat should it dance?
*/
public var danceEvery:Float = 0;
/**
* Internal, handles danceLeft and danceRight.
*/
var _danced:Bool = true;
public function dance(restart:Bool = false)
{
if (isDebugged) return;
var idle = animation.getNameList().contains("idle");
var dancing = animation.getNameList().contains("danceLeft") && animation.getNameList().contains("danceRight");
if (!idle && !dancing) return;
if (dancing)
{
if (_danced) playAnim("danceRight", restart);
else
playAnim("danceLeft", restart);
_danced = !_danced;
}
else if (idle)
{
playAnim("idle", restart);
}
}
public function addAnim(name:String, prefix:String, offsets:Array<Float>, indices:Array<Int>, frameRate:Int = 24, looped:Bool = true, flipX:Bool = false,
flipY:Bool = false)
{
if (indices.length > 0) animation.addByIndices(name, prefix, indices, "", frameRate, looped, flipX, flipY);
else
animation.addByPrefix(name, prefix, frameRate, looped, flipX, flipY);
if (animation.getNameList().contains(name)) // sometimes the animation doesnt add
{
animDatas.set(name,
{
name: name,
prefix: prefix,
offsets: offsets,
looped: looped,
frameRate: frameRate,
flipX: flipX,
flipY: flipY,
frameIndices: indices
});
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,6 @@
package funkin.ui.debug.stageeditor.components;
import haxe.ui.containers.dialogs.Dialog;
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/about.xml"))
class AboutDialog extends Dialog {}

View file

@ -0,0 +1,80 @@
package funkin.ui.debug.stageeditor.components;
import haxe.ui.containers.dialogs.Dialog;
import haxe.ui.containers.dialogs.Dialog.DialogButton;
import funkin.util.FileUtil;
import haxe.io.Path;
import funkin.util.DateUtil;
import funkin.util.WindowUtil;
using StringTools;
@:xml('
<dialog id="backupAvailableDialog" width="475" height="150" title="Hey! Listen!">
<vbox width="100%" height="100%">
<label text="There is a chart backup available, would you like to open it?\n" width="100%" textAlign="center" />
<spacer height="6" />
<label id="backupTimeLabel" text="Jan 1, 1970 0:00" width="100%" textAlign="center" />
<spacer height="100%" />
<hbox width="100%">
<button text="No Thanks" id="dialogCancel" />
<spacer width="100%" />
<button text="Take Me There" id="buttonGoToFolder" />
<spacer width="100%" />
<button text="Open It" id="buttonOpenBackup" />
</hbox>
</vbox>
</dialog>
')
class BackupAvailableDialog extends Dialog
{
override public function new(state:StageEditorState, filePath:String)
{
super();
if (!FileUtil.doesFileExist(filePath)) return;
// time text
var fileDate = Path.withoutExtension(Path.withoutDirectory(filePath));
var dateParts = fileDate.split("-");
while (dateParts.length < 8)
dateParts.push("0");
var year:Int = Std.parseInt(dateParts[2]) ?? 0; // copied parts from ChartEditorImportExportHandler.hx
var month:Int = Std.parseInt(dateParts[3]) ?? 1;
var day:Int = Std.parseInt(dateParts[4]) ?? 0;
var hour:Int = Std.parseInt(dateParts[5]) ?? 0;
var minute:Int = Std.parseInt(dateParts[6]) ?? 0;
var second:Int = Std.parseInt(dateParts[7]) ?? 0;
backupTimeLabel.text = DateUtil.generateCleanTimestamp(new Date(year, month - 1, day, hour, minute, second));
// button callbacks
dialogCancel.onClick = function(_) hideDialog(DialogButton.CANCEL);
buttonGoToFolder.onClick = function(_) {
// :[
#if sys
var absoluteBackupsPath:String = Path.join([Sys.getCwd(), StageEditorState.BACKUPS_PATH]);
WindowUtil.openFolder(absoluteBackupsPath);
#end
}
buttonOpenBackup.onClick = function(_) {
if (FileUtil.doesFileExist(filePath) && state.welcomeDialog != null) // doing a check in case a sleezy FUCK decides to delete the backup file AFTER dialog opens
{
state.welcomeDialog.loadFromFilePath(filePath);
}
hideDialog(DialogButton.APPLY);
}
// uhhh
onDialogClosed = function(event) {
if (event.button == DialogButton.APPLY)
{
if (state.welcomeDialog != null) state.welcomeDialog.hideDialog(DialogButton.APPLY);
}
};
}
}

View file

@ -0,0 +1,31 @@
package funkin.ui.debug.stageeditor.components;
import haxe.ui.containers.dialogs.Dialog;
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/exit-confirm.xml"))
class ExitConfirmDialog extends Dialog
{
var onComplete:Void->Void = null;
override public function new(onComp:Void->Void)
{
super();
onComplete = onComp;
buttons = DialogButton.CANCEL | "{{Proceed}}";
defaultButton = "{{Proceed}}";
destroyOnClose = true;
}
public override function validateDialog(button:DialogButton, fn:Bool->Void)
{
if (button == "{{Proceed}}" && onComplete != null)
{
onComplete();
}
fn(true);
}
}

View file

@ -0,0 +1,105 @@
package funkin.ui.debug.stageeditor.components;
import haxe.ui.containers.dialogs.Dialog;
import funkin.play.stage.StageProp;
import funkin.ui.debug.stageeditor.handlers.AssetDataHandler;
import haxe.ui.components.TextField;
import haxe.ui.components.CheckBox;
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/find-object.xml"))
class FindObjDialog extends Dialog
{
var stageEditorState:StageEditorState;
var assets:Array<StageEditorObject> = [];
var curSelected:Int = 0;
var field:TextField;
var checkWord:CheckBox;
var checkCaps:CheckBox;
override public function new(state:StageEditorState, searchFor:String = "")
{
super();
stageEditorState = state;
nameField.text = searchFor;
this.field = nameField;
this.checkWord = wordCheck;
this.checkCaps = capsCheck;
field.onChange = function(_) updateIndicator();
indicator.hide();
top = 20;
left = FlxG.width - width - 20;
buttons = DialogButton.CANCEL | "{{Find Next}}";
defaultButton = "{{Find Next}}";
}
public function updateIndicator()
{
var prevObjCheck = assets[curSelected];
assets = [];
for (ass in stageEditorState.spriteArray)
{
var name = ass.name;
var checkFor = field.text;
if (!checkCaps.selected)
{
name = name.toLowerCase();
checkFor = checkFor.toLowerCase();
}
if (((name.contains(checkFor) && !checkWord.selected) || (name == checkFor && checkWord.selected)) && ass.visible) assets.push(ass);
}
if (assets.length > 0 && prevObjCheck == null)
{
stageEditorState.selectedSprite = assets[0];
}
if (assets.length > 0)
{
indicator.text = "Selected: " + (assets.indexOf(stageEditorState.selectedSprite) + 1) + " / " + assets.length;
}
else
{
indicator.text = "No Matches Found";
}
if (field.text != "" && field.text != null) indicator.show();
else
indicator.hide();
}
public override function validateDialog(button:DialogButton, fn:Bool->Void)
{
var done = true;
if (button == "{{Find Next}}")
{
done = false;
if (assets.length > 0)
{
curSelected = assets.indexOf(stageEditorState.selectedSprite);
curSelected++;
if (curSelected >= assets.length) curSelected = 0;
stageEditorState.selectedSprite = assets[curSelected];
indicator.text = "Selected: " + (assets.indexOf(stageEditorState.selectedSprite) + 1) + " / " + assets.length;
stageEditorState.camFollow.x = assets[curSelected].getMidpoint().x;
stageEditorState.camFollow.y = assets[curSelected].getMidpoint().y;
}
}
fn(done);
}
}

View file

@ -0,0 +1,70 @@
package funkin.ui.debug.stageeditor.components;
import haxe.ui.containers.dialogs.Dialog;
import lime.utils.Bytes;
import haxe.ui.components.TextField;
import openfl.net.URLLoader;
import openfl.net.URLRequest;
import openfl.events.Event;
import openfl.events.IOErrorEvent;
import openfl.events.ProgressEvent;
import openfl.events.SecurityErrorEvent;
import openfl.utils.ByteArray;
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/load-url.xml"))
class LoadFromUrlDialog extends Dialog
{
var urlField:TextField;
var loader:URLLoader;
override public function new(successCallback:Bytes->Void = null, failCallback:String->Void = null)
{
super();
destroyOnClose = true;
loader = new URLLoader();
loader.dataFormat = BINARY;
urlField.text = "";
loader.addEventListener(Event.COMPLETE, function(event:Event) {
var bytes:Bytes = cast(loader.data, ByteArray);
if (successCallback != null) successCallback(bytes);
trace("loaded the image and did success callback");
@:privateAccess
loader.__removeAllListeners();
hideDialog(DialogButton.CANCEL);
});
loader.addEventListener(IOErrorEvent.IO_ERROR, function(event:IOErrorEvent) {
if (failCallback != null) failCallback(urlField.text);
trace("error with this shit");
});
loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function(event:SecurityErrorEvent) {
if (failCallback != null) failCallback(urlField.text);
trace("error with this shit");
});
buttons = DialogButton.CANCEL | "{{Load}}";
defaultButton = "{{Load}}";
}
override public function validateDialog(button:DialogButton, fn:Bool->Void)
{
if (button == DialogButton.CANCEL)
{
fn(true);
}
else
{
loader.load(new URLRequest(urlField.text));
}
}
}

View file

@ -0,0 +1,90 @@
package funkin.ui.debug.stageeditor.components;
import haxe.ui.containers.dialogs.Dialog;
import haxe.ui.components.Link;
import funkin.ui.debug.stageeditor.handlers.AssetDataHandler;
import funkin.save.Save;
import funkin.util.FileUtil;
import lime.ui.FileDialog;
import flixel.FlxG;
import openfl.display.BitmapData;
import haxe.ui.notifications.NotificationType;
import haxe.ui.notifications.NotificationManager;
import funkin.play.stage.StageProp;
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/new-object.xml"))
class NewObjDialog extends Dialog
{
var stageEditorState:StageEditorState;
var bitmap:BitmapData;
override public function new(state:StageEditorState, img:BitmapData = null)
{
super();
stageEditorState = state;
bitmap = img;
field.onChange = function(_) {
field.removeClasses(["invalid-value", "valid-value"]);
}
buttons = DialogButton.CANCEL | "{{Create}}";
defaultButton = "{{Create}}";
destroyOnClose = true;
}
public override function validateDialog(button:DialogButton, fn:Bool->Void)
{
var done = true;
if (button == "{{Create}}")
{
var objNames = [for (obj in StageEditorState.instance.spriteArray) obj.name];
if (field.text == "" || field.text == null || objNames.contains(field.text))
{
field.swapClass("invalid-value", "valid-value");
done = false;
NotificationManager.instance.addNotification(
{
title: "Problem Creating an Object",
body: objNames.contains(field.text) ? "Object with the Name " + field.text + " already exists!" : "Invalid Object Name!",
type: NotificationType.Error
});
}
else
{
var spr = new StageEditorObject();
if (bitmap != null)
{
var bitToLoad = stageEditorState.addBitmap(bitmap);
spr.loadGraphic(stageEditorState.bitmaps[bitToLoad]);
}
else
spr.loadGraphic(AssetDataHandler.getDefaultGraphic());
spr.name = field.text;
spr.screenCenter();
spr.zIndex = 0;
stageEditorState.selectedSprite = spr;
stageEditorState.createAndPushAction(OBJECT_CREATED);
stageEditorState.add(spr);
stageEditorState.updateArray();
stageEditorState.saved = false;
NotificationManager.instance.addNotification(
{
title: "Object Creating Successful",
body: "Successfully created an Object with the Name " + field.text + "!",
type: NotificationType.Success
});
}
}
fn(done);
}
}

View file

@ -0,0 +1,6 @@
package funkin.ui.debug.stageeditor.components;
import haxe.ui.containers.dialogs.Dialog;
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/user-guide.xml"))
class UserGuideDialog extends Dialog {}

View file

@ -0,0 +1,146 @@
package funkin.ui.debug.stageeditor.components;
import haxe.ui.containers.dialogs.Dialog;
import haxe.ui.containers.dialogs.Dialogs;
import haxe.ui.containers.dialogs.MessageBox.MessageBoxType;
import haxe.ui.components.Link;
import funkin.ui.debug.stageeditor.handlers.StageDataHandler;
import funkin.save.Save;
import funkin.util.FileUtil;
import lime.ui.FileDialog;
import flixel.FlxG;
import funkin.input.Cursor;
import funkin.data.stage.StageData;
import funkin.data.stage.StageRegistry;
import funkin.ui.debug.stageeditor.StageEditorState.StageEditorDialogType;
using funkin.util.tools.FloatTools;
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/dialogs/welcome.xml"))
class WelcomeDialog extends Dialog
{
var stageEditorState:StageEditorState;
override public function new(state:StageEditorState)
{
super();
stageEditorState = state;
buttonNew.onClick = function(_) {
stageEditorState.clearAssets();
stageEditorState.loadDummyData();
stageEditorState.currentFile = "";
killDaDialog();
}
for (file in Save.instance.stageEditorPreviousFiles)
{
trace(file);
if (!FileUtil.doesFileExist(file)) continue; // whats the point of loading something that doesnt exist
var patj = new haxe.io.Path(file);
var fileText = new Link();
fileText.percentWidth = 100;
fileText.text = patj.file + "." + patj.ext;
fileText.onClick = function(_) loadFromFilePath(file);
#if sys
var stat = sys.FileSystem.stat(file);
var sizeInMB = (stat.size / 1000000).round(2);
fileText.tooltip = "Full Name: " + file + "\nLast Modified: " + stat.mtime.toString() + "\nSize: " + sizeInMB + "MB";
#end
contentRecent.addComponent(fileText);
}
boxDrag.onClick = function(_) FileUtil.browseForSaveFile([FileUtil.FILE_FILTER_FNFS], loadFromFilePath, null, null, "Open Stage Data");
var defaultStages = StageRegistry.instance.listBaseGameStageIds();
defaultStages.sort(funkin.util.SortUtil.alphabetically);
for (stage in defaultStages)
{
var baseStage = StageRegistry.instance.parseEntryDataWithMigration(stage, StageRegistry.instance.fetchEntryVersion(stage));
if (baseStage == null) continue;
var link = new Link(); // this is how the legend of zelda started btw
link.percentWidth = 100;
link.text = baseStage.name;
link.onClick = function(_) loadFromPreset(baseStage);
contentPresets.addComponent(link);
}
FlxG.stage.window.onDropFile.add(loadFromFilePath);
}
public function loadFromPreset(data:StageData)
{
if (data == null) return;
if (!stageEditorState.saved)
{
Dialogs.messageBox("This will destroy all of your Unsaved Work.\n\nAre you sure? This cannot be undone.", "Load Stage", MessageBoxType.TYPE_YESNO, true,
function(btn:DialogButton) {
if (btn == DialogButton.YES)
{
stageEditorState.saved = true;
loadFromPreset(data);
}
});
return;
}
stageEditorState.clearAssets();
stageEditorState.currentFile = "";
stageEditorState.loadFromDataRaw(data);
killDaDialog();
}
public function loadFromFilePath(file:String)
{
if (!stageEditorState.saved)
{
Dialogs.messageBox("This will destroy all of your Unsaved Work.\n\nAre you sure? This cannot be undone.", "Load Stage", MessageBoxType.TYPE_YESNO, true,
function(btn:DialogButton) {
if (btn == DialogButton.YES)
{
stageEditorState.saved = true;
loadFromFilePath(file);
}
});
return;
}
var bytes = FileUtil.readBytesFromPath(file);
if (bytes == null)
{
stageEditorState.notifyChange("Problem Loading the Stage", "The Stage File could not be loaded.", true);
return;
}
stageEditorState.clearAssets();
stageEditorState.currentFile = file;
stageEditorState.unpackShitFromZip(bytes);
killDaDialog();
}
function killDaDialog()
{
stageEditorState.updateDialog(StageEditorDialogType.OBJECT);
stageEditorState.updateDialog(StageEditorDialogType.CHARACTER);
stageEditorState.updateDialog(StageEditorDialogType.STAGE);
FlxG.stage.window.onDropFile.remove(loadFromFilePath);
hide();
destroy();
}
}

View file

@ -0,0 +1,208 @@
package funkin.ui.debug.stageeditor.handlers;
import flixel.FlxG;
import openfl.display.BitmapData;
import flixel.FlxSprite;
import flixel.util.FlxColor;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.math.FlxRect;
import openfl.display.BlendMode;
import flixel.math.FlxPoint;
import funkin.data.stage.StageData.StageDataProp;
using StringTools;
/**
* Handles the Stage Props and Datas - being able to convert one to the other.
*/
class AssetDataHandler
{
static var state:StageEditorState;
public static function init(state:StageEditorState)
{
AssetDataHandler.state = state;
}
/**
* Turns an Object into Data.
* @param obj the Object whose data to read.
* @param useBitmaps Whether to Save object's BitmapData directly.
* @return Data of the Object
*/
public static function toData(obj:StageEditorObject, useBitmaps:Bool = false):StageEditorObjectData
{
var outputData:StageEditorObjectData =
{
name: obj.name,
assetPath: "",
position: [obj.x, obj.y],
zIndex: obj.zIndex,
isPixel: !obj.antialiasing,
scale: obj.scale.x == obj.scale.y ? Left(obj.scale.x) : Right([obj.scale.x, obj.scale.y]),
alpha: obj.alpha,
danceEvery: obj.animation.getNameList().length > 0 ? obj.danceEvery : 0,
scroll: [obj.scrollFactor.x, obj.scrollFactor.y],
animations: [for (n => d in obj.animDatas) d],
startingAnimation: obj.startingAnimation,
animType: "sparrow", // automatically making sparrow atlases yeah
angle: obj.angle,
blend: obj.blend == null ? "" : Std.string(obj.blend),
color: obj.color.toWebString(),
xmlData: obj.generateXML()
}
if (useBitmaps)
{
outputData.bitmap = obj.pixels.clone();
return outputData;
}
for (name => bit in state.bitmaps)
{
if (areTheseBitmapsEqual(bit, obj.pixels))
{
outputData.assetPath = name;
return outputData;
}
}
outputData.assetPath = "#FFFFFF";
return outputData;
}
/**
* Modifies an Object based on the Data.
* @param object Object to modify. Set to null to create a new one.
* @param data The Data used for the Object.
*/
public static function fromData(object:StageEditorObject, data:StageEditorObjectData)
{
if (data.bitmap != null)
{
var bitToLoad = state.addBitmap(data.bitmap.clone());
object.loadGraphic(state.bitmaps[bitToLoad]);
}
else
{
if (data.animations != null && data.animations.length > 0) // considering we're unpacking we might as well just do this instead of switch
{
object.frames = flixel.graphics.frames.FlxAtlasFrames.fromSparrow(state.bitmaps[data.assetPath].clone(), data.xmlData);
}
else if (data.assetPath.startsWith("#"))
{
object.loadGraphic(getDefaultGraphic());
object.color = FlxColor.fromString(data.assetPath);
}
else
object.loadGraphic(state.bitmaps[data.assetPath].clone());
}
object.name = data.name;
object.setPosition(data.position[0], data.position[1]);
object.zIndex = data.zIndex;
object.antialiasing = !data.isPixel;
object.alpha = data.alpha;
object.danceEvery = data.danceEvery;
object.scrollFactor.set(data.scroll[0], data.scroll[1]);
object.startingAnimation = data.startingAnimation;
object.angle = data.angle;
object.blend = blendFromString(data.blend);
if (!data.assetPath.startsWith("#")) object.color = FlxColor.fromString(data.color);
// yeah
object.pixelPerfectRender = data.isPixel;
object.pixelPerfectPosition = data.isPixel;
for (anim in data.animations)
{
object.addAnim(anim.name, anim.prefix, anim.offsets ?? [0, 0], anim.frameIndices ?? [], anim.frameRate ?? 24, anim.looped ?? false, anim.flipX ?? false,
anim.flipY ?? false);
}
if (object.animation.getNameList().contains(data.startingAnimation)) object.startingAnimation = data.startingAnimation;
switch (data.scale)
{
case Left(value):
object.scale.set(value, value);
case Right(values):
object.scale.set(values[0], values[1]);
}
object.updateHitbox();
object.playAnim(object.startingAnimation);
flixel.util.FlxTimer.wait(StageEditorState.TIME_BEFORE_ANIM_STOP, function() {
if (object != null && object.animation.curAnim != null) object.animation.stop();
});
return object;
}
/**
* Returns a default BitmapData to be used for all the props.
* @return BitmapData
*/
public static function getDefaultGraphic():BitmapData
{
return new FlxSprite().makeGraphic(1, 1, FlxColor.WHITE).pixels.clone();
}
/**
* Returns OpenFL's BlendMode based on the Name.
* @param blend the BlendMode Name.
* @return BlendMode
*/
public static function blendFromString(blend:String):BlendMode
{
// originally this was a MASSIVE and I do mean MASSIVE switch case, though then I found out that blendmode already has one implemented
@:privateAccess
return BlendMode.fromString(blend.toLowerCase().trim());
}
public static function generateXML(obj:StageEditorObject)
{
// the last check is for if the only frame is the standard graphic frame
if (obj == null || obj.frames.frames.length == 0 || obj.frames.frames[0].name == null) return "";
var xml = [
"<!--This XML File was automatically generated by StageEditorEngine, in order to make Funkin' be able to load it.-->",
'<?xml version="1.0" encoding="UTF-8"?>',
'<TextureAtlas imagePath="${obj.toData(false).assetPath}.png" width="${obj.pixels.width}" height="${obj.pixels.height}">'
].join("\n");
for (daFrame in obj.frames.frames)
{
xml += ' <SubTexture name="${daFrame.name}" x="${daFrame.frame.x}" y="${daFrame.frame.y}" width="${daFrame.frame.width}" height="${daFrame.frame.height}" frameX="${- daFrame.offset.x}" frameY="${- daFrame.offset.y}" frameWidth="${daFrame.sourceSize.x}" frameHeight="${daFrame.sourceSize.y}" flipX="${daFrame.flipX}" flipY="${daFrame.flipY}"/>\n';
}
xml += "</TextureAtlas>";
return xml;
}
// I am aware OpenFL has it's own compare bitmap function, though I find this to be better ngl
static function areTheseBitmapsEqual(bitmap1:BitmapData, bitmap2:BitmapData)
{
if (bitmap1.width != bitmap2.width || bitmap1.height != bitmap2.height) return false;
for (px in 0...bitmap1.width)
{
for (py in 0...bitmap1.height)
{
if (bitmap1.getPixel32(px, py) != bitmap2.getPixel32(px, py)) return false;
}
}
return true;
}
}
typedef StageEditorObjectData =
{
> StageDataProp,
var xmlData:String;
var ?bitmap:BitmapData;
}

View file

@ -0,0 +1,365 @@
package funkin.ui.debug.stageeditor.handlers;
import funkin.ui.debug.stageeditor.handlers.AssetDataHandler;
import haxe.io.Bytes;
import funkin.util.FileUtil;
import openfl.display.BitmapData;
import haxe.Json;
import haxe.zip.Entry;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.BaseCharacter;
import funkin.data.stage.StageData;
import funkin.data.stage.StageData.StageDataCharacter;
import funkin.data.stage.StageRegistry;
import openfl.utils.Assets as OpenFLAssets;
import lime.utils.Assets as LimeAssets;
using StringTools;
class StageDataHandler
{
public static function checkForCharacter(char:BaseCharacter)
return char != null;
public static function packShitToZip(state:StageEditorState)
{
// step 1: data
var endData:StageData = new StageData();
endData.name = state.stageName;
endData.cameraZoom = state.stageZoom;
endData.directory = state.stageFolder;
// step 1 phase 1: object data
var xmlMap:Map<String, String> = [];
for (obj in state.spriteArray)
{
var data = obj.toData(false);
endData.props.push(
{
name: data.name,
assetPath: data.assetPath.startsWith("#") ? data.color : data.assetPath,
position: data.position.copy(),
zIndex: data.zIndex,
isPixel: data.isPixel,
scale: data.scale,
alpha: data.alpha,
danceEvery: data.danceEvery,
scroll: data.scroll.copy(),
animations: data.animations,
startingAnimation: data.startingAnimation,
animType: data.animType,
angle: data.angle,
blend: data.blend,
color: data.assetPath.startsWith("#") ? "#FFFFFF" : data.color
});
if (!xmlMap.exists(data.assetPath) && data.xmlData != "") xmlMap.set(data.assetPath, data.xmlData);
}
// step 1 phase 2: character data
endData.characters.bf.zIndex = state.charGroups[CharacterType.BF].zIndex;
endData.characters.dad.zIndex = state.charGroups[CharacterType.DAD].zIndex;
endData.characters.gf.zIndex = state.charGroups[CharacterType.GF].zIndex;
endData.characters.bf.scale = state.bf.scale.x / state.bf.getBaseScale();
endData.characters.dad.scale = state.dad.scale.x / state.dad.getBaseScale();
endData.characters.gf.scale = state.gf.scale.x / state.gf.getBaseScale();
endData.characters.bf.cameraOffsets = state.charCamOffsets[CharacterType.BF].copy();
endData.characters.gf.cameraOffsets = state.charCamOffsets[CharacterType.GF].copy();
endData.characters.dad.cameraOffsets = state.charCamOffsets[CharacterType.DAD].copy();
endData.characters.bf.position = [
state.bf.feetPosition.x - state.bf.globalOffsets[0],
state.bf.feetPosition.y - state.bf.globalOffsets[1]
];
endData.characters.gf.position = [
state.gf.feetPosition.x - state.gf.globalOffsets[0],
state.gf.feetPosition.y - state.gf.globalOffsets[1]
];
endData.characters.dad.position = [
state.dad.feetPosition.x - state.dad.globalOffsets[0],
state.dad.feetPosition.y - state.dad.globalOffsets[1]
];
// step 2: saving everything to entryList
var entryList = new Array<Entry>();
// step 2 phase 1: images
state.removeUnusedBitmaps();
for (name => img in state.bitmaps)
{
var bytes = img?.image?.encode(PNG);
if (bytes == null) continue;
var entry:Entry =
{
fileName: name + ".png",
fileSize: bytes.length,
fileTime: Date.now(),
compressed: false,
dataSize: bytes.length,
data: bytes,
crc32: null // apparently fileutil.hx does not like crc32, idk why but i dont even know what crc32 is
}
entryList.push(entry);
}
// step 2 phase 2: xmls
for (obj in endData.props)
{
if (!xmlMap.exists(obj.assetPath)) continue; // damn
var bytes = Bytes.ofString(xmlMap[obj.assetPath]);
var entry:Entry =
{
fileName: obj.assetPath + ".xml",
fileSize: bytes.length,
fileTime: Date.now(),
compressed: false,
dataSize: bytes.length,
data: bytes,
crc32: null
}
entryList.push(entry);
}
// step 2 phase 3: the main data
var stageBytes = Bytes.ofString(endData.serialize());
entryList.push(
{
fileName: "yourstagename.json",
fileSize: stageBytes.length,
fileTime: Date.now(),
compressed: false,
dataSize: stageBytes.length,
data: stageBytes,
crc32: null
});
var zipFileBytes = FileUtil.createZIPFromEntries(entryList);
return zipFileBytes;
}
public static function unpackShitFromZip(state:StageEditorState, zip:Bytes)
{
state.clearAssets();
state.bitmaps.clear();
var entries = FileUtil.readZIPFromBytes(zip);
var stageData:StageData = new StageData();
var xmls:Map<String, String> = [];
for (stuff in entries)
{
var ext = stuff.fileName.split(".")[1];
switch (ext)
{
case "png":
var data = BitmapData.fromBytes(stuff.data);
state.bitmaps.set(stuff.fileName.replace(".png", ""), data);
case "xml":
xmls.set(stuff.fileName.replace(".xml", ""), stuff.data.toString());
case "json":
stageData = StageRegistry.instance.parseEntryDataRaw(stuff.data.toString(), stuff.fileName);
}
}
if (stageData == null)
{
// TODO: throw an error, then load a dummy data
loadDummyData(state);
return;
}
// actual data unpacking
state.stageName = stageData.name;
state.stageZoom = stageData.cameraZoom;
state.stageFolder = stageData.directory ?? "shared";
// chars
state.loadCharDatas(stageData);
// objects
for (objData in stageData.props)
{
// make the data and roll with it
var spr = new StageEditorObject();
spr.fromData(
{
name: objData.name ?? "Unnamed",
assetPath: objData.assetPath,
animations: objData.animations.copy(),
scale: objData.scale,
position: objData.position,
alpha: objData.alpha,
angle: objData.angle,
zIndex: objData.zIndex,
danceEvery: objData.danceEvery,
isPixel: objData.isPixel,
scroll: objData.scroll.copy(),
color: objData.color,
blend: objData.blend,
startingAnimation: objData.startingAnimation,
xmlData: xmls[objData.assetPath] ?? ""
});
state.add(spr);
}
state.updateArray();
state.sortAssets();
state.updateMarkerPos();
}
static function loadCharDatas(state:StageEditorState, data:StageData)
{
var chars = state.getCharacters();
for (char in chars)
{
var charData:StageDataCharacter = null;
switch (char.characterType)
{
case CharacterType.BF:
charData = data.characters.bf;
case CharacterType.GF:
charData = data.characters.gf;
case CharacterType.DAD:
charData = data.characters.dad;
default: // nothing rip
}
char.resetCharacter(true);
if (charData == null) continue;
char.x = charData.position[0] - char.characterOrigin.x + char.globalOffsets[0];
char.y = charData.position[1] - char.characterOrigin.y + char.globalOffsets[1];
state.charGroups[char.characterType].zIndex = charData.zIndex;
char.setScale(char.getBaseScale() * charData.scale);
char.cameraFocusPoint.x += charData.cameraOffsets[0];
char.cameraFocusPoint.y += charData.cameraOffsets[1];
state.charCamOffsets[char.characterType] = charData.cameraOffsets.copy();
}
}
public static function loadFromDataRaw(state:StageEditorState, data:StageData)
{
state.clearAssets();
state.bitmaps.clear();
if (data == null)
{
loadDummyData(state);
return;
}
@:privateAccess
if (!LimeAssets.libraryPaths.exists(data.directory))
{
loadDummyData(state);
return;
}
Paths.setCurrentLevel(data.directory);
if (OpenFLAssets.getLibrary(data.directory) == null)
{
OpenFLAssets.loadLibrary(data.directory).onComplete(function(_) {
loadFromDataRaw(state, data);
});
return;
}
state.stageName = data.name;
state.stageZoom = data.cameraZoom;
state.stageFolder = data.directory ?? "shared";
state.loadCharDatas(data);
for (objData in data.props)
{
var spr = new StageEditorObject();
if (!objData.assetPath.startsWith("#")) state.bitmaps.set(objData.assetPath, Assets.getBitmapData(Paths.image(objData.assetPath)));
spr.fromData(
{
name: objData.name ?? "Unnamed",
assetPath: objData.assetPath,
animations: objData.animations.copy(),
scale: objData.scale,
position: objData.position,
alpha: objData.alpha,
angle: objData.angle,
zIndex: objData.zIndex,
danceEvery: objData.danceEvery,
isPixel: objData.isPixel,
scroll: objData.scroll.copy(),
color: objData.color,
blend: objData.blend,
startingAnimation: objData.startingAnimation,
xmlData: Assets.exists(Paths.file("images/" + objData.assetPath + ".xml")) ? Assets.getText(Paths.file("images/" + objData.assetPath + ".xml")) : ""
});
state.add(spr);
}
state.updateArray();
state.sortAssets();
state.updateMarkerPos();
}
public static function loadDummyData(state:StageEditorState)
{
state.clearAssets();
state.stageName = "Unnamed";
state.stageZoom = 1.0;
state.stageFolder = "shared";
state.charCamOffsets = StageEditorState.DEFAULT_CAMERA_OFFSETS.copy();
state.charPos = StageEditorState.DEFAULT_POSITIONS.copy();
state.gf.resetCharacter(true);
state.dad.resetCharacter(true);
state.bf.resetCharacter(true);
state.charGroups[CharacterType.BF].zIndex = 300;
state.charGroups[CharacterType.DAD].zIndex = 200;
state.charGroups[CharacterType.GF].zIndex = 100;
state.gf.x = state.charPos[CharacterType.GF][0] - state.gf.characterOrigin.x + state.gf.globalOffsets[0];
state.gf.y = state.charPos[CharacterType.GF][1] - state.gf.characterOrigin.y + state.gf.globalOffsets[1];
state.dad.x = state.charPos[CharacterType.DAD][0] - state.dad.characterOrigin.x + state.dad.globalOffsets[0];
state.dad.y = state.charPos[CharacterType.DAD][1] - state.dad.characterOrigin.y + state.dad.globalOffsets[1];
state.bf.x = state.charPos[CharacterType.BF][0] - state.bf.characterOrigin.x + state.bf.globalOffsets[0];
state.bf.y = state.charPos[CharacterType.BF][1] - state.bf.characterOrigin.y + state.bf.globalOffsets[1];
state.gf.setScale(state.gf.getBaseScale());
state.dad.setScale(state.dad.getBaseScale());
state.bf.setScale(state.bf.getBaseScale());
state.gf.cameraFocusPoint.x += state.charCamOffsets[CharacterType.GF][0];
state.gf.cameraFocusPoint.y += state.charCamOffsets[CharacterType.GF][1];
state.dad.cameraFocusPoint.x += state.charCamOffsets[CharacterType.DAD][0];
state.dad.cameraFocusPoint.y += state.charCamOffsets[CharacterType.DAD][1];
state.bf.cameraFocusPoint.x += state.charCamOffsets[CharacterType.BF][0];
state.bf.cameraFocusPoint.y += state.charCamOffsets[CharacterType.BF][1];
// no props :p
state.updateMarkerPos();
}
}

View file

@ -0,0 +1,191 @@
package funkin.ui.debug.stageeditor.handlers;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.ui.debug.stageeditor.handlers.AssetDataHandler.StageEditorObjectData;
import funkin.ui.debug.stageeditor.StageEditorState.StageEditorDialogType;
class UndoRedoHandler
{
public static function performLastAction(state:StageEditorState, redo:Bool = false)
{
if (state == null || (state.undoArray.length <= 0 && !redo) || (state.redoArray.length <= 0 && redo)) return;
var actionToDo = redo ? state.redoArray.pop() : state.undoArray.pop();
switch (actionToDo.type)
{
case CHARACTER_MOVED:
createAndPushAction(state, actionToDo.type, !redo);
var type = actionToDo.data.type == null ? CharacterType.BF : actionToDo.data.type;
var pos = actionToDo.data.pos == null ? [0, 0] : actionToDo.data.pos;
for (char in state.getCharacters())
{
if (char.characterType == type) state.selectedChar = char;
}
state.selectedChar.x = pos[0] - state.selectedChar.characterOrigin.x + state.selectedChar.globalOffsets[0];
state.selectedChar.y = pos[1] - state.selectedChar.characterOrigin.y + state.selectedChar.globalOffsets[1];
state.updateMarkerPos();
state.updateDialog(StageEditorDialogType.CHARACTER);
case OBJECT_MOVED:
var id = actionToDo.data.ID ?? -1;
var pos = actionToDo.data.pos ?? [0, 0];
for (obj in state.spriteArray)
{
if (obj.ID == id) state.selectedSprite = obj;
}
if (state.selectedSprite != null)
{
createAndPushAction(state, actionToDo.type, !redo);
state.selectedSprite.x = pos[0];
state.selectedSprite.y = pos[1];
state.updateDialog(StageEditorDialogType.OBJECT);
}
case OBJECT_CREATED: // this removes the object
var id = actionToDo.data.ID ?? -1;
for (obj in state.spriteArray)
{
if (obj.ID == id)
{
state.selectedSprite = obj;
createAndPushAction(state, OBJECT_DELETED, !redo);
state.selectedSprite = null;
obj.kill();
state.remove(obj, true);
obj.destroy();
state.updateArray();
state.updateDialog(StageEditorDialogType.OBJECT);
trace("found object");
continue;
}
}
case OBJECT_DELETED: // this creates the object
if (actionToDo.data.data == null) return;
var id = actionToDo.data.ID ?? -1;
var data:StageEditorObjectData = cast actionToDo.data.data;
var obj = new StageEditorObject().fromData(data);
obj.ID = id;
state.selectedSprite = obj;
createAndPushAction(state, OBJECT_CREATED, !redo);
state.add(obj);
state.updateDialog(StageEditorDialogType.OBJECT);
state.updateArray();
case OBJECT_ROTATED: // primarily copied from OBJECT_MOVED
var id = actionToDo.data.ID ?? -1;
var angle = actionToDo.data.angle ?? 0;
for (obj in state.spriteArray)
{
if (obj.ID == id) state.selectedSprite = obj;
}
if (state.selectedSprite != null)
{
createAndPushAction(state, actionToDo.type, !redo);
state.selectedSprite.angle = angle;
state.updateDialog(StageEditorDialogType.OBJECT);
}
default: // do nothing dumbass
}
}
public static function createAndPushAction(state:StageEditorState, action:UndoActionType, redo:Bool = false)
{
if (state == null) return;
var finalAction:UndoAction = {type: action, data: null};
if (!redo && state.redoArray.length > 0) state.redoArray = []; // incorporate resetting as well
switch (action)
{
case CHARACTER_MOVED:
var char = state.selectedChar.characterType;
finalAction.data = {type: char, pos: state.charPos[char].copy()};
case OBJECT_MOVED:
finalAction.data = {ID: state.selectedSprite.ID, pos: [state.selectedSprite.x, state.selectedSprite.y]}
case OBJECT_CREATED:
finalAction.data = {ID: state.selectedSprite.ID}
case OBJECT_DELETED:
finalAction.data =
{
ID: state.selectedSprite.ID,
data: state.selectedSprite.toData(true)
}
case OBJECT_ROTATED:
finalAction.data = {ID: state.selectedSprite.ID, angle: state.selectedSprite.angle}
default: // nop
}
if (finalAction.data == null) return;
if (redo) state.redoArray.push(finalAction);
else if (!redo) state.undoArray.push(finalAction);
}
}
typedef UndoAction =
{
/**
* The Type of Undo Action to store.
*/
var type:UndoActionType;
/**
* The added Data of the Action.
*/
var data:Dynamic;
}
enum abstract UndoActionType(String) from String
{
/**
* Triggerred when an Object is deleted.
*/
var OBJECT_DELETED = "object_deleted";
/**
* Triggerred when an Object is created.
*/
var OBJECT_CREATED = "object_created";
/**
* Triggerred when an Object is moved.
*/
var OBJECT_MOVED = "object_moved";
/**
* Triggerred when a Character is moved.
*/
var CHARACTER_MOVED = "character_moved";
/**
* Triggerred when an Object is rotated.
*/
var OBJECT_ROTATED = "object_rotated";
}

View file

@ -0,0 +1,7 @@
package funkin.ui.debug.stageeditor;
#if !macro
using funkin.ui.debug.stageeditor.handlers.StageDataHandler;
using funkin.ui.debug.stageeditor.handlers.AssetDataHandler;
using funkin.ui.debug.stageeditor.handlers.UndoRedoHandler;
#end

View file

@ -0,0 +1,242 @@
package funkin.ui.debug.stageeditor.toolboxes;
import haxe.ui.components.NumberStepper;
import funkin.play.character.BaseCharacter;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.util.SortUtil;
import haxe.ui.data.ArrayDataSource;
import haxe.ui.components.DropDown;
import haxe.ui.components.Button;
import haxe.ui.components.Slider;
import haxe.ui.components.Label;
import funkin.ui.debug.stageeditor.handlers.StageDataHandler;
import haxe.ui.containers.menus.Menu;
import haxe.ui.containers.ScrollView;
import haxe.ui.core.Screen;
import flixel.tweens.FlxTween;
import flixel.tweens.FlxEase;
import haxe.ui.containers.Grid;
import funkin.play.character.CharacterData;
using StringTools;
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/character-properties.xml"))
class StageEditorCharacterToolbox extends StageEditorDefaultToolbox
{
var characterPosXStepper:NumberStepper;
var characterPosYStepper:NumberStepper;
var characterPosReset:Button;
var characterZIdxStepper:NumberStepper;
var characterZIdxReset:Button;
var characterCamXStepper:NumberStepper;
var characterCamYStepper:NumberStepper;
var characterCamReset:Button;
var characterScaleSlider:Slider;
var characterScaleReset:Button;
var characterTypeButton:Button;
var charMenu:StageEditorCharacterMenu;
override public function new(state:StageEditorState)
{
super(state);
// position
characterPosXStepper.onChange = characterPosYStepper.onChange = function(_) {
repositionCharacter();
state.saved = false;
}
characterPosReset.onClick = function(_) {
if (!StageEditorState.DEFAULT_POSITIONS.exists(state.selectedChar.characterType)) return;
var oldPositions = StageEditorState.DEFAULT_POSITIONS[state.selectedChar.characterType];
characterPosXStepper.pos = oldPositions[0];
characterPosYStepper.pos = oldPositions[1];
}
// zidx
characterZIdxStepper.max = StageEditorState.MAX_Z_INDEX;
characterZIdxStepper.onChange = function(_) {
state.charGroups[state.selectedChar.characterType].zIndex = Std.int(characterZIdxStepper.pos);
state.saved = false;
state.sortAssets();
}
characterZIdxReset.onClick = function(_) {
var thingies = [CharacterType.GF, CharacterType.DAD, CharacterType.BF];
var thingIdxies = thingies.indexOf(state.selectedChar.characterType);
characterZIdxStepper.pos = (thingIdxies * 100);
}
// camera
characterCamXStepper.onChange = characterCamYStepper.onChange = function(_) {
state.charCamOffsets[state.selectedChar.characterType] = [characterCamXStepper.pos, characterCamYStepper.pos];
state.updateMarkerPos();
state.saved = false;
}
characterCamReset.onClick = function(_) characterCamXStepper.pos = characterCamYStepper.pos = 0; // lol
// scale
characterScaleSlider.onChange = function(_) {
state.selectedChar.setScale(state.selectedChar.getBaseScale() * characterScaleSlider.pos);
repositionCharacter();
state.saved = false;
}
characterScaleReset.onChange = function(_) characterScaleSlider.pos = 1;
// character button
characterTypeButton.onClick = function(_) {
charMenu = new StageEditorCharacterMenu(state, this);
Screen.instance.addComponent(charMenu);
}
refresh();
}
override public function refresh()
{
var name = stageEditorState.selectedChar.characterType;
characterPosXStepper.step = characterPosYStepper.step = stageEditorState.moveStep;
characterCamXStepper.step = characterCamYStepper.step = stageEditorState.moveStep;
if (characterPosXStepper.pos != stageEditorState.charPos[name][0]) characterPosXStepper.pos = stageEditorState.charPos[name][0];
if (characterPosYStepper.pos != stageEditorState.charPos[name][1]) characterPosYStepper.pos = stageEditorState.charPos[name][1];
if (characterZIdxStepper.pos != stageEditorState.charGroups[stageEditorState.selectedChar.characterType].zIndex)
characterZIdxStepper.pos = stageEditorState.charGroups[stageEditorState.selectedChar.characterType].zIndex;
if (characterCamXStepper.pos != stageEditorState.charCamOffsets[name][0]) characterCamXStepper.pos = stageEditorState.charCamOffsets[name][0];
if (characterCamYStepper.pos != stageEditorState.charCamOffsets[name][1]) characterCamYStepper.pos = stageEditorState.charCamOffsets[name][1];
if (characterScaleSlider.pos != stageEditorState.selectedChar.scale.x / stageEditorState.selectedChar.getBaseScale())
characterScaleSlider.pos = stageEditorState.selectedChar.scale.x / stageEditorState.selectedChar.getBaseScale();
var prevText = characterTypeButton.text;
var charData = CharacterDataParser.fetchCharacterData(stageEditorState.selectedChar.characterId);
characterTypeButton.icon = (charData == null ? null : CharacterDataParser.getCharPixelIconAsset(stageEditorState.selectedChar.characterId));
characterTypeButton.text = (charData == null ? "None" : charData.name.length > 6 ? '${charData.name.substr(0, 6)}.' : '${charData.name}');
if (prevText != characterTypeButton.text)
{
Screen.instance.removeComponent(charMenu);
}
}
public function repositionCharacter()
{
stageEditorState.selectedChar.x = characterPosXStepper.pos - stageEditorState.selectedChar.characterOrigin.x
+ stageEditorState.selectedChar.globalOffsets[0];
stageEditorState.selectedChar.y = characterPosYStepper.pos - stageEditorState.selectedChar.characterOrigin.y
+ stageEditorState.selectedChar.globalOffsets[1];
stageEditorState.selectedChar.setScale(stageEditorState.selectedChar.getBaseScale() * characterScaleSlider.pos);
stageEditorState.updateMarkerPos();
}
}
@:xml('
<menu id="iconSelector" width="410" height="185" padding="8">
<vbox width="100%" height="100%">
<scrollview id="charSelectScroll" width="390" height="150" contentWidth="100%" />
<label id="charIconName" text="(choose a character)" />
</vbox>
</menu>
')
class StageEditorCharacterMenu extends Menu // copied from chart editor
{
override public function new(state:StageEditorState, parent:StageEditorCharacterToolbox)
{
super();
this.x = Screen.instance.currentMouseX;
this.y = Screen.instance.currentMouseY;
var charGrid = new Grid();
charGrid.columns = 5;
charGrid.width = this.width;
charSelectScroll.addComponent(charGrid);
var charIds = CharacterDataParser.listCharacterIds();
charIds.sort(SortUtil.alphabetically);
var defaultText:String = '(choose a character)';
for (charIndex => charId in charIds)
{
var charData:CharacterData = CharacterDataParser.fetchCharacterData(charId);
var charButton = new haxe.ui.components.Button();
charButton.width = 70;
charButton.height = 70;
charButton.padding = 8;
charButton.iconPosition = "top";
if (charId == state.selectedChar.characterId)
{
// Scroll to the character if it is already selected.
charSelectScroll.hscrollPos = Math.floor(charIndex / 5) * 80;
charButton.selected = true;
defaultText = '${charData.name} [${charId}]';
}
var LIMIT = 6;
charButton.icon = CharacterDataParser.getCharPixelIconAsset(charId);
charButton.text = charData.name.length > LIMIT ? '${charData.name.substr(0, LIMIT)}.' : '${charData.name}';
charButton.onClick = _ -> {
var type = state.selectedChar.characterType;
if (state.selectedChar.characterId == charId) return; // saves on memory
var group = state.charGroups[type];
group.killMembers();
for (member in group.members)
{
member.kill();
group.remove(member, true);
member.destroy();
}
group.clear();
// okay i think that was enough cleaning phew you can see how clean this group is now!!!
// anyways new character!!!!
var newChar = CharacterDataParser.fetchCharacter(charId, true);
newChar.characterType = type;
newChar.resetCharacter(true);
newChar.flipX = type == CharacterType.BF ? !newChar.getDataFlipX() : newChar.getDataFlipX();
state.selectedChar = newChar;
group.add(newChar);
parent.repositionCharacter();
};
charButton.onMouseOver = _ -> {
charIconName.text = '${charData.name} [${charId}]';
};
charButton.onMouseOut = _ -> {
charIconName.text = defaultText;
};
charGrid.addComponent(charButton);
}
charIconName.text = defaultText;
this.alpha = 0;
this.y -= 10;
FlxTween.tween(this, {alpha: 1, y: this.y + 10}, 0.2, {ease: FlxEase.quartOut});
}
}

View file

@ -0,0 +1,44 @@
package funkin.ui.debug.stageeditor.toolboxes;
import haxe.ui.containers.dialogs.CollapsibleDialog;
import funkin.audio.FunkinSound;
@:access(funkin.ui.debug.stageeditor.StageEditorState)
class StageEditorDefaultToolbox extends CollapsibleDialog
{
var stageEditorState:StageEditorState;
public var dialogVisible:Bool = false;
private function new(stageEditorState:StageEditorState)
{
super();
this.stageEditorState = stageEditorState;
closable = false;
modal = true;
destroyOnClose = false;
}
/**
* Handles the Sound and Visibility
* @param on
*/
public function toggle(on:Bool)
{
if (!dialogVisible && on) FunkinSound.playOnce(Paths.sound('chartingSounds/openWindow'));
else if (dialogVisible && !on) FunkinSound.playOnce(Paths.sound('chartingSounds/exitWindow'));
if (on) showDialog(false);
else
hide();
dialogVisible = on;
}
/**
* Override to implement this.
*/
public function refresh() {}
}

View file

@ -0,0 +1,581 @@
package funkin.ui.debug.stageeditor.toolboxes;
import haxe.ui.components.HorizontalSlider;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.TextField;
import haxe.ui.components.TextArea;
import haxe.ui.components.Button;
import haxe.ui.components.Image;
import haxe.ui.containers.dialogs.Dialogs.FileDialogTypes;
import haxe.ui.ToolkitAssets;
import haxe.ui.containers.dialogs.Dialogs;
import funkin.ui.debug.stageeditor.handlers.AssetDataHandler;
import flixel.graphics.frames.FlxAtlasFrames;
import haxe.ui.components.DropDown;
import haxe.ui.containers.ListView;
import haxe.ui.components.CheckBox;
import haxe.ui.components.Switch;
import flixel.util.FlxTimer;
import haxe.ui.data.ArrayDataSource;
import haxe.ui.events.ItemEvent;
import haxe.ui.components.ColorPicker;
import flixel.util.FlxColor;
import haxe.ui.util.Color;
import flixel.graphics.frames.FlxFrame;
import flixel.animation.FlxAnimation;
import funkin.util.FileUtil;
import funkin.ui.debug.stageeditor.components.LoadFromUrlDialog;
import openfl.display.BitmapData;
using StringTools;
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/object-properties.xml"))
class StageEditorObjectToolbox extends StageEditorDefaultToolbox
{
var linkedObject:StageEditorObject = null;
var objectImagePreview:Image;
var objectLoadImageButton:Button;
var objectLoadInternetButton:Button;
var objectDownloadImageButton:Button;
var objectResetImageButton:Button;
var objectZIdxStepper:NumberStepper;
var objectZIdxReset:Button;
var objectPosXStepper:NumberStepper;
var objectPosYStepper:NumberStepper;
var objectPosResetButton:Button;
var objectAlphaSlider:HorizontalSlider;
var objectAlphaResetButton:Button;
var objectAngleSlider:HorizontalSlider;
var objectAngleResetButton:Button;
var objectScaleXStepper:NumberStepper;
var objectScaleYStepper:NumberStepper;
var objectScaleResetButton:Button;
var objectSizeXStepper:NumberStepper;
var objectSizeYStepper:NumberStepper;
var objectSizeResetButton:Button;
var objectScrollXSlider:HorizontalSlider;
var objectScrollYSlider:HorizontalSlider;
var objectScrollResetButton:Button;
var objectFrameText:TextArea;
var objectFrameTextLoad:Button;
var objectFrameTextSparrow:Button;
var objectFrameTextPacker:Button;
var objectFrameImageWidth:NumberStepper;
var objectFrameImageHeight:NumberStepper;
var objectFrameImageSetter:Button;
var objectFrameReset:Button;
var objectAnimDropdown:DropDown;
var objectAnimName:TextField;
var objectAnimFrameList:ListView;
var objectAnimPrefix:TextField;
var objectAnimFrames:TextField;
var objectAnimLooped:CheckBox;
var objectAnimFlipX:CheckBox;
var objectAnimFlipY:CheckBox;
var objectAnimFramerate:NumberStepper;
var objectAnimOffsetX:NumberStepper;
var objectAnimOffsetY:NumberStepper;
var objectAnimDanceBeat:NumberStepper;
var objectAnimDanceBeatReset:Button;
var objectAnimStart:TextField;
var objectAnimStartReset:Button;
var objectMiscAntialias:CheckBox;
var objectMiscAntialiasReset:Button;
var objectMiscFlipReset:Button;
var objectMiscBlendDrop:DropDown;
var objectMiscBlendReset:Button;
var objectMiscColor:ColorPicker;
var objectMiscColorReset:Button;
override public function new(state:StageEditorState)
{
super(state);
// basic callbacks
objectLoadImageButton.onClick = function(_) {
if (linkedObject == null) return;
Dialogs.openBinaryFile("Open Image File", FileDialogTypes.IMAGES, function(selectedFile) {
if (selectedFile == null) return;
objectImagePreview.resource = null;
ToolkitAssets.instance.imageFromBytes(selectedFile.bytes, function(imageInfo) {
if (imageInfo == null) return;
objectImagePreview.resource = imageInfo.data;
linkedObject.frame = imageInfo.data;
var bit = linkedObject.updateFramePixels();
var bitToLoad = state.addBitmap(bit);
linkedObject.loadGraphic(state.bitmaps[bitToLoad]);
linkedObject.updateHitbox();
// update size stuff
objectSizeXStepper.pos = linkedObject.width;
objectSizeYStepper.pos = linkedObject.height;
// remove unused bitmaps
state.removeUnusedBitmaps();
});
});
}
objectLoadInternetButton.onClick = function(_) {
if (linkedObject == null) return;
state.createURLDialog(function(bytes:lime.utils.Bytes) {
linkedObject.loadGraphic(BitmapData.fromBytes(bytes));
linkedObject.updateHitbox();
refresh();
});
}
objectDownloadImageButton.onClick = function(_) {
if (linkedObject == null) return;
FileUtil.saveFile(linkedObject.pixels.image.encode(PNG), [FileUtil.FILE_FILTER_PNG], null, null,
linkedObject.name + "-graphic.png"); // i'on need any callbacks
}
objectZIdxStepper.max = StageEditorState.MAX_Z_INDEX;
objectZIdxStepper.onChange = function(_) {
if (linkedObject != null)
{
linkedObject.zIndex = Std.int(objectZIdxStepper.pos);
state.sortAssets();
}
}
// numeric callbacks
objectPosXStepper.onChange = function(_) {
if (linkedObject != null) linkedObject.x = objectPosXStepper.pos;
};
objectPosYStepper.onChange = function(_) {
if (linkedObject != null) linkedObject.y = objectPosYStepper.pos;
};
objectAlphaSlider.onChange = function(_) {
if (linkedObject != null) linkedObject.alpha = objectAlphaSlider.pos;
};
objectAngleSlider.onChange = function(_) {
if (linkedObject != null) linkedObject.angle = objectAngleSlider.pos;
};
objectScaleXStepper.onChange = objectScaleYStepper.onChange = function(_) {
if (linkedObject != null)
{
linkedObject.scale.set(objectScaleXStepper.pos, objectScaleYStepper.pos);
linkedObject.updateHitbox();
objectSizeXStepper.pos = linkedObject.width;
objectSizeYStepper.pos = linkedObject.height;
linkedObject.playAnim(linkedObject.animation.name); // load offsets
}
};
objectSizeXStepper.onChange = objectSizeYStepper.onChange = function(_) {
if (linkedObject != null)
{
linkedObject.setGraphicSize(Std.int(objectSizeXStepper.pos), Std.int(objectSizeYStepper.pos));
linkedObject.updateHitbox();
objectScaleXStepper.pos = linkedObject.scale.x;
objectScaleYStepper.pos = linkedObject.scale.y;
linkedObject.playAnim(linkedObject.animation.name); // load offsets
}
};
objectScrollXSlider.onChange = objectScrollYSlider.onChange = function(_) {
if (linkedObject != null) linkedObject.scrollFactor.set(objectScrollXSlider.pos, objectScrollYSlider.pos);
};
// frame callbacks
objectFrameTextLoad.onClick = function(_) {
Dialogs.openTextFile("Open Text File", FileDialogTypes.TEXTS, function(selectedFile) {
if (selectedFile.text == null || (!selectedFile.name.endsWith(".xml") && !selectedFile.name.endsWith(".txt"))) return;
objectFrameText.text = selectedFile.text;
state.notifyChange("Frame Text Loaded", "The Text File " + selectedFile.name + " has been loaded.");
});
}
objectFrameTextSparrow.onClick = function(_) {
if (linkedObject == null || objectFrameText.text == null || objectFrameText.text == "") return;
try
{
linkedObject.frames = FlxAtlasFrames.fromSparrow(linkedObject.graphic, objectFrameText.text);
}
catch (e)
{
state.notifyChange("Frame Setup Error", e.toString(), true);
return;
}
// might as well clear animations because frames SUCK
linkedObject.animDatas.clear();
linkedObject.animation.destroyAnimations();
linkedObject.updateHitbox();
refresh();
state.notifyChange("Frame Setup Done", "Finished the Sparrow Frame Setup for the Object " + linkedObject.name + ".");
}
objectFrameTextPacker.onClick = function(_) {
if (linkedObject == null || objectFrameText.text == null || objectFrameText.text == "") return;
try // crash prevention
{
linkedObject.frames = FlxAtlasFrames.fromSpriteSheetPacker(linkedObject.graphic, objectFrameText.text);
}
catch (e)
{
state.notifyChange("Frame Setup Error", e.toString(), true);
return;
}
// might as well clear animations because frames SUCK
linkedObject.animDatas.clear();
linkedObject.animation.destroyAnimations();
linkedObject.updateHitbox();
refresh();
state.notifyChange("Frame Setup Done", "Finished the Packer Frame Setup for the Object " + linkedObject.name + ".");
}
objectFrameImageSetter.onClick = function(_) {
if (linkedObject == null) return;
linkedObject.loadGraphic(linkedObject.graphic, true, Std.int(objectFrameImageWidth.pos), Std.int(objectFrameImageHeight.pos));
linkedObject.updateHitbox();
// set da names
for (i in 0...linkedObject.frames.frames.length)
{
linkedObject.frames.framesHash.set("Frame" + i, linkedObject.frames.frames[i]);
linkedObject.frames.frames[i].name = "Frame" + i;
}
// might as well clear animations because frames SUCK
linkedObject.animDatas.clear();
linkedObject.animation.destroyAnimations();
refresh();
state.notifyChange("Frame Setup Done", "Finished the Image Frame Setup for the Object " + linkedObject.name + ".");
}
// animation
objectAnimDropdown.onChange = function(_) {
if (linkedObject == null) return;
if (objectAnimDropdown.selectedIndex == -1) // RESET EVERYTHING INSTANTENEOUSLY
{
objectAnimName.text = "";
objectAnimLooped.selected = objectAnimFlipX.selected = objectAnimFlipY.selected = false;
objectAnimFramerate.pos = 24;
objectAnimOffsetX.pos = objectAnimOffsetY.pos = 0;
objectAnimFrames.text = "";
return;
}
var animData = linkedObject.animDatas[objectAnimDropdown.selectedItem.text];
if (animData == null) return;
objectAnimName.text = objectAnimDropdown.selectedItem.text;
objectAnimPrefix.text = animData.prefix ?? "";
objectAnimFrames.text = (animData.frameIndices != null && animData.frameIndices.length > 0 ? animData.frameIndices.join(", ") : "");
objectAnimLooped.selected = animData.looped ?? false;
objectAnimFlipX.selected = animData.flipX ?? false;
objectAnimFlipY.selected = animData.flipY ?? false;
objectAnimFramerate.pos = animData.frameRate ?? 24;
objectAnimOffsetX.pos = (animData.offsets != null && animData.offsets.length == 2 ? animData.offsets[0] : 0);
objectAnimOffsetY.pos = (animData.offsets != null && animData.offsets.length == 2 ? animData.offsets[1] : 0);
}
objectAnimSave.onClick = function(_) {
if (linkedObject == null) return;
if (objectAnimName.text == null || objectAnimName.text == "")
{
state.notifyChange("Animation Saving Error", "Invalid Animation Name!", true);
return;
}
if (objectAnimPrefix.text == null || objectAnimPrefix.text == "")
{
state.notifyChange("Animation Saving Error", "Missing Animation Prefix!", true);
return;
}
if (linkedObject.animation.getNameList().contains(objectAnimName.text)) linkedObject.animation.remove(objectAnimName.text);
var indices = [];
if (objectAnimFrames.text != null && objectAnimFrames.text != "")
{
var splitter = objectAnimFrames.text.replace(" ", "").split(",");
for (num in splitter)
{
indices.push(Std.parseInt(num));
}
}
var shouldDoIndices:Bool = (indices.length > 0 && !indices.contains(null));
linkedObject.addAnim(objectAnimName.text, objectAnimPrefix.text, [objectAnimOffsetX.pos, objectAnimOffsetY.pos], (shouldDoIndices ? indices : []),
Std.int(objectAnimFramerate.pos), objectAnimLooped.selected, objectAnimFlipX.selected, objectAnimFlipY.selected);
if (linkedObject.animation.getByName(objectAnimName.text) == null)
{
state.notifyChange("Animation Saving Error", "Invalid Frames!", true);
return;
}
linkedObject.playAnim(objectAnimName.text);
state.notifyChange("Animation Saving Done", "Animation " + objectAnimName.text + " has been saved to the Object " + linkedObject.name + ".");
updateAnimList();
// stops the animation preview if animation is looped for too long
FlxTimer.wait(StageEditorState.TIME_BEFORE_ANIM_STOP, function() {
if (linkedObject != null && linkedObject.animation.curAnim != null)
linkedObject.animation.stop(); // null check cuz if we stop an anim for a null object the game crashes :[
});
}
objectAnimDelete.onClick = function(_) {
if (linkedObject == null || linkedObject.animation.getNameList().length <= 0 || objectAnimDropdown.selectedIndex < 0) return;
linkedObject.animation.pause();
linkedObject.animation.stop();
linkedObject.animation.curAnim = null;
var daAnim = linkedObject.animation.getNameList()[objectAnimDropdown.selectedIndex];
linkedObject.animation.remove(daAnim);
linkedObject.animDatas.remove(daAnim);
linkedObject.offset.set();
state.notifyChange("Animation Deletion Done",
"Animation "
+ objectAnimDropdown.selectedItem.text
+ " has been removed from the Object "
+ linkedObject.name
+ ".");
updateAnimList();
objectAnimDropdown.selectedIndex = objectAnimDropdown.dataSource.size - 1;
}
objectAnimDanceBeat.onChange = function(_) {
if (linkedObject != null) linkedObject.danceEvery = Std.int(objectAnimDanceBeat.pos);
}
objectAnimStart.onChange = function(_) {
if (linkedObject != null)
{
if (linkedObject.animation.getNameList().contains(objectAnimStart.text)) objectAnimStart.styleString = "color: white";
else
objectAnimStart.styleString = "color: indianred";
linkedObject.startingAnimation = objectAnimStart.text;
}
}
// misc
objectMiscAntialias.onClick = function(_) {
if (linkedObject != null) linkedObject.antialiasing = objectMiscAntialias.selected;
}
objectMiscBlendDrop.onChange = function(_) {
if (linkedObject != null)
linkedObject.blend = objectMiscBlendDrop.selectedItem.text == "NONE" ? null : AssetDataHandler.blendFromString(objectMiscBlendDrop.selectedItem.text);
}
objectMiscColor.onChange = function(_) {
if (linkedObject != null) linkedObject.color = FlxColor.fromRGB(objectMiscColor.currentColor.r, objectMiscColor.currentColor.g,
objectMiscColor.currentColor.b);
}
// reset button callbacks
objectResetImageButton.onClick = function(_) {
if (linkedObject != null)
{
linkedObject.loadGraphic(AssetDataHandler.getDefaultGraphic());
linkedObject.updateHitbox();
refresh();
// remove unused bitmaps
state.removeUnusedBitmaps();
}
}
objectZIdxReset.onClick = function(_) {
if (linkedObject != null) objectZIdxStepper.pos = 0; // corner cutting because onChange will activate with this
}
objectPosResetButton.onClick = function(_) {
if (linkedObject != null)
{
linkedObject.screenCenter();
objectPosXStepper.pos = linkedObject.x;
objectPosYStepper.pos = linkedObject.y;
}
}
objectAlphaResetButton.onClick = function(_) {
if (linkedObject != null) linkedObject.alpha = objectAlphaSlider.pos = 1;
}
objectAngleResetButton.onClick = function(_) {
if (linkedObject != null) linkedObject.angle = objectAngleSlider.pos = 0;
}
objectScaleResetButton.onClick = objectSizeResetButton.onClick = function(_) // the corner cutting goes crazy
{
if (linkedObject != null)
{
linkedObject.scale.set(1, 1);
refresh(); // refreshes like multiple shit
}
}
objectScrollResetButton.onClick = function(_) {
if (linkedObject != null) linkedObject.scrollFactor.x = linkedObject.scrollFactor.y = objectScrollXSlider.pos = objectScrollYSlider.pos = 1;
}
objectFrameReset.onClick = function(_) {
if (linkedObject == null) return;
linkedObject.loadGraphic(linkedObject.pixels);
linkedObject.animDatas.clear();
linkedObject.animation.destroyAnimations();
refresh();
}
objectMiscAntialiasReset.onClick = function(_) {
if (linkedObject != null) objectMiscAntialias.selected = true;
}
objectMiscBlendReset.onClick = function(_) {
if (linkedObject != null) objectMiscBlendDrop.selectedItem = "NORMAL";
}
objectMiscColorReset.onClick = function(_) {
if (linkedObject != null) objectMiscColor.currentColor = Color.fromString("white");
}
objectAnimDanceBeatReset.onClick = function(_) {
if (linkedObject != null) objectAnimDanceBeat.pos = 0;
}
objectAnimStartReset.onClick = function(_) {
if (linkedObject != null) objectAnimStart.text = "";
}
refresh();
}
var prevFrames:Array<FlxFrame> = [];
var prevAnims:Array<String> = [];
override public function refresh()
{
linkedObject = stageEditorState.selectedSprite;
objectPosXStepper.step = stageEditorState.moveStep;
objectPosYStepper.step = stageEditorState.moveStep;
objectAngleSlider.step = funkin.save.Save.instance.stageEditorAngleStep;
if (linkedObject == null)
{
updateFrameList();
updateAnimList();
return;
}
// saving fps
if (objectImagePreview.resource != linkedObject.frame) objectImagePreview.resource = linkedObject.frame;
if (objectZIdxStepper.pos != linkedObject.zIndex) objectZIdxStepper.pos = linkedObject.zIndex;
if (objectPosXStepper.pos != linkedObject.x) objectPosXStepper.pos = linkedObject.x;
if (objectPosYStepper.pos != linkedObject.y) objectPosYStepper.pos = linkedObject.y;
if (objectAlphaSlider.pos != linkedObject.alpha) objectAlphaSlider.pos = linkedObject.alpha;
if (objectAngleSlider.pos != linkedObject.angle) objectAngleSlider.pos = linkedObject.angle;
if (objectScaleXStepper.pos != linkedObject.scale.x) objectScaleXStepper.pos = linkedObject.scale.x;
if (objectScaleYStepper.pos != linkedObject.scale.y) objectScaleYStepper.pos = linkedObject.scale.y;
if (objectSizeXStepper.pos != linkedObject.width) objectSizeXStepper.pos = linkedObject.width;
if (objectSizeYStepper.pos != linkedObject.height) objectSizeYStepper.pos = linkedObject.height;
if (objectScrollXSlider.pos != linkedObject.scrollFactor.x) objectScrollXSlider.pos = linkedObject.scrollFactor.x;
if (objectScrollYSlider.pos != linkedObject.scrollFactor.y) objectScrollYSlider.pos = linkedObject.scrollFactor.y;
if (objectMiscAntialias.selected != linkedObject.antialiasing) objectMiscAntialias.selected = linkedObject.antialiasing;
if (objectMiscColor.currentColor != Color.fromString(linkedObject.color.toHexString() ?? "white"))
objectMiscColor.currentColor = Color.fromString(linkedObject.color.toHexString());
if (objectAnimDanceBeat.pos != linkedObject.danceEvery) objectAnimDanceBeat.pos = linkedObject.danceEvery;
if (objectAnimStart.text != linkedObject.startingAnimation) objectAnimStart.text = linkedObject.startingAnimation;
var objBlend = Std.string(linkedObject.blend) ?? "NONE";
if (objectMiscBlendDrop.selectedItem != objBlend.toUpperCase()) objectMiscBlendDrop.selectedItem = objBlend.toUpperCase();
// ough the max
if (objectFrameImageWidth.max != linkedObject.pixels.width) objectFrameImageWidth.max = linkedObject.graphic.width;
if (objectFrameImageHeight.max != linkedObject.pixels.height) objectFrameImageHeight.max = linkedObject.graphic.height;
// update some anim shit
if (prevFrames != linkedObject.frames.frames.copy()) updateFrameList();
if (prevAnims != linkedObject.animation.getNameList().copy()) updateAnimList();
}
function updateFrameList()
{
prevFrames = [];
objectAnimFrameList.dataSource = new ArrayDataSource();
if (linkedObject == null) return;
for (fname in linkedObject.frames.frames)
{
if (fname != null) objectAnimFrameList.dataSource.add({name: fname.name, tooltip: fname.name});
prevFrames.push(fname);
}
}
function updateAnimList()
{
objectAnimDropdown.dataSource.clear();
prevAnims = [];
if (linkedObject == null) return;
for (aname in linkedObject.animation.getNameList())
{
objectAnimDropdown.dataSource.add({text: aname});
prevAnims.push(aname);
}
if (linkedObject.animation.getNameList().contains(objectAnimStart.text)) objectAnimStart.styleString = "color: white";
else
objectAnimStart.styleString = "color: indianred";
linkedObject.startingAnimation = objectAnimStart.text;
}
}

View file

@ -0,0 +1,60 @@
package funkin.ui.debug.stageeditor.toolboxes;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.TextField;
import haxe.ui.components.DropDown;
import funkin.util.SortUtil;
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/stage-settings.xml"))
class StageEditorStageToolbox extends StageEditorDefaultToolbox
{
var stageNameText:TextField;
var stageZoomStepper:NumberStepper;
var stageLibraryDrop:DropDown;
override public function new(state:StageEditorState)
{
super(state);
stageNameText.onChange = function(_) {
state.stageName = stageNameText.text;
state.saved = false;
}
stageZoomStepper.onChange = function(_) {
state.stageZoom = stageZoomStepper.pos;
state.updateMarkerPos();
state.saved = false;
}
final EXCLUDE_LIBS = ["art", "default", "vlc", "videos", "songs"];
var allLibs = [];
@:privateAccess
{
for (lib => idk in lime.utils.Assets.libraryPaths)
{
if (!EXCLUDE_LIBS.contains(lib)) allLibs.push(lib);
}
}
allLibs.sort(SortUtil.alphabetically); // this system is VERY stupid, it relies on the possibility that the future libraries will be named week(end)[x]
for (lib in allLibs)
{
stageLibraryDrop.dataSource.add({text: lib});
}
stageLibraryDrop.onChange = function(_) {
state.stageFolder = stageLibraryDrop.selectedItem.text;
}
refresh();
}
override public function refresh()
{
stageNameText.text = stageEditorState.stageName;
stageZoomStepper.pos = stageEditorState.stageZoom;
stageLibraryDrop.selectedItem = stageEditorState.stageFolder;
}
}

View file

@ -86,7 +86,7 @@ class LoadingState extends MusicBeatSubState
}
checkLibrary('shared');
checkLibrary(PlayStatePlaylist.campaignId);
checkLibrary(stageDirectory);
checkLibrary('tutorial');
var fadeTime:Float = 0.5;
@ -204,6 +204,8 @@ class LoadingState extends MusicBeatSubState
return Paths.inst(PlayState.instance.currentSong.id);
}
static var stageDirectory:String = "shared";
/**
* Starts the transition to a new `PlayState` to start a new song.
* First switches to the `LoadingState` if assets need to be loaded.
@ -213,7 +215,13 @@ class LoadingState extends MusicBeatSubState
*/
public static function loadPlayState(params:PlayStateParams, shouldStopMusic = false, asSubState = false, ?onConstruct:PlayState->Void):Void
{
Paths.setCurrentLevel(PlayStatePlaylist.campaignId);
var daChart = params.targetSong.getDifficulty(params.targetDifficulty ?? Constants.DEFAULT_DIFFICULTY,
params.targetVariation ?? Constants.DEFAULT_VARIATION);
var daStage = funkin.data.stage.StageRegistry.instance.fetchEntry(daChart.stage);
stageDirectory = daStage?._data?.directory ?? "shared";
Paths.setCurrentLevel(stageDirectory);
var playStateCtor:() -> PlayState = function() {
return new PlayState(params);
};

View file

@ -22,6 +22,7 @@ class FileUtil
public static final FILE_FILTER_JSON:FileFilter = new FileFilter("JSON Data File (.json)", "*.json");
public static final FILE_FILTER_ZIP:FileFilter = new FileFilter("ZIP Archive (.zip)", "*.zip");
public static final FILE_FILTER_PNG:FileFilter = new FileFilter("PNG Image (.png)", "*.png");
public static final FILE_FILTER_FNFS:FileFilter = new FileFilter("Friday Night Funkin' Stage (.fnfs)", "*.fnfs");
public static final FILE_EXTENSION_INFO_FNFC:FileDialogExtensionInfo =
{
@ -39,6 +40,12 @@ class FileUtil
label: 'PNG Image',
};
public static final FILE_EXTENSION_INFO_FNFS:FileDialogExtensionInfo =
{
extension: 'fnfs',
label: 'Friday Night Funkin\' Stage',
};
/**
* Browses for a single file, then calls `onSelect(fileInfo)` when a file is selected.
* Powered by HaxeUI, so it works on all platforms.