Merge branch 'develop' into smoothlerp

This commit is contained in:
cyn 2024-10-11 11:11:25 -07:00 committed by GitHub
commit ac97fe2aa0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 4081 additions and 16 deletions

View file

@ -7,6 +7,6 @@ medium:
min: 10
max: 99
# Add 'large' to any changes for more than 100 lines
# Add 'large' to any changes of at least 100 lines
large:
min: 100

4
.github/labeler.yml vendored
View file

@ -1,11 +1,11 @@
# Add Documentation tag to PR's changing markdown files, or anyhting in the docs folder
# Add Documentation tag to PR's changing markdown files, or anything in the docs folder
Documentation:
- changed-files:
- any-glob-to-any-file:
- docs/*
- '**/*.md'
# Adds Haxe tag to PR's changing haxe code files
# Add Haxe tag to PR's changing haxe code files
Haxe:
- changed-files:
- any-glob-to-any-file: '**/*.hx'

View file

@ -4,14 +4,14 @@ Code style is enforced using Visual Studio Code extensions.
## .hx
Formatting is handled by the `nadako.vshaxe` extension, which includes Haxe Formatter.
Haxe Formatter automatically resolves issues such as intentation style and line breaks, and can be configured in `hxformat.json`.
Haxe Formatter automatically resolves issues such as indentation style and line breaks, and can be configured in `hxformat.json`.
Code Quality is handled by the `vshaxe.haxe-checkstyle` extension, which includes Haxe Checkstyle.
### Haxe Checkstyle Notes
* Checks can be escalated to display as different serverities in the Problems window.
* Checks can be escalated to display as different severities in the Problems window.
* Checks can be disabled by setting the severity to `IGNORE`.
* `IndentationCharacter` checks what is used to indent, `Indentation` checks how deep the intentation is.
* `IndentationCharacter` checks what is used to indent, `Indentation` checks how deep the indentation is.
* `CommentedOutCode` check is in place because old code should be retrieved via Git history.
* TODO items: Enable these one-by-one and fix them to improve the overall code quality.
- Reconfigure `MethodLength`

2
assets

@ -1 +1 @@
Subproject commit 7efad31cf80a42600375bf08b397786df5c1037c
Subproject commit 4abd6cc06e56c6d56440fa858262932db118250c

View file

@ -34,10 +34,11 @@ There are several useful build flags you can add to a build to affect how it wor
- `-DGITHUB_BUILD` will enable in-game debug functions (such as the ability to time travel in a song by pressing `PgUp`/`PgDn`), without enabling the other stuff
- `-DFEATURE_POLYMOD_MODS` or `-DNO_FEATURE_POLYMOD_MODS` to forcibly enable or disable modding support.
- `-DREDIRECT_ASSETS_FOLDER` or `-DNO_REDIRECT_ASSETS_FOLDER` to forcibly enable or disable asset redirection.
- This feature causes the game to load exported assets from the project's assets folder rather than the exported one. Great for fast iteration, but the game
- This feature causes the game to load exported assets from the project's assets folder rather than the exported one. Great for fast iteration, but the game will break if you try to zip it up and send it to someone, so it's disabled for release builds.
- `-DFEATURE_DISCORD_RPC` or `-DNO_FEATURE_DISCORD_RPC` to forcibly enable or disable support for Discord Rich Presence.
- `-DFEATURE_VIDEO_PLAYBACK` or `-DNO_FEATURE_VIDEO_PLAYBACK` to forcibly enable or disable video cutscene support.
- `-DFEATURE_CHART_EDITOR` or `-DNO_FEATURE_CHART_EDITOR` to forcibly enable or disable the chart editor in the Debug menu.
- `-DFEATURE_SCREENSHOTS` or `-DNO_FEATURE_SCREENSHOTS` to forcibly enable or disable the screenshots feature.
- `-DFEATURE_STAGE_EDITOR` to forcibly enable the experimental stage editor.
- `-DFEATURE_GHOST_TAPPING` to forcibly enable an experimental gameplay change to the anti-mash system.

View file

@ -15,8 +15,8 @@ Most of this functionality is only available on debug builds of the game!
- `2`: ***GAIN HEALTH***: Debug function, add 10% to the player's health.
- `3`: ***LOSE HEALTH***: Debug function, subtract 5% to the player's health.
- `9`: NEATO!
- `PAGEUP` (MacOS: `Fn-Up`): ***FORWARDS TIME TRAVEL****: Move forward by 2 sections. Hold SHIFT to move forward by 20 sections instead.
- `PAGEDOWN` (MacOS: `Fn-Down`): ***BACKWARDS TIME TRAVEL****: Move backward by 2 sections. Hold SHIFT to move backward by 20 sections instead.
- `PAGEUP` (MacOS: `Fn-Up`): ***FORWARDS TIME TRAVEL***: Move forward by 2 sections. Hold SHIFT to move forward by 20 sections instead.
- `PAGEDOWN` (MacOS: `Fn-Down`): ***BACKWARDS TIME TRAVEL***: Move backward by 2 sections. Hold SHIFT to move backward by 20 sections instead.
## **Freeplay State**
- `F` (Freeplay Menu) - Move to Favorites
@ -27,5 +27,5 @@ Most of this functionality is only available on debug builds of the game!
- `Y` - WOAH
## **Main Menu**
- `~`: ***DEBUG****: Opens a menu to access the Chart Editor and other work-in-progress editors. Rebindable in the options menu.
- `~`: ***DEBUG***: Opens a menu to access the Chart Editor and other work-in-progress editors. Rebindable in the options menu.
- `CTRL-ALT-SHIFT-W`: ***ALL ACCESS***: Unlocks all songs in Freeplay. Only available on debug builds.

