diff --git a/assets/languages/en/AlphabetEditor.xml b/assets/languages/en/AlphabetEditor.xml
new file mode 100644
index 00000000..d3f3d198
--- /dev/null
+++ b/assets/languages/en/AlphabetEditor.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+ Press ACCEPT to edit this typeface.
+ New Typeface
+ Select a typeface to edit
+
+
+ New Typeface: Feature Not Implemented!
+ This feature isn't implemented yet. Please wait for more cne updates to have this functional.\n\n\n- Codename Devs
+
+
+
+
+ Alphabet Editor
+
+
+ File
+
+ New
+ Save
+ Save As...
+ Exit
+
+
+ Edit
+
+ Undo
+ Redo
+
+
+ Glyph
+
+ New Glyph
+ Edit Glyph
+ Delete Glyph
+
+
+ Offsets
+
+ Move Left
+ Move Up
+ Move Down
+ Move Right
+
+
+
+
+ Clear Offsets
+
+
+ View
+
+ Zoom in
+ Zoom out
+ Reset Zoom
+
+
+
+
\ No newline at end of file
diff --git a/source/funkin/editors/EditorPicker.hx b/source/funkin/editors/EditorPicker.hx
index 3fb4c5d9..c6db792c 100644
--- a/source/funkin/editors/EditorPicker.hx
+++ b/source/funkin/editors/EditorPicker.hx
@@ -8,6 +8,12 @@ class EditorPicker extends MusicBeatSubstate {
// Name is for backwards compatability, dont use it, use id instead
public var options:Array = [
+ {
+ name: "Alphabet Editor",
+ id: "alphabetEditor",
+ iconID: -1,
+ state: funkin.editors.alphabet.AlphabetSelection
+ },
{
name: "Chart Editor",
id: "charter",
@@ -186,8 +192,12 @@ class EditorPickerOption extends FlxTypedSpriteGroup {
FlxG.mouse.visible = true;
iconSpr = new FlxSprite();
iconSpr.loadGraphic(Paths.image('editors/icons'), true, 128, 128);
- iconSpr.animation.add("icon", [iconID], 24, true);
- iconSpr.animation.play("icon");
+ if(iconID >= 0) {
+ iconSpr.animation.add("icon", [iconID], 24, true);
+ iconSpr.animation.play("icon");
+ } else {
+ iconSpr.exists = false;
+ }
iconSpr.antialiasing = true;
if (height < 150) {
iconSpr.scale.set(height / 150, height / 150);
diff --git a/source/funkin/editors/alphabet/AlphabetEditor.hx b/source/funkin/editors/alphabet/AlphabetEditor.hx
new file mode 100644
index 00000000..f0b27969
--- /dev/null
+++ b/source/funkin/editors/alphabet/AlphabetEditor.hx
@@ -0,0 +1,318 @@
+package funkin.editors.alphabet;
+
+import flixel.math.FlxPoint;
+import funkin.backend.system.framerate.Framerate;
+import funkin.backend.utils.XMLUtil.AnimData;
+import funkin.editors.ui.UIContextMenu.UIContextMenuOption;
+import funkin.game.Character;
+import haxe.xml.Access;
+import haxe.xml.Printer;
+
+class AlphabetEditor extends UIState {
+ static var __typeface:String;
+
+ public static var instance(get, null):AlphabetEditor;
+
+ private static inline function get_instance()
+ return FlxG.state is AlphabetEditor ? cast FlxG.state : null;
+
+ public var topMenu:Array;
+ public var topMenuSpr:UITopMenu;
+
+ public var uiGroup:FlxTypedGroup = new FlxTypedGroup();
+
+ var editorCamera:FlxCamera;
+ var uiCamera:FlxCamera;
+
+ public function new(typeface:String) {
+ super();
+ if (typeface != null) __typeface = typeface;
+ }
+
+ inline function translate(id:String, ?args:Array)
+ return TU.translate("alphabetEditor." + id, args);
+
+ public override function create() {
+ super.create();
+
+ WindowUtils.suffix = " (" + translate("name") + ")";
+ SaveWarning.selectionClass = AlphabetSelection;
+ SaveWarning.saveFunc = () -> {_file_save(null);};
+
+ topMenu = [
+ {
+ label: translate("topBar.file"),
+ childs: [
+ {
+ label: translate("file.save"),
+ keybind: [CONTROL, S],
+ onSelect: _file_save
+ },
+ {
+ label: translate("file.saveAs"),
+ keybind: [CONTROL, SHIFT, S],
+ onSelect: _file_saveas
+ },
+ null,
+ {
+ label: translate("file.exit"),
+ onSelect: _file_exit
+ }
+ ]
+ // TODO: add more options
+ },
+ {
+ label: translate("topBar.glyph"),
+ childs: [
+ {
+ label: translate("glyph.newGlyph"),
+ //onSelect: _glyph_new
+ },
+ {
+ label: translate("glyph.editGlyph"),
+ //onSelect: _glyph_edit
+ },
+ {
+ label: translate("glyph.deleteGlyph"),
+ //onSelect: _glyph_delete
+ }
+ ]
+ },
+ {
+ label: translate("topBar.view"),
+ childs: [
+ {
+ label: translate("view.zoomIn"),
+ keybind: [CONTROL, NUMPADPLUS],
+ //onSelect: _view_zoomin
+ },
+ {
+ label: translate("view.zoomOut"),
+ keybind: [CONTROL, NUMPADMINUS],
+ //onSelect: _view_zoomout
+ },
+ {
+ label: translate("view.resetZoom"),
+ keybind: [CONTROL, NUMPADZERO],
+ //onSelect: _view_zoomreset
+ },
+ ]
+ },
+ {
+ label: " ",
+ childs: [
+ {
+ label: "Tape",
+ //onSelect: _tape
+ }
+ ]
+ },
+ {
+ label: "Move Tape Left",
+ childs: [
+ {
+ label: "Tape",
+ //onSelect: _tape
+ }
+ ]
+ },
+ {
+ label: "Move Tape Right",
+ childs: [
+ {
+ label: "Tape",
+ //onSelect: _tape
+ }
+ ]
+ }
+ ];
+
+ editorCamera = FlxG.camera;
+
+ uiCamera = new FlxCamera();
+ uiCamera.bgColor = 0;
+
+ FlxG.cameras.add(uiCamera);
+
+ var bg = new FlxSprite(0, 0).makeSolid(Std.int(FlxG.width + 100), Std.int(FlxG.height + 100), 0xFF7f7f7f);
+ bg.cameras = [editorCamera];
+ bg.scrollFactor.set();
+ bg.screenCenter();
+ add(bg);
+
+ topMenuSpr = new UITopMenu(topMenu);
+ topMenuSpr.cameras = uiGroup.cameras = [uiCamera];
+
+ //var alphabet:Alphabet = new Alphabet(0, 0, __typeface);
+ //add(alphabet);
+
+ var xml = Xml.parse(Assets.getText(Paths.xml('alphabet/$__typeface'))).firstElement();
+ var spritesheet = null;
+ for(node in xml.elements()) {
+ if (node.nodeName == "spritesheet") {
+ spritesheet = node.firstChild().nodeValue;
+ break;
+ }
+ }
+
+ var useColorOffsets = xml.get("useColorOffsets").getDefault("false") == "true";
+
+
+ // todo fix crash if invalid spritesheet;
+ var alphabetFrames = Paths.getFrames(spritesheet);
+
+ var allChars = "";
+ for(frame in alphabetFrames.frames) {
+ var secondChar = frame.name.charAt(1);
+ if(secondChar != " ") continue;
+ if(allChars.contains(frame.name.charAt(0))) continue;
+ allChars += frame.name.charAt(0);
+ }
+
+ var letterSprites:Array = [];
+ for(letter in allChars.split("")) {
+ var spr = new FlxSprite(0, 0);
+ spr.frames = alphabetFrames;
+ spr.antialiasing = true;
+ spr.animation.addByPrefix("letter", letter, 24);
+ spr.animation.play("letter");
+ add(spr);
+ letterSprites.push(spr);
+ }
+
+ var length = letterSprites.length;
+ var letterWidth = 70;
+ var letterHeight = 80;
+ //var width = 10;
+ //var totalWidth = 10 * 70;
+ //for(i in 0...length) {
+ // var letter = letterSprites[i];
+ // var x = i % width * 70 + (FlxG.width / 2 - totalWidth / 2);
+ // var y = Std.int(i / width) * 80 + 100;
+ // letter.setPosition(x, y);
+ //}
+
+ var randomIndex = FlxG.random.int(0, length-1);
+ for(i in 0...length) {
+ var letter = letterSprites[i];
+ letter.alpha = 0.2;
+ }
+ letterSprites[randomIndex].alpha = 1;
+
+ for(i in 0...length) {
+ var letter = letterSprites[i];
+ letter.x = i * letterWidth - randomIndex * letterWidth + (FlxG.width / 2 - letterWidth / 2);
+ letter.y = 50;
+ }
+
+ var bigletter = new FlxSprite(0, 0);
+ bigletter.frames = alphabetFrames;
+ bigletter.antialiasing = true;
+ bigletter.animation.addByPrefix("letter", allChars.charAt(randomIndex), 24);
+ bigletter.animation.play("letter");
+ bigletter.updateHitbox();
+ bigletter.screenCenter();
+ bigletter.scale.set(4, 4);
+ add(bigletter);
+
+ var infoWindow = new GlyphInfoWindow();
+ infoWindow.info.text = [
+ "Char: " + allChars.charAt(randomIndex),
+ "Anim Name: " + allChars.charAt(randomIndex) + " bold0",
+ "X Offset: " + 0,
+ "Y Offset: " + 0,
+ "ScaleX: " + 1,
+ "ScaleY: " + 1,
+ "Angle: " + 0,
+ ].join('\n');
+ uiGroup.add(infoWindow);
+
+ var leftWindow = new UIWindow(30, 720 - 170 - 30, 230, 170, "Components:");
+ leftWindow.members.push({
+ var info = new UIText(leftWindow.x + 28, leftWindow.y + 46, 400, "");
+ info.text = "[0] Letter (" + allChars.charAt(randomIndex) + ")";
+ info.alignment = LEFT;
+ info;
+ });
+ leftWindow.members.push(new UIButton(leftWindow.x + 28, leftWindow.y + 170 - 40, "Add", function () {}));
+ uiGroup.add(leftWindow);
+
+ add(topMenuSpr);
+ add(uiGroup);
+
+ if(Framerate.isLoaded) {
+ Framerate.fpsCounter.alpha = 0.4;
+ Framerate.memoryCounter.alpha = 0.4;
+ Framerate.codenameBuildField.alpha = 0.4;
+ }
+
+ DiscordUtil.call("onEditorLoaded", ["Alphabet Editor", __typeface]);
+ }
+
+ override function destroy() {
+ super.destroy();
+ if(Framerate.isLoaded) {
+ Framerate.fpsCounter.alpha = 1;
+ Framerate.memoryCounter.alpha = 1;
+ Framerate.codenameBuildField.alpha = 1;
+ }
+ }
+
+ public override function update(elapsed:Float) {
+ super.update(elapsed);
+
+ if (true) {
+ if(FlxG.keys.justPressed.ANY)
+ UIUtil.processShortcuts(topMenu);
+ }
+
+ if(FlxG.keys.pressed.J) {
+ editorCamera.scroll.x -= 100 * elapsed;
+ }
+ if(FlxG.keys.pressed.L) {
+ editorCamera.scroll.x += 100 * elapsed;
+ }
+ if(FlxG.keys.pressed.I) {
+ editorCamera.scroll.y -= 100 * elapsed;
+ }
+ if(FlxG.keys.pressed.K) {
+ editorCamera.scroll.y += 100 * elapsed;
+ }
+ }
+
+ function _file_save(_) {
+ #if sys
+ CoolUtil.safeSaveFile(
+ '${Paths.getAssetsRoot()}/data/alphabet/${__typeface}.xml',
+ ""//alphabet.buildXML()
+ );
+ #else
+ _file_saveas(_);
+ #end
+ }
+
+ function _file_saveas(_) {
+ openSubState(new SaveSubstate(""/*alphabet.buildXML()*/, {
+ defaultSaveFile: '${__typeface}.xml'
+ }));
+ }
+
+ function _file_exit(_) {
+ /*if (undos.unsaved) SaveWarning.triggerWarning();
+ else */FlxG.switchState(new AlphabetSelection());
+ }
+}
+
+class GlyphInfoWindow extends UIWindow {
+ public var info:UIText;
+ public function new() {
+ var width = 230;
+ var height = 170;
+ var margin = 30;
+ super(FlxG.width - width - margin, FlxG.height - height - margin, width, height, "Glyph Info");
+
+ info = new UIText(x + 28, y + 46, 400, "");
+ info.alignment = LEFT;
+ members.push(info);
+ }
+}
\ No newline at end of file
diff --git a/source/funkin/editors/alphabet/AlphabetSelection.hx b/source/funkin/editors/alphabet/AlphabetSelection.hx
new file mode 100644
index 00000000..87b7412a
--- /dev/null
+++ b/source/funkin/editors/alphabet/AlphabetSelection.hx
@@ -0,0 +1,89 @@
+package funkin.editors.alphabet;
+
+import haxe.xml.Access;
+import funkin.game.Character;
+import funkin.options.OptionsScreen;
+import funkin.options.type.IconOption;
+import funkin.options.type.NewOption;
+import funkin.options.type.TextOption;
+import funkin.options.type.OptionType;
+import haxe.io.Path;
+
+class AlphabetSelection extends EditorTreeMenu
+{
+ inline function translate(id:String, ?args:Array)
+ return TU.translate("alphabetSelection." + id, args);
+
+ public override function create()
+ {
+ bgType = "charter";
+ super.create();
+
+ var modsList:Array = [];
+ for(file in Paths.getFolderContent('data/alphabet/', true, BOTH)) // mods ? MODS : BOTH
+ if (Path.extension(file) == "xml") modsList.push(CoolUtil.getFilename(file));
+
+ var list:Array = [
+ for (typeface in modsList)
+ new AlphabetIconOption(typeface, translate("acceptTypeface"), typeface, function() {
+ FlxG.switchState(new AlphabetEditor(typeface));
+ })
+ ];
+
+ var newChar = translate("newTypeface");
+ list.insert(0, new NewOption(newChar, newChar, function() {
+ openSubState(new UIWarningSubstate(translate("warnings.notImplemented-title"), translate("warnings.notImplemented-body"), [
+ {label: TU.translate("editor.ok"), color: 0xFFFF0000, onClick: function(t) {}}
+ ]));
+ }));
+
+ main = new OptionsScreen(TU.translate("alphabetEditor.name"), translate("desc"), list);
+
+ DiscordUtil.call("onEditorTreeLoaded", ["Alphabet Editor"]);
+ }
+
+ override function createPost() {
+ super.createPost();
+
+ main.changeSelection(1);
+ }
+}
+
+class AlphabetIconOption extends TextOption {
+ public var iconSpr:FlxSprite;
+
+ public function new(name:String, desc:String, typeface:String, callback:Void->Void) {
+ super(name, desc, callback);
+
+ var xml = Xml.parse(Assets.getText(Paths.xml('alphabet/$typeface'))).firstElement();
+ var spritesheet = null;
+ for(node in xml.elements()) {
+ if (node.nodeName == "spritesheet") {
+ spritesheet = node.firstChild().nodeValue;
+ break;
+ }
+ }
+
+ var useColorOffsets = xml.get("useColorOffsets").getDefault("false") == "true";
+
+
+ // todo fix crash if invalid spritesheet;
+
+ iconSpr = new FlxSprite();
+ iconSpr.frames = Paths.getFrames(spritesheet);
+ iconSpr.antialiasing = true;
+ var frameToUse = iconSpr.frames.frames[0];
+ for(frame in iconSpr.frames.frames) {
+ if (frame.name.toUpperCase().startsWith("A")) {
+ frameToUse = frame;
+ break;
+ }
+ }
+ iconSpr.frame = frameToUse;
+ if(useColorOffsets) {
+ iconSpr.colorTransform.color = -1;
+ }
+ iconSpr.setPosition(90 - iconSpr.width - 20, (__text.height - iconSpr.height) / 2);
+ add(iconSpr);
+ }
+}
\ No newline at end of file