View file

@ -25,7 +25,7 @@ class Project extends HXProject {
* REMEMBER TO CHANGE THIS WHEN THE GAME UPDATES!
* You only have to change it here, the rest of the game will query this value.
*/
static final VERSION:String = "0.5.1";
static final VERSION:String = "0.5.2";
/**
* The game's name. Used as the default window title.

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

@ -0,0 +1,23 @@
package funkin.graphics.shaders;
import flixel.addons.display.FlxRuntimeShader;
/**
* Create a little dotting effect.
*/
class InverseDotsShader extends FlxRuntimeShader
{
public var amount:Float;
public function new(amount:Float = 1.0)
{
super(Assets.getText(Paths.frag("InverseDots")));
setAmount(amount);
}
public function setAmount(value:Float):Void
{
this.amount = value;
this.setFloat("_amount", amount);
}
}

View file

@ -288,6 +288,7 @@ class PolymodHandler
Polymod.blacklistImport('openfl.utils.Assets');
Polymod.blacklistImport('openfl.Lib');
Polymod.blacklistImport('openfl.system.ApplicationDomain');
Polymod.blacklistImport('funkin.util.FunkinTypeResolver');
// `openfl.desktop.NativeProcess`
// Can load native processes on the host operating system.

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,125 @@
package funkin.ui.debug.stageeditor;
import funkin.data.animation.AnimationData;
import funkin.graphics.FunkinSprite;
import funkin.modding.events.ScriptEvent;
import funkin.graphics.shaders.InverseDotsShader;
/**
* 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";
public var selectedShader:InverseDotsShader;
/**
* What animation to play upon starting.
*/
public var startingAnimation:String = "";
public var animDatas:Map<String, AnimationData> = [];
override public function new()
{
super();
selectedShader = new InverseDotsShader(0);
shader = selectedShader;
}
/**
* Whether the Object is currently being modified in the Stage Editor.
*/
public var isDebugged(default, set):Bool = true;
function set_isDebugged(value:Bool):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):Void
{
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):Void
{
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):Void
{
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

@ -1689,6 +1689,7 @@ class FreeplayState extends MusicBeatSubState
function changeDiff(change:Int = 0, force:Bool = false):Void
{
touchTimer = 0;
var previousVariation:String = currentVariation;
// Available variations for current character. We get this since bf is usually `default` variation, and `pico` is `pico`
// but sometimes pico can be the default variation (weekend 1 songs), and bf can be `bf` variation (darnell)
@ -1784,7 +1785,7 @@ class FreeplayState extends MusicBeatSubState
}
// Reset the song preview in case we changed variations (normal->erect etc)
playCurSongPreview();
if (currentVariation != previousVariation) playCurSongPreview();
}
// Set the album graphic and play the animation if relevant.

View file

@ -33,6 +33,7 @@ class FunkinSoundTray extends FlxSoundTray
var bg:Bitmap = new Bitmap(Assets.getBitmapData(Paths.image("soundtray/volumebox")));
bg.scaleX = graphicScale;
bg.scaleY = graphicScale;
bg.smoothing = true;
addChild(bg);
y = -height;
@ -44,6 +45,7 @@ class FunkinSoundTray extends FlxSoundTray
backingBar.y = 5;
backingBar.scaleX = graphicScale;
backingBar.scaleY = graphicScale;
backingBar.smoothing = true;
addChild(backingBar);
backingBar.alpha = 0.4;
@ -60,6 +62,7 @@ class FunkinSoundTray extends FlxSoundTray
bar.y = 5;
bar.scaleX = graphicScale;
bar.scaleY = graphicScale;
bar.smoothing = true;
addChild(bar);
_bars.push(bar);
}

View file

@ -46,6 +46,8 @@ class EnumPreferenceItem extends TextMenuItem
}
lefthandText = new AtlasText(15, y, formatted(defaultValue), AtlasFont.DEFAULT);
this.fireInstantly = true;
}
override function update(elapsed:Float):Void

View file

@ -58,6 +58,8 @@ class NumberPreferenceItem extends TextMenuItem
this.precision = precision;
this.onChangeCallback = callback;
this.valueFormatter = valueFormatter;
this.fireInstantly = true;
}
override function update(elapsed:Float):Void

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.