diff --git a/assets/images/editors/bgs/default.png b/assets/images/editors/bgs/default.png new file mode 100644 index 00000000..1b99f9a5 Binary files /dev/null and b/assets/images/editors/bgs/default.png differ diff --git a/assets/images/menus/options/meter.png b/assets/images/menus/options/meter.png deleted file mode 100644 index 4607e9b0..00000000 Binary files a/assets/images/menus/options/meter.png and /dev/null differ diff --git a/assets/images/menus/options/meter.xml b/assets/images/menus/options/meter.xml deleted file mode 100644 index fddf0e4e..00000000 --- a/assets/images/menus/options/meter.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/assets/images/menus/options/slider.png b/assets/images/menus/options/slider.png new file mode 100644 index 00000000..e2bf2192 Binary files /dev/null and b/assets/images/menus/options/slider.png differ diff --git a/assets/images/menus/options/slider.xml b/assets/images/menus/options/slider.xml new file mode 100644 index 00000000..81a9dd2b --- /dev/null +++ b/assets/images/menus/options/slider.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/languages/en/Editors.xml b/assets/languages/en/Editors.xml index 3fe1239c..a0df9dc4 100644 --- a/assets/languages/en/Editors.xml +++ b/assets/languages/en/Editors.xml @@ -46,7 +46,9 @@ Press ACCEPT to edit the chart for the selected difficulty. Select a difficulty to continue. New Difficulty + Press ACCEPT to create a new chart difficulty. New Song + Press ACCEPT to create a new song. @@ -437,6 +439,11 @@ Creating New Character Character Name $* Required$ Character Image File $* Required$ + + + Creating Character: Error! + The character you are trying to create already exists, if you would like to override it delete the character first! + @@ -444,6 +451,7 @@ Press ACCEPT to edit this typeface. New Typeface + Press ACCEPT to create a new typeface. Select a typeface to edit @@ -556,6 +564,11 @@ Stage Info Stage Name Stage Path + + + Creating Stage: Error! + The stage you are trying to create alreadly exists, if you would like to override it delete the stage first! + diff --git a/assets/languages/en/Options.xml b/assets/languages/en/Options.xml index 2b5a6c00..7145f793 100644 --- a/assets/languages/en/Options.xml +++ b/assets/languages/en/Options.xml @@ -12,10 +12,11 @@ Gameplay Change Gameplay options such as Downscroll, Scroll Speed, Naughtyness... - Change more advanced options such as Window Size, Streaming Audio... + Change more advanced options such as Hit Window Size, Streaming Audio... Appearance Change Appearance options such as Flashing menus... + Change more advanced options such as VRAM-Only Sprites, Antialiasing, Shaders Language Change the language that the engine uses. @@ -23,6 +24,9 @@ Miscellaneous Use this menu to reset save data or engine settings. + Debug Options + Debug Options for Developer Mode. + @@ -62,8 +66,8 @@ Framerate Pretty self explanatory, isn't it? - Antialiasing - If unchecked, will disable antialiasing on every sprite. Can boost performances at the cost of sharper, more pixely sprites. + Flashing Menu + If unchecked, will disable menu flashing when you select an option in the Main Menu, and other flashes will be slower. Colored Healthbar If unchecked, the game will use the original red/green health bar from the week 6 fnf game. @@ -71,17 +75,25 @@ Pixel Perfect Effect If checked, Week 6 will have a pixel perfect effect to it enabled, aligning every pixel on the screen. - Gameplay Shaders - If unchecked, gameplay shaders (visual effects like Thorns's Chromatic Aberration) wont be loaded; this may be helpful on weak devices. + + Quality + Quality modes to choose, "LOW" = Disable every heavy visual effects as it can get to smoothen the gameplay, "HIGH" = Enable all of the visual effects, with the cost of performance, "CUSTOM" = Allow of custom options that is available in Advanced. + LOW + HIGH + CUSTOM - Flashing Menu - If unchecked, will disable menu flashing when you select an option in the Main Menu, and other flashes will be slower. + Antialiasing + If unchecked, will disable antialiasing on every sprite. Can boost performances at the cost of sharper, more pixely sprites. - Low Memory Mode - If checked, will disable certain background elements in stages to reduce memory usage. + Low Memory Mode + If checked, will disable certain background elements in stages to reduce memory usage. - VRAM-Only Sprites - If checked, will only store the bitmaps in the GPU, freeing a LOT of memory (EXPERIMENTAL). Turning this off will consume a lot of memory, especially on bigger sprites. If you aren't sure, leave this on. + Gameplay Shaders + If unchecked, gameplay shaders (visual effects like Thorns's Chromatic Aberration) wont be loaded; this may be helpful on weak devices. + + VRAM-Only Sprites + If checked, will only store the bitmaps in the GPU, freeing a LOT of memory (EXPERIMENTAL). Turning this off will consume a lot of memory, especially on bigger sprites. If you aren't sure, leave this on. + @@ -100,6 +112,9 @@ Auto Pause If checked, switching windows will pause the game. + Music Volume + Changes only the volume of the music instead of everything. + SFX Volume Changes only the volume of the SFX instead of everything. @@ -137,6 +152,9 @@ Developer Mode If checked, you will be able to access developer features like certain keybinds, editors, developer options, state reloads, console and more. + Show Configs Warning + If checked, a popup at the start of the game will appear if the current mod doesn't have a mod configuration file + Reset Save Data Select this option to reset save data. This will remove all of your highscores. diff --git a/assets/languages/pl/Options.xml b/assets/languages/pl/Options.xml index 0415b455..68526dde 100644 --- a/assets/languages/pl/Options.xml +++ b/assets/languages/pl/Options.xml @@ -62,26 +62,28 @@ Klatki Ilość Klatek na sekundę ktore gra renderuje - Wygładzanie - Jeśli wyłączone, Wygładzanie nie będzie widoczne na Sprite'ach. Może poprawić wydajność na koszt ostrzejszego, pikselowatego wyglądu Sprite'ów. - Kolorowe Zdrowie Jeśli wyłączone, Pasek zdrowia będzie tylko używał czerwonego i zielonego koloru, takiego jak z oryginalnej gry. Perfekcyjne Piksele Jeśli włączone, Tydzień 6 będzie miał efekt perfekcyjnych pikseli, ustawiający każdy piksel na ekranie. - Shadery Rozgrywki - Jeśli wyłączone, Shadery Rozgrywki (efekty wizualne jak Aberacja Chromatyczna z Thorns) nie będą ładowane; może być to pomocne na słabszych urządzeniach. - Migające Menu Jeśli wyłączone, tło menu głównego nie będzie migać kolorów po wybraniu opcji. Inne migania będą znacznie wolniejsze. - Niskie zużycie pamięci + + Wygładzanie + Jeśli wyłączone, Wygładzanie nie będzie widoczne na Sprite'ach. Może poprawić wydajność na koszt ostrzejszego, pikselowatego wyglądu Sprite'ów. + + Niskie zużycie pamięci Jeśli włączone, usunie niektóre elementy tła, co użyje mniej pamięci. - Sprite'y w VRAM - Jeśli włączone, utrzyma obrazy tylko w GPU, uwalniając więcej pamięci (EKSPERYMENTALNE). Wyłaczenie tego użyje o wiele więcej pamięci, szczególnie przy większych duszkach. Zostaw włączone w wypadku wątpliwości. + Shadery Rozgrywki + Jeśli wyłączone, Shadery Rozgrywki (efekty wizualne jak Aberacja Chromatyczna z Thorns) nie będą ładowane; może być to pomocne na słabszych urządzeniach. + + Sprite'y w VRAM + Jeśli włączone, utrzyma obrazy tylko w GPU, uwalniając więcej pamięci (EKSPERYMENTALNE). Wyłaczenie tego użyje o wiele więcej pamięci, szczególnie przy większych duszkach. Zostaw włączone w wypadku wątpliwości. + @@ -112,7 +114,7 @@ Strumieniowana Muzyka Jeśli włączone, tylko muzyka będzie strumieniować audio, uwalniając więcej pamięci, na możliwy koszt większego użycia CPU. (EKSPERYMENTALNE) -If checked, only musics will have streamed audio, ALSO freeing a LOT of memory with the downside of higher cpu usage if more audio are being streamed at once (EXPERIMENTAL). Turning this off will consume a lot of memory, especially on longer songs. If you aren't sure, leave this on. + Strumieniowane Wokale Jeśli włączone, tylko wokale będą strumieniować audio, uwalniając więcej pamięci, na możliwy koszt większego użycia CPU. (EKSPERYMENTALNE) diff --git a/source/flixel/sound/FlxSound.hx b/source/flixel/sound/FlxSound.hx index 146cdf1a..eb39c4ac 100644 --- a/source/flixel/sound/FlxSound.hx +++ b/source/flixel/sound/FlxSound.hx @@ -312,6 +312,7 @@ class FlxSound extends FlxBasic { _source = null; _sound = null; + if (autoDestroy) persist = false; reset(); } else if (_channel != null && _channel.__isValid) { diff --git a/source/funkin/backend/TurboControls.hx b/source/funkin/backend/TurboControls.hx new file mode 100644 index 00000000..b8bedf22 --- /dev/null +++ b/source/funkin/backend/TurboControls.hx @@ -0,0 +1,121 @@ +package funkin.backend; + +import flixel.input.actions.FlxAction; +import flixel.input.gamepad.FlxGamepadInputID; +import flixel.input.gamepad.FlxGamepad; +import flixel.input.keyboard.FlxKey; +import flixel.input.FlxInput.FlxInputState; +import flixel.FlxBasic; + +import funkin.backend.system.Controls; +import funkin.options.PlayerSettings; + +class TurboBasic extends FlxBasic { + public static inline final DEFAULT_DELAY:Float = 0.4; + public static inline final DEFAULT_INTERVAL:Float = 1 / 18; + + public var delay:Float; + public var interval:Float; + public var activated(default, null):Bool; + public var pressed(get, never):Bool; function get_pressed() return false; + public var allPress:Bool; + + var time:Float = 0; + + public function new(delay = DEFAULT_DELAY, interval = DEFAULT_INTERVAL, allPress = false) { + super(); + this.delay = delay; + this.interval = interval; + this.allPress = allPress; + } + + override function update(elapsed:Float) { + super.update(elapsed); + + if (pressed) { + if (time == 0) activated = true; + else if (activated = (time >= delay + interval)) time -= interval; + time += elapsed; + } + else { + activated = false; + time = 0; + } + } +} + +class TurboControls extends TurboBasic { + public var controlsInstance:Controls; + public var controls:Array; + public function new(controls:Array, ?controlsInstance:Controls, ?delay:Float, ?interval:Float, ?allPress:Bool) { + super(delay, interval, allPress); + this.controls = controls; + this.controlsInstance = controlsInstance != null ? controlsInstance : PlayerSettings.solo.controls; + } + + override function get_pressed() { + if (allPress) { + for (control in controls) if (!controlsInstance.getActionFromControl(control).check()) return false; + } + else { + for (control in controls) if (controlsInstance.getActionFromControl(control).check()) return true; + } + return allPress; + } +} + +class TurboActions extends TurboBasic { + public var actions:Array; + public function new(actions:Array, ?delay:Float, ?interval:Float, ?allPress:Bool) { + super(delay, interval, allPress); + this.actions = actions; + } + + override function get_pressed() { + if (allPress) { + for (action in actions) if (!action.check()) return false; + } + else { + for (action in actions) if (action.check()) return true; + } + return allPress; + } +} + +class TurboKeys extends TurboBasic { + public var keys:Array; + public function new(keys:Array, ?delay:Float, ?interval:Float, ?allPress:Bool) { + super(delay, interval, allPress); + this.keys = keys; + } + + override function get_pressed() { + if (allPress) { + for (key in keys) if (!FlxG.keys.checkStatus(key, PRESSED)) return false; + } + else { + for (key in keys) if (FlxG.keys.checkStatus(key, PRESSED)) return true; + } + return allPress; + } +} + +class TurboButtons extends TurboBasic { + public var inputs:Array; + public var gamepad:FlxGamepad; + public function new(inputs:Array, ?gamepad:FlxGamepad, ?delay:Float, ?interval:Float, ?allPress:Bool) { + super(delay, interval, allPress); + this.inputs = inputs; + this.gamepad = gamepad != null ? gamepad : FlxG.gamepads.firstActive; + } + + override function get_pressed() { + if (allPress) { + for (input in inputs) if (!gamepad.checkStatus(input, PRESSED)) return false; + } + else { + for (input in inputs) if (gamepad.checkStatus(input, PRESSED)) return true; + } + return allPress; + } +} \ No newline at end of file diff --git a/source/funkin/backend/system/Controls.hx b/source/funkin/backend/system/Controls.hx index 0778469d..5f6f8f98 100644 --- a/source/funkin/backend/system/Controls.hx +++ b/source/funkin/backend/system/Controls.hx @@ -191,6 +191,8 @@ class Controls extends FlxActionSet setKeyboardScheme(scheme, false); } + public function getActionFromControl(control:Control):FlxAction return macro_getActionFromControl(control); + public function getKeyName(control:Control, idx:Int = 0):String { var action = macro_getActionFromControl(control); diff --git a/source/funkin/backend/system/MainState.hx b/source/funkin/backend/system/MainState.hx index ba58479c..0d415f26 100644 --- a/source/funkin/backend/system/MainState.hx +++ b/source/funkin/backend/system/MainState.hx @@ -122,7 +122,7 @@ class MainState extends FlxState { CoolUtil.safeAddAttributes('./.temp/', NativeAPI.FileAttribute.HIDDEN); #end - if (Options.devMode) { + if (Options.devMode && Options.allowConfigWarning) { var lib:ModsFolderLibrary; for (e in Paths.assetsTree.libraries) { @:privateAccess if (!(e is openfl.utils.AssetLibrary) || !((lib = cast cast(e, openfl.utils.AssetLibrary).__proxy) is ModsFolderLibrary)) continue; diff --git a/source/funkin/backend/utils/CoolUtil.hx b/source/funkin/backend/utils/CoolUtil.hx index d24e153a..175e0eb0 100644 --- a/source/funkin/backend/utils/CoolUtil.hx +++ b/source/funkin/backend/utils/CoolUtil.hx @@ -519,8 +519,8 @@ final class CoolUtil * @param menuSFX Menu SFX to play * @param volume At which volume it should play */ - @:noUsing public static inline function playMenuSFX(menuSFX:CoolSfx = SCROLL, volume:Float = 1) { - FlxG.sound.play(Paths.sound('menu/' + switch(menuSFX) { + @:noUsing public static inline function playMenuSFX(menuSFX:CoolSfx = SCROLL, volume:Float = 1):FlxSound { + return FlxG.sound.play(Paths.sound('menu/' + switch(menuSFX) { case CONFIRM: 'confirm'; case CANCEL: 'cancel'; case SCROLL: 'scroll'; @@ -528,7 +528,7 @@ final class CoolUtil case UNCHECKED: 'checkboxUnchecked'; case WARNING: 'warningMenu'; default: 'scroll'; - }), volume); + }), volume * Options.volumeSFX); } /** diff --git a/source/funkin/editors/EditorTreeMenu.hx b/source/funkin/editors/EditorTreeMenu.hx index 7bf5a0fb..1a7a0e37 100644 --- a/source/funkin/editors/EditorTreeMenu.hx +++ b/source/funkin/editors/EditorTreeMenu.hx @@ -1,26 +1,25 @@ package funkin.editors; import flixel.addons.display.FlxBackdrop; -import funkin.menus.MainMenuState; -import funkin.options.*; +import funkin.options.type.OptionType; -class EditorTreeMenu extends TreeMenu { +class EditorTreeMenu extends funkin.options.TreeMenu { public var bg:FlxBackdrop; public var bgType:String = "default"; public var bgMovement:FlxPoint = new FlxPoint(); - public override function create() { + override function create() { super.create(); - UIState.setResolutionAware(); - FlxG.camera.fade(0xFF000000, 0.5, true); + } - bg = new FlxBackdrop(); + override function createPost() { + insert(0, bg = new FlxBackdrop()); bg.loadGraphic(Paths.image('editors/bgs/${bgType}')); bg.antialiasing = true; setBackgroundRotation(-5); - add(bg); + super.createPost(); } public inline function setBackgroundRotation(rotation:Float) { @@ -28,7 +27,47 @@ class EditorTreeMenu extends TreeMenu { bg.velocity.set(85, 0).degrees = bg.rotation; } - public override function exit() { - FlxG.switchState(new MainMenuState()); + override function exit() { + FlxG.switchState(new funkin.menus.MainMenuState()); + } + + override function update(elapsed:Float) { + super.update(elapsed); + + bg.colorTransform.redOffset = lerp(bg.colorTransform.redOffset, 0, 0.0625); + bg.colorTransform.greenOffset = lerp(bg.colorTransform.greenOffset, 0, 0.0625); + bg.colorTransform.blueOffset = lerp(bg.colorTransform.blueOffset, 0, 0.0625); + bg.colorTransform.redMultiplier = lerp(bg.colorTransform.redMultiplier, 1, 0.0625); + bg.colorTransform.greenMultiplier = lerp(bg.colorTransform.greenMultiplier, 1, 0.0625); + bg.colorTransform.blueMultiplier = lerp(bg.colorTransform.blueMultiplier, 1, 0.0625); + } + + override function menuChanged() @:privateAccess { + super.menuChanged(); + if (previousMenus.length > 0) return; // selected a sub-menu + + var prev = tree[tree.length - 2]; + if (prev == null || prev.curOption == null || !(prev.curOption is OptionType)) return; + + // small flashbang + var color = cast(prev.curOption, OptionType).editorFlashColor; + bg.colorTransform.redOffset = 0.25 * color.red; + bg.colorTransform.greenOffset = 0.25 * color.green; + bg.colorTransform.blueOffset = 0.25 * color.blue; + bg.colorTransform.redMultiplier = FlxMath.lerp(1, color.redFloat, 0.25); + bg.colorTransform.greenMultiplier = FlxMath.lerp(1, color.greenFloat, 0.25); + bg.colorTransform.blueMultiplier = FlxMath.lerp(1, color.blueFloat, 0.25); + } +} + +class EditorTreeMenuScreen extends funkin.options.TreeMenuScreen { + public function new(name:String, desc:String, ?prefix:String, ?objects:Array, + ?newButton:String, ?newButtonDesc:String, ?newCallback:Void->Void) + { + super(name, desc, prefix, objects); + if (newCallback != null) { + insert(0, new funkin.options.type.NewOption(getID(newButton), getID(newButtonDesc), newCallback)); + curSelected = 1; + } } } \ No newline at end of file diff --git a/source/funkin/editors/alphabet/AlphabetSelection.hx b/source/funkin/editors/alphabet/AlphabetSelection.hx index 1b8155ee..bb274c8f 100644 --- a/source/funkin/editors/alphabet/AlphabetSelection.hx +++ b/source/funkin/editors/alphabet/AlphabetSelection.hx @@ -1,51 +1,35 @@ package funkin.editors.alphabet; +import haxe.io.Path; import haxe.xml.Access; import funkin.game.Character; -import funkin.options.OptionsScreen; -import funkin.options.type.IconOption; +import funkin.editors.EditorTreeMenu; 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("editor.alphabet.selection." + id, args); - - public override function create() - { - bgType = "charter"; +class AlphabetSelection extends EditorTreeMenu { + override function create() { super.create(); + DiscordUtil.call("onEditorTreeLoaded", ["Alphabet Editor"]); + addMenu(new AlphabetSelectionScreen()); + } +} + +class AlphabetSelectionScreen extends EditorTreeMenuScreen { + public function new() { + super('editor.alphabet.name', 'editor.alphabet.selection.desc', 'editor.alphabet.selection.', 'newTypeface', 'newTypefaceDesc', () -> { + parent.openSubState(new UIWarningSubstate(translate('warnings.notImplemented-title'), translate('warnings.notImplemented-body'), [ + {label: TU.translate("editor.ok"), color: 0xFFFF0000, onClick: (t) -> {}} + ])); + }); var modsList:Array = []; - for(file in Paths.getFolderContent('data/alphabet/', true, BOTH)) // mods ? MODS : BOTH + 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("editor.alphabet.name"), translate("desc"), list); - - DiscordUtil.call("onEditorTreeLoaded", ["Alphabet Editor"]); - } - - override function createPost() { - super.createPost(); - - main.changeSelection(1); + for (typeface in modsList) + add(new AlphabetIconOption(typeface, getID('acceptTypeface'), typeface, () -> FlxG.switchState(new AlphabetEditor(typeface)))); } } @@ -57,7 +41,7 @@ class AlphabetIconOption extends TextOption { var xml = Xml.parse(Assets.getText(Paths.xml('alphabet/$typeface'))).firstElement(); var spritesheet = null; - for(node in xml.elements()) { + for (node in xml.elements()) { if (node.nodeName == "spritesheet") { spritesheet = node.firstChild().nodeValue; break; @@ -73,17 +57,19 @@ class AlphabetIconOption extends TextOption { iconSpr.frames = Paths.getFrames(spritesheet); iconSpr.antialiasing = true; var frameToUse = iconSpr.frames.frames[0]; - for(frame in iconSpr.frames.frames) { + for (frame in iconSpr.frames.frames) { if (frame.name.toUpperCase().startsWith("A")) { frameToUse = frame; break; } } iconSpr.frame = frameToUse; - if(useColorOffsets) { + if (useColorOffsets) { iconSpr.colorTransform.color = -1; } iconSpr.setPosition(90 - iconSpr.width - 20, (__text.height - iconSpr.height) / 2); add(iconSpr); + + __text.x = 100; } } \ No newline at end of file diff --git a/source/funkin/editors/character/CharacterSelection.hx b/source/funkin/editors/character/CharacterSelection.hx index 9f009ccb..1b9d2d70 100644 --- a/source/funkin/editors/character/CharacterSelection.hx +++ b/source/funkin/editors/character/CharacterSelection.hx @@ -1,81 +1,69 @@ package funkin.editors.character; import haxe.xml.Printer; -import funkin.editors.ui.UIImageExplorer.ImageSaveData; import funkin.game.Character; -import funkin.options.OptionsScreen; +import funkin.editors.ui.UIImageExplorer.ImageSaveData; +import funkin.editors.EditorTreeMenu; import funkin.options.type.IconOption; import funkin.options.type.NewOption; import funkin.options.type.TextOption; import funkin.options.type.OptionType; -import funkin.backend.assets.ModsFolder; -class CharacterSelection extends EditorTreeMenu -{ - inline function translate(id:String, ?args:Array) - return TU.translate("characterSelection." + id, args); +class CharacterSelection extends EditorTreeMenu { + override function create() { + super.create(); + DiscordUtil.call("onEditorTreeLoaded", ["Character Editor"]); + addMenu(new CharacterSelectionScreen()); + } +} +class CharacterSelectionScreen extends EditorTreeMenuScreen { public var modsList:Array = []; - public override function create() - { - bgType = "charter"; - super.create(); + public function new() { + super('editor.character.name', 'characterSelection.desc', 'characterSelection.', 'newCharacter', 'newCharacterDesc', () -> { + parent.openSubState(new CharacterCreationScreen(createCharacter)); + }); var isMods:Bool = true; modsList = Character.getList(true, true); - if(modsList.length == 0) { + if (modsList.length == 0) { modsList = Character.getList(false, true); isMods = false; } - function generateList(modsList:Array, isMods:Bool, folderPath:String = ""):Array { - var list:Array = []; + function generateList(modsList:Array, isMods:Bool, folderPath:String = ""):Array { + var list:Array = []; - for(char in modsList) { - if(char.endsWith("/")) { + for (char in modsList) { + if (char.endsWith("/")) { var folderName = CoolUtil.getFilename(char.substr(0, char.length-1)); - list.push(new TextOption(folderName + " >", translate("acceptFolder"), function() { + list.push(new TextOption(folderName, getID('acceptFolder'), ' >', () -> { var newModsList = Character.getList(isMods, true, char); - var newList:Array = generateList(newModsList, isMods, folderPath + folderName + "/"); - optionsTree.add(new OptionsScreen(folderPath + folderName, translate("desc-folder", [folderPath + folderName + "/"]), newList)); + var newList:Array = generateList(newModsList, isMods, folderPath + folderName + "/"); + parent.addMenu(new EditorTreeMenuScreen(folderPath + folderName, translate('desc-folder', [folderPath + folderName + "/"]), newList)); + })); + } + else { + list.push(new IconOption(char, getID('acceptCharacter'), Character.getIconFromCharName(folderPath + char, char), () -> { + FlxG.switchState(new CharacterEditor(folderPath + char)); })); - } else { - list.push(new IconOption(char, translate("acceptCharacter"), Character.getIconFromCharName(folderPath + char, char), - function() { - FlxG.switchState(new CharacterEditor(folderPath + char)); - }) - ); } } - list.insert(0, new NewOption(translate("newCharacter"), translate("newCharacterDesc"), function() { - openSubState(new CharacterCreationScreen(createCharacter)); - })); - return list; } - var list = generateList(modsList, isMods); - - main = new OptionsScreen(TU.translate("editor.character.name"), translate("desc"), list); - - DiscordUtil.call("onEditorTreeLoaded", ["Character Editor"]); - } - - override function createPost() { - super.createPost(); - - main.changeSelection(1); + for (o in generateList(modsList, isMods)) add(o); } public function createCharacter(name:String, imageSaveData:ImageSaveData, xml:Xml) { var characterAlreadyExists:Bool = modsList.contains(name); if (characterAlreadyExists) { - openSubState(new UIWarningSubstate("Creating Character: Error!", "The character you are trying to create already exists, if you would like to override it delete the character first!", [ - {label: "Ok", color: 0xFFFF0000, onClick: function(t) {}} + parent.openSubState(new UIWarningSubstate(TU.translate('characterCreationScreen.warning.char-exists-title'), TU.translate('characterCreationScreen.warning.char-exists-body'), [ + {label: TU.translate('editor.ok'), color: 0xFFFF0000, onClick: (t) -> {}} ])); return; } @@ -88,11 +76,10 @@ class CharacterSelection extends EditorTreeMenu UIImageExplorer.saveFilesGlobal(imageSaveData, '${Paths.getAssetsRoot()}/images/characters'); // Add to Menu >:D - var option:IconOption = new IconOption(name, "Press ACCEPT to edit this character.", Character.getIconFromCharName(name), - function() { - FlxG.switchState(new CharacterEditor(name)); - } - ); - optionsTree.members[optionsTree.members.length-1].insert(1, option); + var option:IconOption = new IconOption(name, getID('acceptCharacter'), Character.getIconFromCharName(name), () -> { + FlxG.switchState(new CharacterEditor(name)); + }); + + insert(1, option); } } \ No newline at end of file diff --git a/source/funkin/editors/charter/CharterSelection.hx b/source/funkin/editors/charter/CharterSelection.hx index 467817d7..85776f44 100644 --- a/source/funkin/editors/charter/CharterSelection.hx +++ b/source/funkin/editors/charter/CharterSelection.hx @@ -1,118 +1,83 @@ package funkin.editors.charter; +import flixel.util.FlxColor; import funkin.backend.chart.Chart; -import funkin.backend.chart.ChartData.ChartMetaData; import funkin.backend.chart.ChartData; -import funkin.backend.system.framerate.Framerate; -import funkin.editors.EditorTreeMenu; import funkin.editors.charter.SongCreationScreen.SongCreationData; +import funkin.editors.EditorTreeMenu; import funkin.menus.FreeplayState.FreeplaySonglist; -import funkin.options.*; import funkin.options.type.*; -import funkin.options.type.NewOption; import haxe.Json; using StringTools; class CharterSelection extends EditorTreeMenu { - public var freeplayList:FreeplaySonglist; - public var curSong:ChartMetaData; - public override function create() { - bgType = "charter"; - + override function create() { super.create(); + DiscordUtil.call("onEditorTreeLoaded", ["Chart Editor"]); + addMenu(new CharterSelectionScreen()); + bgType = 'charter'; + } +} - Framerate.offset.y = 60; +class CharterSelectionScreen extends EditorTreeMenuScreen { + public var freeplayList:FreeplaySonglist; + public var songList:Array = []; + public var curSong:ChartMetaData; + inline public function makeChartOption(d:String, name:String):TextOption { + return new TextOption(d, getID('acceptDifficulty'), () -> FlxG.switchState(new Charter(name, d))); + } + + public function makeSongOption(s:ChartMetaData):IconOption { + songList.push(s.name.toLowerCase()); + + var opt = new IconOption(s.name, getID('acceptSong'), s.icon, () -> { + curSong = s; + + var screen = new EditorTreeMenuScreen(s.name, getID('selectDifficulty'), [ + for (d in s.difficulties) if (d != '') makeChartOption(d, s.name) + ]); + + #if sys + screen.insert(0, new NewOption(getID('newDifficulty'), getID('newDifficultyDesc'), () -> { + parent.openSubState(new ChartCreationScreen(saveChart)); + })); + screen.curSelected = 1; + #end + + parent.addMenu(screen); + }); + opt.suffix = ' >'; + opt.editorFlashColor = s.color.getDefault(FlxColor.WHITE); + + return opt; + } + + public function new() { + super('editor.chart.name', 'charterSelection.desc', 'charterSelection.', 'newSong', 'newSongDesc', () -> { + parent.openSubState(new SongCreationScreen(saveSong)); + }); freeplayList = FreeplaySonglist.get(false); - var list:Array = [ - for(s in freeplayList.songs) new EditorIconOption(s.name, TU.translate("charterSelection.acceptSong"), s.icon, function() { - curSong = s; - var list:Array = [ - for(d in s.difficulties) if (d != "") - new TextOption(d, TU.translate("charterSelection.acceptDifficulty"), function() { - FlxG.switchState(new Charter(s.name, d)); - }) - ]; - #if sys - var newDiff = TU.translate("charterSelection.newDifficulty"); - list.push(new NewOption(newDiff, newDiff, function() { - FlxG.state.openSubState(new ChartCreationScreen(saveChart)); - })); - #end - optionsTree.add(new OptionsScreen(s.name, TU.translate("charterSelection.selectDifficulty"), list)); - }, s.color.getDefault(0xFFFFFFFF)) - ]; - - #if sys - var newSong = TU.translate("charterSelection.newSong"); - list.insert(0, new NewOption(newSong, newSong, function() { - FlxG.state.openSubState(new SongCreationScreen(saveSong)); - })); - #end - - main = new OptionsScreen(TU.translate("editor.chart.name"), TU.translate("charterSelection.desc"), list); - - DiscordUtil.call("onEditorTreeLoaded", ["Chart Editor"]); - } - - override function createPost() { - super.createPost(); - - main.changeSelection(1); - } - - public override function update(elapsed:Float) { - super.update(elapsed); - - bg.colorTransform.redOffset = lerp(bg.colorTransform.redOffset, 0, 0.0625); - bg.colorTransform.greenOffset = lerp(bg.colorTransform.greenOffset, 0, 0.0625); - bg.colorTransform.blueOffset = lerp(bg.colorTransform.blueOffset, 0, 0.0625); - bg.colorTransform.redMultiplier = lerp(bg.colorTransform.redMultiplier, 1, 0.0625); - bg.colorTransform.greenMultiplier = lerp(bg.colorTransform.greenMultiplier, 1, 0.0625); - bg.colorTransform.blueMultiplier = lerp(bg.colorTransform.blueMultiplier, 1, 0.0625); - } - - public override function onMenuChange() { - super.onMenuChange(); - if (optionsTree.members.length > 1) { // selected a song - if(main != null) { - var opt = main.members[main.curSelected]; - if(opt is EditorIconOption) { - var opt:EditorIconOption = cast opt; - - // small flashbang - var color = opt.flashColor; - bg.colorTransform.redOffset = 0.25 * color.red; - bg.colorTransform.greenOffset = 0.25 * color.green; - bg.colorTransform.blueOffset = 0.25 * color.blue; - bg.colorTransform.redMultiplier = FlxMath.lerp(1, color.redFloat, 0.25); - bg.colorTransform.greenMultiplier = FlxMath.lerp(1, color.greenFloat, 0.25); - bg.colorTransform.blueMultiplier = FlxMath.lerp(1, color.blueFloat, 0.25); - } - } - } + for (i => s in freeplayList.songs) add(makeSongOption(s)); } #if sys public function saveSong(creation:SongCreationData, ?callback:String -> SongCreationData -> Void) { - var songAlreadyExists:Bool = [for (s in freeplayList.songs) s.name.toLowerCase()].contains(creation.meta.name.toLowerCase()); - + var songAlreadyExists:Bool = songList.contains(creation.meta.name.toLowerCase()); if (songAlreadyExists) { - openSubState(new UIWarningSubstate(TU.translate("chartCreation.warnings.song-exists-title"), TU.translate("chartCreation.warnings.song-exists-body"), [ - {label: TU.translate("editor.ok"), color: 0xFFFF0000, onClick: function(t) {}}, + parent.openSubState(new UIWarningSubstate(TU.translate("chartCreation.warnings.song-exists-title"), TU.translate("chartCreation.warnings.song-exists-body"), [ + {label: TU.translate("editor.ok"), color: 0xFFFF0000, onClick: (t) -> {}}, ])); return; } - // Paths - var songsDir:String = '${Paths.getAssetsRoot()}/songs/'; - var songFolder:String = '$songsDir${creation.meta.name}'; + var songFolder:String = '${Paths.getAssetsRoot()}/songs/${creation.meta.name}'; #if sys // Make Directories - sys.FileSystem.createDirectory(songFolder); + CoolUtil.addMissingFolders(songFolder); sys.FileSystem.createDirectory('$songFolder/song'); sys.FileSystem.createDirectory('$songFolder/charts'); @@ -125,52 +90,30 @@ class CharterSelection extends EditorTreeMenu { if (creation.oppVocals != null) sys.io.File.saveBytes('$songFolder/song/Voices-Opponent.${Flags.SOUND_EXT}', creation.oppVocals); #end - if (callback != null) - callback(songFolder, creation); - - var option = new EditorIconOption(creation.meta.name, TU.translate("charterSelection.acceptSong"), creation.meta.icon, function() { - curSong = creation.meta; - var list:Array = [ - for(d in creation.meta.difficulties) - if (d != "") new TextOption(d, TU.translate("charterSelection.acceptDifficulty"), function() { - FlxG.switchState(new Charter(creation.meta.name, d)); - }) - ]; - var newDiff = TU.translate("charterSelection.newDifficulty"); - list.push(new NewOption(newDiff, newDiff, function() { - FlxG.state.openSubState(new ChartCreationScreen(saveChart)); - })); - optionsTree.insert(1, new OptionsScreen(creation.meta.name, TU.translate("charterSelection.selectDifficulty"), list)); - }, creation.meta.color.getDefault(0xFFFFFFFF)); + if (callback != null) callback(songFolder, creation); // Add to List freeplayList.songs.insert(0, creation.meta); - main.insert(1, option); + insert(1, makeSongOption(creation.meta)); } public function saveChart(name:String, data:ChartData) { var difficultyAlreadyExists:Bool = curSong.difficulties.contains(name); - if (difficultyAlreadyExists) { - openSubState(new UIWarningSubstate(TU.translate("chartCreation.warnings.chart-exists-title"), TU.translate("chartCreation.warnings.chart-exists-body"), [ - {label: TU.translate("editor.ok"), color: 0xFFFF0000, onClick: function(t) {}}, + parent.openSubState(new UIWarningSubstate(TU.translate("chartCreation.warnings.chart-exists-title"), TU.translate("chartCreation.warnings.chart-exists-body"), [ + {label: TU.translate("editor.ok"), color: 0xFFFF0000, onClick: (t) -> {}}, ])); return; } - // Paths var songFolder:String = '${Paths.getAssetsRoot()}/songs/${curSong.name}'; // Save Files CoolUtil.safeSaveFile('$songFolder/charts/${name}.json', Json.stringify(data, Flags.JSON_PRETTY_PRINT)); // Add to List - // duplicated code, todo: fix this curSong.difficulties.push(name); - var option = new TextOption(name, TU.translate("charterSelection.acceptDifficulty"), function() { - FlxG.switchState(new Charter(curSong.name, name)); - }); - optionsTree.members[optionsTree.members.length-1].insert(optionsTree.members[optionsTree.members.length-1].length-1, option); + parent.tree.last().insert(parent.tree.last().length - 1, makeChartOption(name, curSong.name)); // Add to Meta var meta = Json.parse(sys.io.File.getContent('$songFolder/meta.json')); diff --git a/source/funkin/editors/stage/StageSelection.hx b/source/funkin/editors/stage/StageSelection.hx index b78aebc3..418e0190 100644 --- a/source/funkin/editors/stage/StageSelection.hx +++ b/source/funkin/editors/stage/StageSelection.hx @@ -1,46 +1,47 @@ package funkin.editors.stage; import funkin.editors.stage.StageCreationScreen.StageCreationData; +import funkin.editors.EditorTreeMenu; import funkin.game.Stage; -import funkin.options.OptionsScreen; import funkin.options.type.NewOption; import funkin.options.type.OptionType; import funkin.options.type.TextOption; -class StageSelection extends EditorTreeMenu -{ +using StringTools; + +class StageSelection extends EditorTreeMenu { + override function create() { + super.create(); + DiscordUtil.call("onEditorTreeLoaded", ["Stage Editor"]); + addMenu(new StageSelectionScreen()); + } +} + +class StageSelectionScreen extends EditorTreeMenuScreen { public var stages:Array = []; - public override function create() - { - bgType = "charter"; - super.create(); + public function makeStageOption(stage:String):TextOption { + return new TextOption(stage, getID('acceptStage'), () -> { + FlxG.switchState(new StageEditor(stage)); + }); + } + + public function new() { + super('editor.stage.name', 'stageSelection.desc', 'stageSelection.', 'newStage', 'acceptNewStage', () -> { + parent.openSubState(new StageCreationScreen(saveStage)); + }); var modsList:Array = Stage.getList(true, true); - - var list:Array = [ - for (stage in (modsList.length == 0 ? Stage.getList(false, true) : modsList)) { - stages.push(stage); - new TextOption(stage, TU.translate("stageSelection.acceptStage"), - function() { - FlxG.switchState(new StageEditor(stage)); - }); - } - ]; - - list.insert(0, new NewOption(TU.translate("stageSelection.newStage"), TU.translate("stageSelection.acceptNewStage"), function() { - FlxG.state.openSubState(new StageCreationScreen(saveStage)); - })); - - main = new OptionsScreen(TU.translate("editor.stage.name"), TU.translate("stageSelection.desc"), list); - - DiscordUtil.call("onEditorTreeLoaded", ["Stage Editor"]); + for (stage in (modsList.length == 0 ? Stage.getList(false, true) : modsList)) { + stages.push(stage.toLowerCase()); + add(makeStageOption(stage)); + } } public function saveStage(creation:StageCreationData) { - if ([for (s in stages) s.toLowerCase()].contains(creation.name.toLowerCase())) { - openSubState(new UIWarningSubstate("Creating Stage: Error!", "The stage you are trying to create alreadly exists, if you would like to override it delete the stage first!", [ - {label: "Ok", color: 0xFFFF0000, onClick: function(t) {}} + if (stages.contains(creation.name.toLowerCase())) { + parent.openSubState(new UIWarningSubstate(TU.translate("stageCreationScreen.warnings.stage-exists-title"), TU.translate("stageCreationScreen.warnings.stage-exists-body"), [ + {label: TU.translate("editor.ok"), color: 0xFFFF0000, onClick: (t) -> {}}, ])); return; } @@ -51,15 +52,7 @@ class StageSelection extends EditorTreeMenu #end // Add to List - var option = new TextOption(creation.name, TU.translate("stageSelection.acceptStage"), function() { - FlxG.switchState(new StageEditor(creation.name)); - }); - optionsTree.members[optionsTree.members.length-1].insert(1, option); - } - - override function createPost() { - super.createPost(); - - main.changeSelection(1); + stages.push(creation.name.toLowerCase()); + parent.tree.last().insert(parent.tree.last().length - 1, makeStageOption(creation.name)); } } \ No newline at end of file diff --git a/source/funkin/menus/PauseSubState.hx b/source/funkin/menus/PauseSubState.hx index f652b36c..2f054d9f 100644 --- a/source/funkin/menus/PauseSubState.hx +++ b/source/funkin/menus/PauseSubState.hx @@ -14,7 +14,6 @@ import funkin.backend.utils.FunkinParentDisabler; import funkin.editors.charter.Charter; import funkin.menus.StoryMenuState; import funkin.options.OptionsMenu; -import funkin.options.TreeMenu; import funkin.options.keybinds.KeybindsOptions; class PauseSubState extends MusicBeatSubstate @@ -166,8 +165,7 @@ class PauseSubState extends MusicBeatSubstate persistentDraw = false; openSubState(new KeybindsOptions()); case "Change Options": - TreeMenu.lastState = PlayState; - FlxG.switchState(new OptionsMenu()); + FlxG.switchState(new OptionsMenu((_) -> FlxG.switchState(new PlayState()))); case "Exit to charter": FlxG.switchState(new Charter(PlayState.SONG.meta.name, PlayState.difficulty, false)); case "Exit to menu": diff --git a/source/funkin/menus/credits/CreditsCodename.hx b/source/funkin/menus/credits/CreditsCodename.hx index 70e172dc..7051e2f6 100644 --- a/source/funkin/menus/credits/CreditsCodename.hx +++ b/source/funkin/menus/credits/CreditsCodename.hx @@ -9,21 +9,20 @@ import funkin.options.type.GithubIconOption; using StringTools; -class CreditsCodename extends funkin.options.OptionsScreen { +class CreditsCodename extends funkin.options.TreeMenuScreen { public var error:Bool = false; public var totalContributions:Int = 0; public var contribFormats:Array = []; - public override function new() - { - super("Codename Engine", TU.translate("credits.allContributors")); + public function new() { + super("Codename Engine", "credits.allContributors"); tryUpdating(true); } // blame the secondary threads if the code has to look this bad - Nex private var _canReset:Bool = true; private var _downloadingSteps:Int = 0; - public override function update(elapsed:Float) { + override function update(elapsed:Float) { super.update(elapsed); if (_downloadingSteps == 2) { @@ -35,37 +34,37 @@ class CreditsCodename extends funkin.options.OptionsScreen { _canReset = true; updateMenuDesc(); } - else if(_canReset && PlayerSettings.solo.controls.RESET) tryUpdating(); + else if (_canReset && PlayerSettings.solo.controls.RESET) tryUpdating(); } public function tryUpdating(forceDisplaying:Bool = false) { updateMenuDesc(TU.translate("credits.downloadingList")); _canReset = false; - Main.execAsync(function() { - if(checkUpdate() || forceDisplaying) _downloadingSteps = 2; + Main.execAsync(() -> { + if (checkUpdate() || forceDisplaying) _downloadingSteps = 2; else _downloadingSteps = 1; }); } public override function updateMenuDesc(?txt:String) { - if(!_canReset) return; + if (!_canReset) return; super.updateMenuDesc(txt); updateMarkup(); } public function updateMarkup() { - if(parent == null || parent.treeParent == null) return; - var text:String = parent.treeParent.pathDesc.text; - parent.treeParent.pathDesc.text = ""; - parent.treeParent.pathDesc.applyMarkup(text, contribFormats = [ + if (parent == null) return; + var text:String = parent.descLabel.text; + parent.descLabel.text = ""; + parent.descLabel.applyMarkup(text, contribFormats = [ new FlxTextFormatMarkerPair(new FlxTextFormat(Flags.MAIN_DEVS_COLOR), '*'), new FlxTextFormatMarkerPair(new FlxTextFormat(FlxColor.interpolate(Flags.MIN_CONTRIBUTIONS_COLOR, Flags.MAIN_DEVS_COLOR, Options.contributors[curSelected].contributions / totalContributions)), '~') ]); } override function close() { + for (frmt in contribFormats) parent.descLabel.removeFormat(frmt.format); super.close(); - for (frmt in contribFormats) parent.treeParent.pathDesc.removeFormat(frmt.format); } public function checkUpdate():Bool { diff --git a/source/funkin/menus/credits/CreditsMain.hx b/source/funkin/menus/credits/CreditsMain.hx index 76485cbf..79182bcc 100644 --- a/source/funkin/menus/credits/CreditsMain.hx +++ b/source/funkin/menus/credits/CreditsMain.hx @@ -3,8 +3,8 @@ package funkin.menus.credits; import flixel.util.FlxColor; import funkin.backend.assets.AssetSource; import funkin.backend.system.github.GitHubContributor.CreditsGitHubContributor; -import funkin.options.OptionsScreen; import funkin.options.TreeMenu; +import funkin.options.TreeMenuScreen; import funkin.options.type.*; import haxe.xml.Access; @@ -12,15 +12,18 @@ class CreditsMain extends TreeMenu { var bg:FlxSprite; var items:Array = []; - public override function create() { - bg = new FlxSprite(-80).loadAnimatedGraphic(Paths.image('menus/menuBGBlue')); - // bg.scrollFactor.set(); - bg.scale.set(1.15, 1.15); - bg.updateHitbox(); - bg.screenCenter(); - bg.scrollFactor.set(); + override function create() { + super.create(); + + DiscordUtil.call("onMenuLoaded", ["Credits Menu"]); + + add(bg = new FlxSprite().loadAnimatedGraphic(Paths.image('menus/menuBGBlue'))); bg.antialiasing = true; - add(bg); + bg.scrollFactor.set(); + updateBG(); + + var first = new TreeMenuScreen('credits.name', 'credits.madePossible'); + addMenu(first); for (i in funkin.backend.assets.ModsFolder.getLoadedMods()) { var xmlPath = Paths.xml('config/credits/LIB_$i'); @@ -29,28 +32,24 @@ class CreditsMain extends TreeMenu { var access:Access = null; try access = new Access(Xml.parse(Paths.assetsTree.getSpecificAsset(xmlPath, "TEXT"))) catch(e) Logs.trace('[CreditsMain] Error while parsing credits.xml: ${Std.string(e)}', ERROR); - if (access != null) for (c in parseCreditsFromXML(access)) items.push(c); + if (access != null) for (c in parseCreditsFromXML(access)) first.add(c); } } - items.push(new TextOption("Codename Engine >", TU.translate("credits.selectCodename"), function() { - optionsTree.add(Type.createInstance(CreditsCodename, [])); - })); - items.push(new TextOption("Friday Night Funkin'", TU.translate("credits.selectBase"), function() { - CoolUtil.openURL(Flags.URL_FNF_ITCH); - })); - - main = new OptionsScreen(TU.translate("credits.name"), TU.translate("credits.madePossible"), items); - super.create(); - - DiscordUtil.call("onMenuLoaded", ["Credits Menu"]); + first.add(new TextOption('Codename Engine', 'credits.selectCodename', ' >', () -> addMenu(new CreditsCodename()))); + first.add(new TextOption('Friday Night Funkin\'', 'credits.selectBase', ' >', () -> CoolUtil.openURL(Flags.URL_FNF_ITCH))); } - /** - * XML STUFF - */ - public function parseCreditsFromXML(xml:Access, source:AssetSource = BOTH):Array { - var credsMenus:Array = []; + public function updateBG() { + var scaleX:Float = FlxG.width / bg.width; + var scaleY:Float = FlxG.height / bg.height; + bg.scale.x = bg.scale.y = Math.max(scaleX, scaleY) * 1.15; + bg.screenCenter(); + } + + // XML STUFF + public function parseCreditsFromXML(xml:Access, source:AssetSource = BOTH):Array { + var credsMenus:Array = []; for(node in xml.elements) { var desc = node.getAtt("desc").getDefault("No Description"); @@ -94,13 +93,11 @@ class CreditsMain extends TreeMenu { credsMenus.push(opt); case "menu": - credsMenus.push(new TextOption(name + " >", desc, function() { - optionsTree.add(new OptionsScreen(name, desc, parseCreditsFromXML(node, source))); - })); + credsMenus.push(new TextOption(name, desc, ' >', () -> addMenu(new TreeMenuScreen(name, desc, parseCreditsFromXML(node, source))))); } } } return credsMenus; } -} +} \ No newline at end of file diff --git a/source/funkin/menus/ui/Alphabet.hx b/source/funkin/menus/ui/Alphabet.hx index c3508c31..fcc447a9 100644 --- a/source/funkin/menus/ui/Alphabet.hx +++ b/source/funkin/menus/ui/Alphabet.hx @@ -167,6 +167,7 @@ class Alphabet extends FlxSprite { // for menu shit public var targetY:Float = 0; public var isMenuItem:Bool = false; + public var itemHeight:Float = 120; public function new(?x:Float, ?y:Float, ?text:String = "", ?font:OneOfTwo = "normal") { super(x, y); @@ -202,7 +203,7 @@ class Alphabet extends FlxSprite { if (isMenuItem) { var scaledY = targetY * 1.3; - y = CoolUtil.fpsLerp(y, (scaledY * 120) + (FlxG.height - height) * 0.5, 0.16); + y = CoolUtil.fpsLerp(y, (scaledY * itemHeight) + (FlxG.height - height) * 0.5, 0.16); x = CoolUtil.fpsLerp(x, (targetY * 20) + 90, 0.16); } } diff --git a/source/funkin/menus/ui/Slider.hx b/source/funkin/menus/ui/Slider.hx new file mode 100644 index 00000000..c55d9724 --- /dev/null +++ b/source/funkin/menus/ui/Slider.hx @@ -0,0 +1,300 @@ +package funkin.menus.ui; + +import flixel.animation.FlxAnimation; +import flixel.graphics.frames.FlxFramesCollection; +import flixel.graphics.frames.FlxFrame; +import flixel.math.FlxAngle; +import flixel.math.FlxPoint; +import flixel.math.FlxRect; +import flixel.util.FlxDestroyUtil; + +class Slider extends FlxSprite { + public var playSound:Bool = true; + public var selected:Bool = false; + public var value:Float; + public var segments(default, set):Int; + + public var barWidth(default, set):Float; + public var barHeight(default, null):Float; + public var showSlider:Bool = true; + + public var barFramerate:Float = 24.0; + var __animTime:Float = 0.0; + var __frameTime:Int; + var __curSegments:Int; + var __barFrame:FlxFrame; + var __barClipRect:FlxRect; + var __valueWidth:Float; + var __cornerWidth:Float; + var __segmentWidth:Float; + var __barWidth:Float; + var __flipX:Bool; + var __flipY:Bool; + + public function new(?x:Float, ?y:Float, value:Float = 0.5, barWidth:Float = 500, segments:Int = 5) { + super(x, y); + this.value = value; + this.barWidth = barWidth; + this.segments = segments; + + frames = Paths.getFrames('menus/options/slider'); + antialiasing = true; + } + + override function initVars() { + super.initVars(); + __barClipRect = FlxRect.get(); + } + + override function destroy() { + super.destroy(); + __barFrame = FlxDestroyUtil.destroy(__barFrame); + __barClipRect = FlxDestroyUtil.put(__barClipRect); + } + + function playAnimOrDefault(a:String, d:String) { + if (animation.exists(a)) animation.play(a); + else animation.play(d); + } + + override function update(elapsed:Float) { + super.update(elapsed); + __frameTime = Math.floor((__animTime += elapsed) * barFramerate); + + if (animation.curAnim == null) { + animation.play(selected ? 'selected' : 'unselected'); + __curSegments = CoolUtil.minInt(segments, Math.floor(value * (segments + 1))); + } + else { + switch (animation.curAnim.name) { + case 'selected': if (!selected) playAnimOrDefault('deselect', 'unselected'); + case 'unselected': if (selected) playAnimOrDefault('selecting', 'selected'); + case 'selecting' | 'segment': if (animation.finished) animation.play('selected'); + case 'deselect': if (animation.finished) animation.play('unselected'); + } + + if (__curSegments != (__curSegments = CoolUtil.minInt(segments, Math.floor(value * (segments + 1)))) && playSound) { + FlxG.sound.play(Paths.sound('menu/volume')).pitch = 0.75 + __curSegments * 0.5 / (segments + 1); + if (animation.curAnim.name == 'selected' && animation.exists('segment')) animation.play('segment'); + } + } + } + + override function isSimpleRender(?camera:FlxCamera):Bool return false; + override function drawComplex(camera:FlxCamera) { + __flipX = checkFlipX(); + __flipY = checkFlipY(); + __valueWidth = barWidth * value; + + var width = barWidth / (segments + 1), x = drawCorner(camera, 0, false); + var end = barWidth - __cornerWidth, nextSegment = width - (segments == 0 ? 0 : __segmentWidth * 0.5); + while (x < end) { + if (x == (x = drawBar(camera, x, Math.min(nextSegment - x, end - x)))) break; + if (x < end && x < nextSegment + __segmentWidth * 0.5) { + x = drawSegment(camera, x, Math.min(__segmentWidth, nextSegment + __segmentWidth - x), CoolUtil.bound(x - end, 0, __segmentWidth)); + } + nextSegment += width; + } + drawCorner(camera, x, true); + + if (showSlider) { + _frame.prepareMatrix(_matrix, ANGLE_0, __flipX != camera.flipX, __flipY != camera.flipY); + prepareDrawMatrix(camera, -_frame.sourceSize.x * 0.5 + (__flipX ? barWidth - __valueWidth : __valueWidth), -(_frame.sourceSize.y - barHeight) * 0.5); + forceDrawFrame(camera, _frame); + } + } + + inline function prepareDrawMatrix(camera:FlxCamera, x:Float, y:Float) { + _matrix.translate(-origin.x, -origin.y); + + if (frameOffsetAngle != null && frameOffsetAngle != angle) { + var angleOff = (frameOffsetAngle - angle) * FlxAngle.TO_RAD; + var cos = Math.cos(angleOff), sin = Math.sin(angleOff); + + _matrix.rotateWithTrig(cos, -sin); + _matrix.translate(-frameOffset.x + x, -frameOffset.y + y); + _matrix.rotateWithTrig(cos, sin); + } + else + _matrix.translate(-frameOffset.x + x, -frameOffset.y + y); + + _matrix.scale(scale.x, scale.y); + + if (bakedRotationAngle <= 0) { + updateTrig(); + if (angle != 0) _matrix.rotateWithTrig(_cosAngle, _sinAngle); + } + + getScreenPosition(_point, camera).subtractPoint(offset); + _point.add(origin.x, origin.y); + _matrix.translate(_point.x, _point.y); + + if (isPixelPerfectRender(camera)) { + _matrix.tx = Math.floor(_matrix.tx); + _matrix.ty = Math.floor(_matrix.ty); + } + + doAdditionalMatrixStuff(_matrix, camera); + } + + function forceDrawFrame(camera:FlxCamera, frame:FlxFrame) { + if (layer != null) + layer.drawPixels(this, camera, frame, frame.parent.bitmap, _matrix, colorTransform, blend, antialiasing, shaderEnabled ? shader : null); + else + camera.drawPixels(frame, frame.parent.bitmap, _matrix, colorTransform, blend, antialiasing, shaderEnabled ? shader : null); + } + + function drawCorner(camera:FlxCamera, x:Float, flip:Bool):Float { + var anim, frame; + + if (__valueWidth > x) { + anim = getBarAnim(1, true); + frame = frames.frames[anim.frames[__frameTime % anim.numFrames]]; + + if (flip) __barClipRect.set(__cornerWidth - __valueWidth + x, 0, __valueWidth - x, frame.sourceSize.y); + else __barClipRect.set(0, 0, __valueWidth - x, frame.sourceSize.y); + __barFrame = frame.clipTo(__barClipRect, __barFrame); + __barFrame.prepareMatrix(_matrix, ANGLE_0, flip != __flipX != camera.flipX, __flipY != camera.flipY); + prepareDrawMatrix(camera, __flipX ? barWidth - x - frame.sourceSize.x : x, 0); + forceDrawFrame(camera, __barFrame); + } + + if (__valueWidth - x < __cornerWidth) { + anim = getBarAnim(1, false); + frame = frames.frames[anim.frames[__frameTime % anim.numFrames]]; + + if (flip) __barClipRect.set(0, 0, __cornerWidth - __valueWidth + x, frame.sourceSize.y); + else __barClipRect.set(__valueWidth - x, 0, __cornerWidth - __valueWidth + x, frame.sourceSize.y); + __barFrame = frame.clipTo(__barClipRect, __barFrame); + __barFrame.prepareMatrix(_matrix, ANGLE_0, flip != __flipX != camera.flipX, __flipY != camera.flipY); + prepareDrawMatrix(camera, __flipX ? barWidth - x - frame.sourceSize.x : x, 0); + forceDrawFrame(camera, __barFrame); + } + + return x + __cornerWidth; + } + + function drawBar(camera:FlxCamera, x:Float, width:Float):Float { + if (width <= 0) return x; + var anim, frame, s; + + if (__valueWidth > x) { + anim = getBarAnim(0, true); + frame = frames.frames[anim.frames[__frameTime % anim.numFrames]]; + s = width / frame.sourceSize.x; + + __barClipRect.set(0, 0, (__valueWidth - x) / s, frame.sourceSize.y); + __barFrame = frame.clipTo(__barClipRect, __barFrame); + __barFrame.prepareMatrix(_matrix, ANGLE_0, __flipX != camera.flipX, __flipY != camera.flipY); + _matrix.scale(s, 1); + prepareDrawMatrix(camera, __flipX ? barWidth - x - width : x, 0); + forceDrawFrame(camera, __barFrame); + } + + if (__valueWidth - x < width) { + anim = getBarAnim(0, false); + frame = frames.frames[anim.frames[__frameTime % anim.numFrames]]; + s = width / frame.sourceSize.x; + + __barClipRect.set((__valueWidth - x) / s, 0, (width - __valueWidth + x) / s, frame.sourceSize.y); + __barFrame = frame.clipTo(__barClipRect, __barFrame); + __barFrame.prepareMatrix(_matrix, ANGLE_0, __flipX != camera.flipX, __flipY != camera.flipY); + _matrix.scale(s, 1); + prepareDrawMatrix(camera, __flipX ? barWidth - x - width : x, 0); + forceDrawFrame(camera, __barFrame); + } + + return x + width; + } + + function drawSegment(camera:FlxCamera, x:Float, width:Float, offset:Float):Float { + if (width <= 0) return x; + var anim, frame; + + if (__valueWidth > x) { + anim = getBarAnim(2, true); + frame = frames.frames[anim.frames[__frameTime % anim.numFrames]]; + + __barClipRect.set(offset, 0, __valueWidth - x, frame.sourceSize.y); + __barFrame = frame.clipTo(__barClipRect, __barFrame); + __barFrame.prepareMatrix(_matrix, ANGLE_0, __flipX != camera.flipX, __flipY != camera.flipY); + prepareDrawMatrix(camera, __flipX ? barWidth - x - width : x, 0); + forceDrawFrame(camera, __barFrame); + } + + if (__valueWidth - x < __segmentWidth) { + anim = getBarAnim(2, false); + frame = frames.frames[anim.frames[__frameTime % anim.numFrames]]; + + __barClipRect.set(__valueWidth - x + offset, 0, __segmentWidth - offset - __valueWidth + x, frame.sourceSize.y); + __barFrame = frame.clipTo(__barClipRect, __barFrame); + __barFrame.prepareMatrix(_matrix, ANGLE_0, __flipX != camera.flipX, __flipY != camera.flipY); + prepareDrawMatrix(camera, __flipX ? barWidth - x - width : x, 0); + forceDrawFrame(camera, __barFrame); + } + + return x + width; + } + + function getBarAnim(type:Int, filled:Bool):FlxAnimation { + var name = (type == 1 ? 'corner ' : (type == 2 ? 'segment ' : 'bar ')) + (filled ? 'filled0' : 'empty0'); + if (animation.exists(name)) return animation.getByName(name); + + animation.addByPrefix(name, name, barFramerate, true); + return animation.getByName(name); + } + + inline function sliderResetFrameSize() { + frameWidth = Std.int(barWidth); + frameHeight = Std.int(barHeight); + _halfSize.set(0.5 * frameWidth, 0.5 * frameHeight); + resetSize(); + } + + override function resetHelpers() { + animation.addByPrefix('unselected', 'slider unselected0', 24, true); + animation.addByPrefix('selected', 'slider selected0', 24, true); + animation.addByPrefix('deselect', 'slider deselect0', 24, false); + animation.addByPrefix('selecting', 'slider selecting0', 24, false); + animation.addByPrefix('segment', 'slider segment0', 24, false); + + var anim, width; + inline function checkSize(type) { + barHeight = Math.max(barHeight, frames.frames[anim.frames[0]].sourceSize.y); + width = frames.frames[anim.frames[0]].sourceSize.x; + switch (type) { + case 0: __barWidth = Math.max(__barWidth, width); + case 1: __cornerWidth = Math.max(__cornerWidth, width); + case 2: __segmentWidth = Math.max(__segmentWidth, width); + } + } + + barHeight = 0; + for (type in 0...3) { + if ((anim = getBarAnim(type, false)) != null) checkSize(type); + if ((anim = getBarAnim(type, true)) != null) checkSize(type); + } + + sliderResetFrameSize(); + resetSizeFromFrame(); + centerOrigin(); + + if (FlxG.renderBlit) { + dirty = true; + updateFramePixels(); + } + } + + override function set_frame(v:FlxFrame):FlxFrame { + super.set_frame(v); + if (v != null) sliderResetFrameSize(); + return v; + } + + function set_barWidth(v:Float):Float { + origin.x = _halfSize.x = 0.5 * (frameWidth = Std.int(barWidth = v)); + return v; + } + + function set_segments(v:Int):Int return segments = CoolUtil.maxInt(v, 0); +} \ No newline at end of file diff --git a/source/funkin/options/Options.hx b/source/funkin/options/Options.hx index 0e31a51a..0b0bda8d 100644 --- a/source/funkin/options/Options.hx +++ b/source/funkin/options/Options.hx @@ -29,6 +29,7 @@ class Options public static var autoPause:Bool = true; public static var antialiasing:Bool = true; public static var volume:Float = 1; + public static var volumeMusic:Float = 1; public static var volumeSFX:Float = 1; public static var week6PixelPerfect:Bool = true; public static var gameplayShaders:Bool = true; @@ -44,6 +45,8 @@ class Options public static var language = "en"; // default to english, Flags.DEFAULT_LANGUAGE should not modify this public static var streamedMusic:Bool = true; public static var streamedVocals:Bool = true; + public static var quality:Int = 1; + public static var allowConfigWarning:Bool = true; #if MODCHARTING_FEATURES public static var modchartingHoldSubdivisions:Int = 4; #end @@ -211,9 +214,23 @@ class Options public static function applySettings() { applyKeybinds(); - FlxG.game.stage.quality = (FlxG.enableAntialiasing = antialiasing) ? LOW : BEST; + + switch (quality) { + case 0: + antialiasing = false; + lowMemoryMode = true; + gameplayShaders = false; + case 1: + antialiasing = true; + lowMemoryMode = false; + gameplayShaders = true; + } + + FlxG.sound.defaultMusicGroup.volume = volumeMusic; + FlxG.game.stage.quality = (FlxG.enableAntialiasing = antialiasing) ? BEST : LOW; FlxG.autoPause = autoPause; - FlxG.drawFramerate = FlxG.updateFramerate = framerate; + if (FlxG.updateFramerate < framerate) FlxG.drawFramerate = FlxG.updateFramerate = framerate; + else FlxG.updateFramerate = FlxG.drawFramerate = framerate; } public static function applyKeybinds() { diff --git a/source/funkin/options/OptionsMenu.hx b/source/funkin/options/OptionsMenu.hx index cba1c9af..6359a21f 100644 --- a/source/funkin/options/OptionsMenu.hx +++ b/source/funkin/options/OptionsMenu.hx @@ -1,103 +1,94 @@ package funkin.options; -import funkin.backend.system.framerate.Framerate; -import funkin.options.TreeMenu; +import haxe.xml.Access; +import flixel.util.typeLimit.OneOfThree; +import funkin.editors.ui.UIState; import funkin.options.categories.*; import funkin.options.type.*; -import funkin.options.type.Checkbox; -import haxe.xml.Access; + +typedef OptionCategory = { + var name:String; + var desc:String; + var ?state:OneOfThree, (name:String, desc:String) -> TreeMenuScreen>; + var ?substate:OneOfThree, (name:String, desc:String) -> MusicBeatSubstate>; + var ?suffix:String; +} class OptionsMenu extends TreeMenu { public static var mainOptions:Array = [ { // name and desc are actually the translations ids! - Nex name: 'optionsTree.controls-name', desc: 'optionsTree.controls-desc', - state: null, - substate: (name:String, desc:String) -> { - return new funkin.options.keybinds.KeybindsOptions(); - } + suffix: '', + substate: funkin.options.keybinds.KeybindsOptions }, { name: 'optionsTree.gameplay-name', desc: 'optionsTree.gameplay-desc', - suffix: " >", state: GameplayOptions }, { name: 'optionsTree.appearance-name', desc: 'optionsTree.appearance-desc', - suffix: " >", state: AppearanceOptions }, #if TRANSLATIONS_SUPPORT { name: 'optionsTree.language-name', desc: 'optionsTree.language-desc', - suffix: " >", state: LanguageOptions }, #end { name: 'optionsTree.miscellaneous-name', desc: 'optionsTree.miscellaneous-desc', - suffix: " >", state: MiscOptions - }, - { - name: "Debug Options", - desc: "debugOptions", - suffix: " >", - state: DebugOptions } ]; var bg:FlxSprite; + var debugOption:TextOption; - var addedDebugOptions:Bool = !Options.devMode; - var debugModeButton:OptionType; - var debugModeIndex:Int = mainOptions.length - 1; - - public override function create() { + override function create() { super.create(); CoolUtil.playMenuSong(); DiscordUtil.call("onMenuLoaded", ["Options Menu"]); - bg = new FlxSprite(-80).loadAnimatedGraphic(Paths.image('menus/menuBGBlue')); - // bg.scrollFactor.set(); - bg.scale.set(1.15, 1.15); - bg.updateHitbox(); - bg.screenCenter(); - bg.scrollFactor.set(); + add(bg = new FlxSprite().loadAnimatedGraphic(Paths.image('menus/menuBGBlue'))); bg.antialiasing = true; - add(bg); + bg.scrollFactor.set(); + updateBG(); - main = new OptionsScreen("optionsMenu.header.title", "optionsMenu.header.desc", [for(o in mainOptions) { - new TextOption(o.name, o.desc, o.suffix, function() { - if (o.substate != null) { - persistentUpdate = false; - persistentDraw = true; - if (o.substate is MusicBeatSubstate) { - openSubState(o.substate); - } else if(Reflect.isFunction(o.substate)) { - var substate:(name:String, desc:String) -> MusicBeatSubstate = o.substate; - openSubState(substate(o.name, o.desc)); - } else { // o.substate is Class - openSubState(Type.createInstance(o.substate, [o.name, o.desc])); - } - } else { - if (o.state is OptionsScreen) { - optionsTree.add(o.state); - } else if(Reflect.isFunction(o.state)) { - var state:(name:String, desc:String) -> OptionsScreen = o.state; - optionsTree.add(state(o.name, o.desc)); - } else { // o.state is Class - optionsTree.add(Type.createInstance(o.state, [o.name, o.desc])); - } + addMenu(new TreeMenuScreen('optionsMenu.header.title', 'optionsMenu.header.desc', [for (o in mainOptions) new TextOption(o.name, o.desc, o.suffix != null ? o.suffix : " >", () -> { + if (o.substate != null) { + persistentUpdate = false; + persistentDraw = true; + + if (o.substate is MusicBeatSubstate) + openSubState(o.substate); + else if(Reflect.isFunction(o.substate)) { + var substate:(name:String, desc:String) -> MusicBeatSubstate = o.substate; + openSubState(substate(o.name, o.desc)); } - }); - }]); + else // o.substate is Class + openSubState(Type.createInstance(o.substate, [o.name, o.desc])); + } + else { + if (o.state is TreeMenuScreen) + addMenu(o.state); + else if (Reflect.isFunction(o.state)) { + var state:(name:String, desc:String) -> TreeMenuScreen = o.state; + addMenu(state(o.name, o.desc)); + } + else { // o.state is Class + addMenu(Type.createInstance(o.state, [o.name, o.desc])); + } + } + })])); + + checkDebugOption(); for (i in funkin.backend.assets.ModsFolder.getLoadedMods()) { var xmlPath = Paths.xml('config/options/LIB_$i'); @@ -106,48 +97,55 @@ class OptionsMenu extends TreeMenu { var access:Access = null; try access = new Access(Xml.parse(Paths.assetsTree.getSpecificAsset(xmlPath, "TEXT"))) catch(e) Logs.trace('Error while parsing options.xml: ${Std.string(e)}', ERROR); - if (access != null) for (o in parseOptionsFromXML(access)) main.add(o); + if (access != null) for (o in parseOptionsFromXML(access)) tree.first().add(o); } } - - doDebugOptionThing(); } - public override function onMenuClose(m:OptionsScreen) { - doDebugOptionThing(); - } - - public function reloadStrings() { - for(o in main.members) { - o.reloadStrings(); + function checkDebugOption() { + var screen = tree.first(); + if (Options.devMode) { + if (debugOption == null) { + screen.insert(CoolUtil.minInt(screen.length, mainOptions.length), + debugOption = new TextOption('optionsTree.debug-name', 'optionsTree.debug-desc', ' >', () -> addMenu(new DebugOptions())) + ); + } + } + else if (debugOption != null) { + screen.remove(debugOption, true); + debugOption = flixel.util.FlxDestroyUtil.destroy(debugOption); + if (screen.curSelected >= screen.length) screen.changeSelection(0, true); } - optionsTree.reloadStrings(); - reloadLabels(); } - public override function exit() { + public function updateBG() { + var scaleX:Float = FlxG.width / bg.width; + var scaleY:Float = FlxG.height / bg.height; + bg.scale.x = bg.scale.y = Math.max(scaleX, scaleY) * 1.15; + bg.screenCenter(); + } + + override function onResize(width:Int, height:Int) { + super.onResize(width, height); + if (!UIState.resolutionAware) return; + + updateBG(); + } + + override function menuChanged() { + super.menuChanged(); + checkDebugOption(); + } + + override function exit() { Options.save(); Options.applySettings(); super.exit(); } - function doDebugOptionThing() { - if ((Options.devMode && !addedDebugOptions) || (!Options.devMode && addedDebugOptions)) { - trace((addedDebugOptions ? "remove" : "add") + " option"); - addedDebugOptions = !addedDebugOptions; - if (debugModeButton == null) debugModeButton = main.members[debugModeIndex]; - if (addedDebugOptions) - main.add(debugModeButton); - else - main.remove(debugModeButton); - } - } - - /** - * XML STUFF - */ - public function parseOptionsFromXML(xml:Access):Array { - var options:Array = []; + // XML STUFF + public function parseOptionsFromXML(xml:Access):Array { + var options:Array = []; for(node in xml.elements) { if (!node.has.name) { @@ -163,7 +161,7 @@ class OptionsMenu extends TreeMenu { Logs.warn("A checkbox option requires an \"id\" for option saving."); continue; } - options.push(new Checkbox(name, desc, node.att.id, FlxG.save.data)); + options.push(new Checkbox(name, desc, node.att.id, null, FlxG.save.data)); case "number": if (!node.has.id) { @@ -189,9 +187,7 @@ class OptionsMenu extends TreeMenu { options.push(new ArrayOption(name, desc, optionOptions, optionDisplayOptions, node.att.id, null, FlxG.save.data)); case "menu": - options.push(new TextOption(name + " >", desc, function() { - optionsTree.add(new OptionsScreen(name, desc, parseOptionsFromXML(node))); - })); + options.push(new TextOption(name, desc, ' >', () -> addMenu(new TreeMenuScreen(name, desc, parseOptionsFromXML(node))))); } } diff --git a/source/funkin/options/OptionsScreen.hx b/source/funkin/options/OptionsScreen.hx deleted file mode 100644 index bb71aec9..00000000 --- a/source/funkin/options/OptionsScreen.hx +++ /dev/null @@ -1,143 +0,0 @@ -package funkin.options; - -import funkin.options.type.OptionType; - -class OptionsScreen extends FlxTypedSpriteGroup { - public static var optionHeight:Float = 120; - - public var parent:OptionsTree; - - public var curSelected:Int = 0; - public var id:Int = 0; - - private var __firstFrame:Bool = true; - - public var name:String; - public var desc:String; - /** - * The prefix to add to the translations ids. - **/ - public var prefix:String = ""; - - private var rawName(default, set):String; - private var rawDesc(default, set):String; - - public inline function getNameID(name):String { - return prefix + name + "-name"; - } - public inline function getDescID(name):String { - return prefix + name + "-desc"; - } - - public function getName(name:String, ?args:Array):String { - return TU.translate(getNameID(name), args); - } - public function getDesc(name:String, ?args:Array):String { - return TU.translate(getDescID(name), args); - } - public function translate(name:String, ?args:Array):String { - return TU.translate(prefix + name, args); - } - - public function new(name:String, desc:String, ?prefix:String, ?options:Array) { - super(); - rawName = name; - rawDesc = desc; - this.prefix = prefix != null ? prefix : ""; - if (options != null) for(o in options) add(o); - } - - function set_rawName(v:String) { - rawName = v; - this.name = TU.exists(rawName) ? TU.translate(rawName) : rawName; - return v; - } - - function set_rawDesc(v:String) { - rawDesc = v; - this.desc = TU.exists(rawDesc) ? TU.translate(rawDesc) : rawDesc; - return v; - } - - public function reloadStrings() { - this.rawName = rawName; - this.rawDesc = rawDesc; - - for(o in members) { - o.reloadStrings(); - } - } - - public override function update(elapsed:Float) { - super.update(elapsed); - - var controls = PlayerSettings.solo.controls; - - changeSelection((controls.UP_P ? -1 : 0) + (controls.DOWN_P ? 1 : 0) - FlxG.mouse.wheel); - x = id * FlxG.width; - for(k=>option in members) { - if(option == null) continue; - - var y:Float = ((FlxG.height - optionHeight) / 2) + ((k - curSelected) * optionHeight); - - option.selected = false; - option.y = __firstFrame ? y : CoolUtil.fpsLerp(option.y, y, 0.25); - option.x = x + (-50 + (Math.abs(Math.cos((option.y + (optionHeight / 2) - (FlxG.camera.scroll.y + (FlxG.camera.height / 2))) / (FlxG.camera.height * 1.25) * Math.PI)) * 150)); - } - if (__firstFrame) { - __firstFrame = false; - return; - } - - if (members.length > 0) { - members[curSelected].selected = true; - if (controls.ACCEPT || (FlxG.mouse.justReleased && Main.timeSinceFocus > 0.25)) { - members[curSelected].onSelect(); - onSelect(members[curSelected]); - } - if (controls.LEFT_P) - members[curSelected].onChangeSelection(-1); - if (controls.RIGHT_P) - members[curSelected].onChangeSelection(1); - } - if (controls.BACK || FlxG.mouse.justReleasedRight) - close(); - } - - public function close() { - onClose(this); - } - - public function changeSelection(sel:Int, force:Bool = false) { - if (members.length <= 0 || (sel == 0 && !force)) return; - - CoolUtil.playMenuSFX(SCROLL); - curSelected = FlxMath.wrap(curSelected + sel, 0, members.length-1); - if (members[curSelected] == null) { - changeSelection(sel, true); - return; - } - members[curSelected].selected = true; - updateMenuDesc(); - } - - public function updateMenuDesc(?customTxt:String) { - if (parent == null || parent.treeParent == null) return; - parent.treeParent.updateDesc(customTxt != null ? customTxt : members[curSelected].desc); - } - - public dynamic function onClose(o:OptionsScreen) {} - public dynamic function onSelect(o:OptionType) { - if(o is funkin.options.type.RadioButton) { - var orb:funkin.options.type.RadioButton = cast o; - for(e in members) { - if(e is funkin.options.type.RadioButton) { - var rb:funkin.options.type.RadioButton = cast e; - if(rb.forId == orb.forId && rb != o) { - rb.checked = false; - } - } - } - } - } -} \ No newline at end of file diff --git a/source/funkin/options/OptionsTree.hx b/source/funkin/options/OptionsTree.hx deleted file mode 100644 index 9fd9e590..00000000 --- a/source/funkin/options/OptionsTree.hx +++ /dev/null @@ -1,89 +0,0 @@ -package funkin.options; - -class OptionsTree extends FlxTypedGroup { - public var lastMenu:OptionsScreen; - public var treeParent:TreeMenu; - public var wasClosing:Bool = false; - //public override function new() { - // super(); - //} - - public override function update(elapsed:Float) { - var last = members.last(); - if (last != null) - last.update(elapsed); - } - - public function updateAll(elapsed:Float) { - for (member in members) { - if(member == null) continue; - if(member.active && member.exists) { - member.update(elapsed); - } - } - } - - public override function draw() { - super.draw(); - if (lastMenu != null) { // manually draw lastMenu since it got removed from the members list - lastMenu.draw(); - } - } - - public function reloadStrings() { - for(o in members) { - o.reloadStrings(); - } - } - - public override function add(m:OptionsScreen) { - super.add(m); - setup(m); - clearLastMenu(); - wasClosing = false; - onMenuChange(); - return m; - } - public override function insert(pos:Int, m:OptionsScreen) { - var last = members.last(); - super.insert(pos, m); - setup(m); - if (last != members.last()) { - wasClosing = false; - onMenuChange(); - } - return m; - } - - public function setup(m:OptionsScreen) { - m.onClose = __subMenuClose; - m.id = members.indexOf(m); - m.parent = this; - m.update(0); - } - - function __subMenuClose(m:OptionsScreen) { - wasClosing = true; - clearLastMenu(); - lastMenu = m; - remove(m, true); - onMenuChange(); - onMenuClose(m); - } - - public function clearLastMenu() { - lastMenu = FlxDestroyUtil.destroy(lastMenu); - } - - public override function destroy() { - super.destroy(); - lastMenu = FlxDestroyUtil.destroy(lastMenu); - } - - public dynamic function onMenuChange() { - - } - - public dynamic function onMenuClose(m:OptionsScreen) { - } -} \ No newline at end of file diff --git a/source/funkin/options/TreeMenu.hx b/source/funkin/options/TreeMenu.hx index 41717e88..33ae47c6 100644 --- a/source/funkin/options/TreeMenu.hx +++ b/source/funkin/options/TreeMenu.hx @@ -1,148 +1,298 @@ package funkin.options; -import flixel.FlxState; import flixel.tweens.FlxTween; -import flixel.util.typeLimit.OneOfTwo; -import flixel.util.typeLimit.OneOfThree; +import flixel.util.FlxArrayUtil; +import flixel.util.FlxColor; +import flixel.util.FlxSignal; import funkin.backend.FunkinText; -import funkin.editors.ui.UIState; -import funkin.menus.MainMenuState; -import funkin.options.type.OptionType; -import funkin.options.type.TextOption; import funkin.backend.system.framerate.Framerate; +import funkin.editors.ui.UIState; + +interface ITreeOption { + var desc:String; + var selected:Bool; + + function changeSelection(change:Int):Void; + function select():Void; +} + +interface ITreeFloatOption extends ITreeOption { + function changeValue(change:Float):Void; +} class TreeMenu extends UIState { - public var main:OptionsScreen; - public var optionsTree:OptionsTree; - public var pathLabel:FunkinText; - public var pathDesc:FunkinText; - public var pathBG:FlxSprite; + public var onMenuClosed:FlxTypedSignalVoid> = new FlxTypedSignal(); + public var onMenuChanged:FlxTypedSignalVoid> = new FlxTypedSignal(); - public var screenScroll(default, set):Float; - private inline function set_screenScroll(val:Float) { - FlxG.camera.scroll.x = val * FlxG.camera.width; - return screenScroll = val; - } + public var tree(default, null):Array = []; + public var treeLength(default, null):Int = 0; + public var previousMenus:Array = []; + public var destroyMenus:Bool = true; - public static var lastState:Class = null; // Static for fixing the softlock bugs when resetting the state - Nex + public var exitCallback:TreeMenu->Void; - public function new(scriptsAllowed:Bool = true, ?scriptName:String) { - if (lastState == null) lastState = Type.getClass(FlxG.state); + public var titleLabel:FunkinText; + public var descLabel:FunkinText; + public var bgLabel:FlxSprite; + + var menuChangeTween:FlxTween; + var __drawer:TreeMenuDrawer; + var __treeCreated:Bool = false; + + public function new(?exitCallback:TreeMenu->Void, + scriptsAllowed:Bool = true, ?scriptName:String) + { super(scriptsAllowed, scriptName); + this.exitCallback = exitCallback; } - public override function createPost() { - if (main == null) main = new OptionsScreen("Fallback Treemenu", "Please set the \"main\" variable in your extended class before createPost", [new TextOption("Oops! No Options", "This doesn't look like it was supposed to happen...", () -> FlxG.resetState() ) ]); + override function create() { + super.create(); - screenScroll = -1; + bgLabel = new FlxSprite().makeGraphic(1, 1, FlxColor.BLACK); + bgLabel.alpha = 0.25; + bgLabel.scrollFactor.set(); - pathLabel = new FunkinText(4, 4, FlxG.width - 8, "> Tree Menu", 32, true); - pathLabel.borderSize = 1.25; - pathLabel.scrollFactor.set(); - - pathDesc = new FunkinText(4, pathLabel.y + pathLabel.height + 2, FlxG.width - 8, "Current Tree Menu Description", 16, true); - pathDesc.scrollFactor.set(); - - pathBG = new FlxSprite().makeGraphic(1, 1, 0xFF000000); - pathBG.scale.set(FlxG.width, pathDesc.y + pathDesc.height + 2); - pathBG.updateHitbox(); - pathBG.alpha = 0.25; - pathBG.scrollFactor.set(); - - optionsTree = new OptionsTree(); - optionsTree.onMenuChange = onMenuChange; - optionsTree.onMenuClose = onMenuClose; - optionsTree.treeParent = this; - optionsTree.add(main); - - - add(optionsTree); - add(pathBG); - add(pathLabel); - add(pathDesc); + titleLabel = new FunkinText(4, 4, FlxG.width - 8, 32); + titleLabel.borderSize = 1.25; + titleLabel.scrollFactor.set(); + descLabel = new FunkinText(4, 0, FlxG.width - 8, 16); + descLabel.scrollFactor.set(); + } + override function createPost() { super.createPost(); + + if ((treeLength = tree.length) != 0) { + updateMenuPositions(true); + tree.last().inputEnabled = true; + } + else + addMenu(new TreeMenuScreen("Fallback TreeMenuScreen", "Please insert menus into \"tree\" variable in your extended class in create or before createPost")); + + add(__drawer = new TreeMenuDrawer(this)); + add(bgLabel); + add(titleLabel); + add(descLabel); + + FlxG.camera.scroll.x = -FlxG.width; + menuChanged(); + + __treeCreated = true; } - public function onMenuChange() { - if (optionsTree.members.length <= 0) { - exit(); - } else { - if (menuChangeTween != null) - menuChangeTween.cancel(); + public function updateMenuPositions(fromIndex:Int = 0, cameraScroll = false) { + var last = tree[fromIndex - 1], menu:TreeMenuScreen; + while (fromIndex < treeLength) if ((menu = tree[fromIndex++]) != null) { + menu.x = last == null ? 0 : last.x + Math.max(FlxG.width, last.width); + (last = menu).parent = this; + } - menuChangeTween = FlxTween.num(screenScroll, Math.max(0, (optionsTree.members.length-1)), 1.5, {ease: menuTransitionEase, onComplete: function(t) { - optionsTree.clearLastMenu(); - menuChangeTween = null; - }}, (val:Float) -> screenScroll = val); + if (cameraScroll && last != null) { + if (menuChangeTween != null && menuChangeTween.active) menuChangeTween.cancel(); + FlxG.camera.scroll.x = last.x; - reloadLabels(); + for (menu in tree) if (menu != null) menu.transitioning = false; + for (menu in previousMenus) if (menu != null) menu.transitioning = false; } } - public function onMenuClose(m:OptionsScreen) { - CoolUtil.playMenuSFX(CANCEL); + public function updateLabels() { + var s = "", last = tree.last(); + for (menu in tree) if (menu != null) s += menu.name + " > "; + + titleLabel.text = s; + descLabel.y = titleLabel.y + titleLabel.height + 2; + + updateDesc(); } - public function reloadLabels() { - var t = ""; - for(o in optionsTree.members) - t += '${o.name} > '; - pathLabel.text = t; + public function updateDesc(?customText:String) { + var last = tree.last(); - var idk:OptionsScreen = optionsTree.members.last(); - if (idk.members.length > 0) updateDesc(idk.members[idk.curSelected].desc); + descLabel.text = last.desc; + if (customText != null && customText.length > 0) descLabel.text += "\n" + customText; + else if (last.curSelected >= 0 && last.curSelected < last.length && last.members[last.curSelected] is ITreeOption) + descLabel.text += "\n" + cast(last.members[last.curSelected], ITreeOption).desc; + + bgLabel.scale.set(FlxG.width, descLabel.y + descLabel.height + 2); + bgLabel.updateHitbox(); + + Framerate.offset.y = bgLabel.height + 2; } - public function updateDesc(moreTxt:String = '') { - pathDesc.text = optionsTree.members.last().desc; - if (moreTxt != null && moreTxt.length > 0) pathDesc.text += '\n' + moreTxt; - pathBG.scale.set(FlxG.width, pathDesc.y + pathDesc.height + 2); - pathBG.updateHitbox(); + public function addMenu(menu:TreeMenuScreen):TreeMenuScreen { + if (menu == null) return null; + if (tree.indexOf(menu) != -1) return menu; + + tree.push(menu); + if (!__treeCreated) return menu; + + menu.parent = this; + treeLength++; + + var prev = tree[treeLength - 2]; + menu.x = prev == null ? 0 : prev.x + Math.max(FlxG.width, prev.width); + + destroyPreviousMenus(); + menuChanged(); + menu.inputEnabled = true; + if (prev != null) prev.inputEnabled = false; + + return menu; } - public function exit() { - FlxG.switchState((lastState != null) ? Type.createInstance(lastState, []) : new MainMenuState()); - lastState = null; + public function insertMenu(position:Int, menu:TreeMenuScreen):TreeMenuScreen { + if (menu == null) return null; + if (tree.indexOf(menu) != -1) return menu; + + if (position < 0) position = treeLength - ((-position - 1) % treeLength); + + tree.insert(position, menu); + if (!__treeCreated) return menu; + + menu.parent = this; + + var lastChanged = position >= treeLength++; + updateMenuPositions(position, !lastChanged); + + if (lastChanged) { + destroyPreviousMenus(); + menuChanged(); + menu.inputEnabled = true; + if (treeLength > 0) tree[treeLength - 2].inputEnabled = false; + } + + return menu; } - var menuChangeTween:FlxTween; - public override function update(elapsed:Float) { + public function popMenu():TreeMenuScreen + return removeMenuPosition(treeLength - 1); + + public function removeMenu(menu:TreeMenuScreen):TreeMenuScreen + return if (menu == null) null; else removeMenuPosition(tree.indexOf(menu)); + + public function removeMenuPosition(position:Int):TreeMenuScreen { + if (position < 0 || position >= treeLength || treeLength == 0) return null; + + tree[position] = tree[tree.length - 1]; + var menu = tree.pop(); + menu.parent = null; + + if (!__treeCreated) return menu; + + previousMenus.push(menu); + if (position == --treeLength) { + menuChanged(); + menu.inputEnabled = false; + if (treeLength > 0) tree[treeLength - 1].inputEnabled = true; + } + else { + updateMenuPositions(position, true); + updateLabels(); + } + + onMenuClosed.dispatch(menu); + return menu; + } + + public function menuChanged() { + if (treeLength == 0) exit(); + else { + updateLabels(); + + if (menuChangeTween != null && menuChangeTween.active) menuChangeTween.cancel(); + menuChangeTween = FlxTween.tween(FlxG.camera.scroll, {x: tree.last().x}, 1.5, {ease: menuTransitionEase, onComplete: (t) -> { + for (menu in tree) if (menu != null) menu.transitioning = false; + for (menu in previousMenus) if (menu != null) menu.transitioning = false; + }}); + + for (menu in tree) if (menu != null) menu.transitioning = true; + } + + for (menu in previousMenus) if (menu != null) menu.transitioning = true; + } + + override function update(elapsed:Float) { super.update(elapsed); - Framerate.offset.y = pathBG.height; - // in case path gets so long it goes offscreen - pathLabel.x = lerp(pathLabel.x, Math.max(0, FlxG.width - 4 - pathLabel.width), 0.125); + var i = 0, menu:TreeMenuScreen; + while (i < treeLength) { + if ((menu = tree[i++]) == null || !menu.active || !menu.exists) continue; + if (i == treeLength || menu.persistentUpdate || menu.transitioning) menu.update(elapsed); + } + + i = previousMenus.length; + while (i-- > 0) { + if ((menu = previousMenus[i]).transitioning) menu.update(elapsed); + else { + menu.destroy(); + FlxArrayUtil.swapAndPop(previousMenus, i); + } + } + + // in case path gets so long it goes offscreen, ALTHOUGH this nevers happens anyway since we have set a expected width to the label. + //titleLabel.x = lerp(titleLabel.x, Math.max(0, FlxG.width - 4 - titleLabel.width), 0.125); } - public override function onResize(width:Int, height:Int) { + override function destroy() { + super.destroy(); + destroyPreviousMenus(); + } + + override function onResize(width:Int, height:Int) { super.onResize(width, height); if (!UIState.resolutionAware) return; if (width < FlxG.initialWidth || height < FlxG.initialHeight) { - width = FlxG.initialWidth; height = FlxG.initialHeight; + width = FlxG.initialWidth; + height = FlxG.initialHeight; } - screenScroll = screenScroll; // Updating the cam position - Nex - - if (pathDesc != null && pathLabel != null) - pathDesc.width = pathLabel.width = width - 8; + updateMenuPositions(true); + descLabel.width = titleLabel.width = (bgLabel.scale.x = width) - 8; + bgLabel.updateHitbox(); } - public static inline function menuTransitionEase(e:Float) - return FlxEase.quintInOut(FlxEase.cubeOut(e)); + public function reloadStrings() { + for (menu in tree) if (menu != null) menu.reloadStrings(); + updateLabels(); + } + + public function destroyPreviousMenus() { + for (menu in previousMenus) menu.destroy(); + previousMenus.resize(0); + } + + public function exit() { + if (exitCallback != null) return exitCallback(this); + + FlxG.switchState(new funkin.menus.MainMenuState()); + } + + public function updateAll(elapsed:Float) { + for (menu in tree) if (menu != null && menu.active && menu.exists) menu.update(elapsed); + } + + public dynamic function menuTransitionEase(e:Float) return FlxEase.quintInOut(FlxEase.cubeOut(e)); } -typedef OptionCategory = { - var name:String; - var desc:String; - var state:OneOfThree, (name:String, desc:String) -> OptionsScreen>; - var ?substate:OneOfThree, (name:String, desc:String) -> MusicBeatSubstate>; - var ?suffix:String; -} +final class TreeMenuDrawer extends FlxBasic { + public var parent:TreeMenu; + public function new(parent:TreeMenu) { + super(); + this.parent = parent; + } -typedef OptionTypeDef = { - var type:Class; - var args:Array; -} + override function draw() { + var i = 0, menu:TreeMenuScreen; + while (i < parent.treeLength) { + if ((menu = parent.tree[i++]) == null || !menu.active || !menu.exists) continue; + if (i == parent.treeLength || menu.persistentUpdate || menu.transitioning) menu.draw(); + } + + i = parent.previousMenus.length; + while (i-- > 0) if (parent.previousMenus[i] != null) parent.previousMenus[i].draw(); + } +} \ No newline at end of file diff --git a/source/funkin/options/TreeMenuScreen.hx b/source/funkin/options/TreeMenuScreen.hx new file mode 100644 index 00000000..33883fc0 --- /dev/null +++ b/source/funkin/options/TreeMenuScreen.hx @@ -0,0 +1,192 @@ +package funkin.options; + +import flixel.group.FlxSpriteGroup; +import flixel.util.FlxSignal; +import funkin.backend.system.Controls; +import funkin.backend.TurboControls; +import funkin.options.TreeMenu.ITreeOption; +import funkin.options.TreeMenu.ITreeFloatOption; +import funkin.options.type.OptionType; +import funkin.options.type.Separator; + +class TreeMenuScreen extends FlxSpriteGroup { + public var persistentUpdate:Bool = false; + public var persistentDraw:Bool = false; + + public var onClose:FlxSignal = new FlxSignal(); + + public var parent:TreeMenu; + public var transitioning:Bool = false; + public var inputEnabled:Bool = false; + public var curSelected:Int = 0; + + public var name:String; + public var desc:String; + /** + * The prefix to add to the translations ids. + **/ + public var prefix:String = ""; + + private var rawName(default, set):String; + private var rawDesc(default, set):String; + + function set_rawName(v:String) { + rawName = v; + name = TU.exists(v) ? TU.translate(v) : v; + return v; + } + + function set_rawDesc(v:String) { + rawDesc = v; + desc = TU.exists(v) ? TU.translate(v) : v; + return v; + } + + public inline function getNameID(name):String return prefix + name + "-name"; + public inline function getDescID(name):String return prefix + name + "-desc"; + public inline function getID(name):String return prefix + name; + public function translate(name:String, ?args:Array):String return TU.translate(getID(name), args); + + public var controls(get, never):Controls; + inline function get_controls():Controls return PlayerSettings.solo.controls; + + var leftTurboControl:TurboControls = new TurboControls([Control.LEFT], 0.2, 1 / 48); + var rightTurboControl:TurboControls = new TurboControls([Control.RIGHT], 0.2, 1 / 48); + var upTurboControl:TurboControls = new TurboControls([Control.UP]); + var downTurboControl:TurboControls = new TurboControls([Control.DOWN]); + var turboBasics:Array; + + var curOption:ITreeOption; + var curFloatOption:ITreeFloatOption; + var __firstFrame:Bool = true; + + public function new(name:String, desc:String, ?prefix:String, ?objects:Array) { + super(); + this.prefix = prefix; + rawName = name; + rawDesc = desc; + + turboBasics = [leftTurboControl, rightTurboControl, upTurboControl, downTurboControl]; + + if (objects != null) for (object in objects) add(object); + } + + public function reloadStrings() { + rawName = rawName; + rawDesc = rawDesc; + + for (object in members) if (object != null && object is OptionType) cast(object, OptionType).reloadStrings(); + } + + override function update(elapsed:Float) { + super.update(elapsed); + + if (__firstFrame) { + __firstFrame = false; + if (members[curSelected] is ITreeOption) { + (curOption = cast members[curSelected]).selected = true; + if (curOption is ITreeFloatOption) curFloatOption = cast curOption; + } + updateItems(true); + return; + } + + if (inputEnabled) { + for (basic in turboBasics) basic.update(elapsed); + + var change = (upTurboControl.activated ? -1 : 0) + (downTurboControl.activated ? 1 : 0) - FlxG.mouse.wheel, mouseControl = false; + if (FlxG.mouse.justPressed) { + for (i in CoolUtil.maxInt(curSelected - 3, 0)...CoolUtil.minInt(curSelected + 4, length)) + if (i != curSelected && members[i] != null && mouseOverlaps(members[i])) { + change = i - curSelected; + mouseControl = true; + break; + } + } + changeSelection(change); + + if (length > 0 && curOption != null) { + if (controls.ACCEPT || (!mouseControl && FlxG.mouse.justPressed && mouseOverlaps(members[curSelected]))) curOption.select(); + if (curFloatOption != null) { + if (controls.LEFT) curFloatOption.changeValue(-elapsed); + if (controls.RIGHT) curFloatOption.changeValue(elapsed); + } + else { + if (leftTurboControl.activated) curOption.changeSelection(-1); + if (rightTurboControl.activated) curOption.changeSelection(1); + } + } + + if (controls.BACK || (FlxG.mouse.justPressedRight && Main.timeSinceFocus > 0.3)) close(); + } + + updateItems(); + } + + public function updateItems(force = false) { + var r = force ? 1 : 0.25, initY = FlxG.height * 0.5; + var i = curSelected, y = initY, object:FlxSprite = null, itemHeight:Float = 0; + + inline function updateItem() { + object.y = CoolUtil.fpsLerp(object.y, y - (itemHeight = object.height) * 0.5, r); + object.x = x + 100 - Math.pow(Math.abs((object.y - (FlxG.height - itemHeight) * 0.5) / itemHeight / FlxG.height * FlxG.initialHeight), 1.6) * 15; + } + + while (i < length) if ((object = members[i++]) != null) { + updateItem(); + y += itemHeight; + } + + y = initY; + i = curSelected; + while (i-- > 0) if ((object = members[i]) != null) { + y -= itemHeight; + updateItem(); + } + } + + public function close() { + onClose.dispatch(); + + if (curOption != null) curOption.selected = false; + + if (parent == null) return destroy(); + else parent.removeMenu(this); + + CoolUtil.playMenuSFX(CANCEL).persist = true; + } + + public function changeSelection(change:Int, force:Bool = false) { + if (length == 0 || (change == 0 && !force)) return; + + var prevSelect = curSelected = FlxMath.wrap(curSelected + change, 0, members.length - 1); + while (members[curSelected] is Separator) + if ((curSelected = FlxMath.wrap(curSelected + (change > 0 ? 1 : -1), 0, members.length - 1)) == prevSelect) break; + + if (curOption != null) curOption.selected = false; + if (members[curSelected] is ITreeOption) { + (curOption = cast members[curSelected]).selected = true; + if (curOption is ITreeFloatOption) curFloatOption = cast curOption; + else curFloatOption = null; + } + else { + curOption = null; + curFloatOption = null; + } + updateMenuDesc(); + + CoolUtil.playMenuSFX(SCROLL); + } + + public function updateMenuDesc(?customTxt:String) { + if (parent != null) parent.updateDesc(customTxt); + } + + function mouseOverlaps(sprite:FlxSprite):Bool + return sprite.overlapsPoint(FlxG.mouse.getPosition(@:privateAccess flixel.input.FlxPointer._cachedPoint), true); + + override function destroy() { + super.destroy(); + for (basic in turboBasics) basic.destroy(); + } +} \ No newline at end of file diff --git a/source/funkin/options/categories/AppearanceOptions.hx b/source/funkin/options/categories/AppearanceOptions.hx index fd364bdd..0f55e496 100644 --- a/source/funkin/options/categories/AppearanceOptions.hx +++ b/source/funkin/options/categories/AppearanceOptions.hx @@ -1,55 +1,63 @@ package funkin.options.categories; -class AppearanceOptions extends OptionsScreen { - public override function new(title:String, desc:String) { - super(title, desc, "AppearanceOptions."); - add(new NumOption( - getName("framerate"), - getDesc("framerate"), - 30, // minimum - 240, // maximum - 10, // change - "framerate", // save name or smth - __changeFPS)); // callback - add(new Checkbox( - getName("antialiasing"), - getDesc("antialiasing"), - "antialiasing")); - add(new Checkbox( - getName("colorHealthBar"), - getDesc("colorHealthBar"), - "colorHealthBar")); - add(new Checkbox( - getName("week6PixelPerfect"), - getDesc("week6PixelPerfect"), - "week6PixelPerfect")); - add(new Checkbox( - getName("gameplayShaders"), - getDesc("gameplayShaders"), - "gameplayShaders")); - add(new Checkbox( - getName("flashingMenu"), - getDesc("flashingMenu"), - "flashingMenu")); - add(new Checkbox( - getName("lowMemoryMode"), - getDesc("lowMemoryMode"), - "lowMemoryMode")); - #if sys - if (!Main.forceGPUOnlyBitmapsOff) { - add(new Checkbox( - getName("gpuOnlyBitmaps"), - getDesc("gpuOnlyBitmaps"), - "gpuOnlyBitmaps")); - } - #end +class AppearanceOptions extends TreeMenuScreen { + public function new() { + super('optionsTree.appearance-name', 'optionsTree.appearance-desc', 'AppearanceOptions.'); + + add(new NumOption(getNameID('framerate'), getDescID('framerate'), + 30, 240, 1, + 'framerate', __changeFPS + )); + add(new Checkbox(getNameID('flashingMenu'), getDescID('flashingMenu'), 'flashingMenu')); + add(new Checkbox(getNameID('colorHealthBar'), getDescID('colorHealthBar'), 'colorHealthBar')); + add(new Checkbox(getNameID('week6PixelPerfect'), getDescID('week6PixelPerfect'), 'week6PixelPerfect')); + + add(new Separator()); + add(new TextOption('optionsMenu.advanced', 'optionsTree.appearance.advanced-desc', ' >', () -> + parent.addMenu(new AdvancedAppearanceOptions()))); } - private function __changeFPS(change:Float) { - // if statement cause of the flixel warning - if(FlxG.updateFramerate < Std.int(change)) - FlxG.drawFramerate = FlxG.updateFramerate = Std.int(change); - else - FlxG.updateFramerate = FlxG.drawFramerate = Std.int(change); + private function __changeFPS(value:Float) { + var framerate = Math.floor(value); + if (FlxG.updateFramerate < framerate) FlxG.drawFramerate = FlxG.updateFramerate = framerate; + else FlxG.updateFramerate = FlxG.drawFramerate = framerate; + } +} + +class AdvancedAppearanceOptions extends TreeMenuScreen { + var qualityOptions:Array = []; + + public function new() { + super('optionsMenu.advanced', 'optionsTree.appearance.advanced-desc', 'AppearanceOptions.Advanced.'); + + add(new ArrayOption(getNameID('quality'), getDescID('quality'), + [0, 1, 2], [getID('quality-low'), getID('quality-high'), getID('quality-custom')], + 'quality', __changeQuality, null + )); + + for (option in (qualityOptions = [ + new Checkbox(getNameID('antialiasing'), getDescID('antialiasing'), 'antialiasing', __changeAntialiasing), + new Checkbox(getNameID('lowMemoryMode'), getDescID('lowMemoryMode'), 'lowMemoryMode'), + new Checkbox(getNameID('gameplayShaders'), getDescID('gameplayShaders'), 'gameplayShaders') + ])) + add(option); + + add(new Checkbox(getNameID('gpuOnlyBitmaps'), getDescID('gpuOnlyBitmaps'), 'gpuOnlyBitmaps')); + + updateQualityOptions(); + } + + private function updateQualityOptions() { + for (option in qualityOptions) option.locked = Options.quality != 2; + } + + private function __changeQuality(value:Dynamic) { + var antialiasing = value == 0 ? false : (value == 1 ? true : Options.antialiasing); + FlxG.game.stage.quality = (FlxG.enableAntialiasing = antialiasing) ? BEST : LOW; + updateQualityOptions(); + } + + private function __changeAntialiasing() { + FlxG.game.stage.quality = (FlxG.enableAntialiasing = Options.antialiasing) ? BEST : LOW; } } \ No newline at end of file diff --git a/source/funkin/options/categories/DebugOptions.hx b/source/funkin/options/categories/DebugOptions.hx index 16676890..b0ff8e71 100644 --- a/source/funkin/options/categories/DebugOptions.hx +++ b/source/funkin/options/categories/DebugOptions.hx @@ -1,62 +1,22 @@ package funkin.options.categories; -import funkin.backend.utils.NativeAPI; +class DebugOptions extends TreeMenuScreen { + public function new() { + super('DebugOptions.title', 'DebugOptions.desc', 'DebugOptions.'); -class DebugOptions extends OptionsScreen { - public override function new(title:String, desc:String) { - super(title, desc, "DebugOptions."); - - add(new TextOption( - getName("showConsole"), - getDesc("showConsole"), - function() NativeAPI.allocConsole())); - add(new Checkbox( - getName("editorsResizable"), - getDesc("editorsResizable"), - "editorsResizable")); - add(new Checkbox( - getName("bypassEditorsResize"), - getDesc("bypassEditorsResize"), - "bypassEditorsResize")); - add(new Checkbox( - getName("editorSFX"), - getDesc("editorSFX"), - "editorSFX")); - add(new Checkbox( - getName("editorCharterPrettyPrint"), - getDesc("editorCharterPrettyPrint"), - "editorCharterPrettyPrint")); - add(new Checkbox( - getName("editorCharacterPrettyPrint"), - getDesc("editorCharacterPrettyPrint"), - "editorCharacterPrettyPrint")); - add(new Checkbox( - getName("editorStagePrettyPrint"), - getDesc("editorStagePrettyPrint"), - "editorStagePrettyPrint")); - add(new Checkbox( - getName("intensiveBlur"), - getDesc("intensiveBlur"), - "intensiveBlur")); - add(new Checkbox( - getName("charterAutoSaves"), - getDesc("charterAutoSaves"), - "charterAutoSaves")); - add(new NumOption( - getName("charterAutoSaveTime"), - getDesc("charterAutoSaveTime"), - 60, 60*10, 30, - "charterAutoSaveTime" - )); - add(new NumOption( - getName("charterAutoSaveWarningTime"), - getDesc("charterAutoSaveWarningTime"), - 0, 15, 1, - "charterAutoSaveWarningTime" - )); - add(new Checkbox( - getName("charterAutoSavesSeparateFolder"), - getDesc("charterAutoSavesSeparateFolder"), - "charterAutoSavesSeparateFolder")); + #if windows + add(new TextOption(getNameID("showConsole"), getDescID("showConsole"), () -> funkin.backend.utils.NativeAPI.allocConsole())); + #end + add(new Checkbox(getNameID("editorsResizable"), getDescID("editorsResizable"), "editorsResizable")); + add(new Checkbox(getNameID("bypassEditorsResize"), getDescID("bypassEditorsResize"), "bypassEditorsResize")); + add(new Checkbox(getNameID("editorSFX"), getDescID("editorSFX"), "editorSFX")); + add(new Checkbox(getNameID("editorCharterPrettyPrint"), getDescID("editorCharterPrettyPrint"), "editorCharterPrettyPrint")); + add(new Checkbox(getNameID("editorCharacterPrettyPrint"), getDescID("editorCharacterPrettyPrint"), "editorCharacterPrettyPrint")); + add(new Checkbox(getNameID("editorStagePrettyPrint"), getDescID("editorStagePrettyPrint"), "editorStagePrettyPrint")); + add(new Checkbox(getNameID("intensiveBlur"), getDescID("intensiveBlur"), "intensiveBlur")); + add(new Checkbox(getNameID("charterAutoSaves"), getDescID("charterAutoSaves"), "charterAutoSaves")); + add(new NumOption(getNameID("charterAutoSaveTime"), getDescID("charterAutoSaveTime"), 60, 60*10, 1, "charterAutoSaveTime")); + add(new NumOption(getNameID("charterAutoSaveWarningTime"), getDescID("charterAutoSaveWarningTime"), 0, 15, 1, "charterAutoSaveWarningTime")); + add(new Checkbox(getNameID("charterAutoSavesSeparateFolder"), getDescID("charterAutoSavesSeparateFolder"), "charterAutoSavesSeparateFolder")); } } \ No newline at end of file diff --git a/source/funkin/options/categories/GameplayOptions.hx b/source/funkin/options/categories/GameplayOptions.hx index 4e1f8a7e..7d66f351 100644 --- a/source/funkin/options/categories/GameplayOptions.hx +++ b/source/funkin/options/categories/GameplayOptions.hx @@ -3,64 +3,39 @@ package funkin.options.categories; import flixel.util.FlxTimer; import funkin.backend.system.Conductor; -class GameplayOptions extends OptionsScreen { +class GameplayOptions extends TreeMenuScreen { var __metronome = FlxG.sound.load(Paths.sound('editors/charter/metronome')); - var offsetSetting:NumOption; - public override function new(title:String, desc:String) { - super(title, desc, "GameplayOptions."); - add(new Checkbox( - getName("downscroll"), - getDesc("downscroll"), - "downscroll")); - add(new Checkbox( - getName("ghostTapping"), - getDesc("ghostTapping"), - "ghostTapping")); - add(new Checkbox( - getName("naughtyness"), - getDesc("naughtyness"), - "naughtyness")); - add(new Checkbox( - getName("camZoomOnBeat"), - getDesc("camZoomOnBeat"), - "camZoomOnBeat")); - add(new Checkbox( - getName("autoPause"), - getDesc("autoPause"), - "autoPause")); - add(new MeterOption( - getName("volumeSFX"), - getDesc("volumeSFX"), - 0, // minimum - 1, // maximum - 0.1, // change - "volumeSFX")); - add(offsetSetting = new NumOption( - getName("songOffset"), - getDesc("songOffset"), - -999, // minimum - 999, // maximum - 1, // change - "songOffset", // save name or smth - __changeOffset)); // callback + public function new() { + super('optionsTree.gameplay-name', 'optionsTree.gameplay-desc', 'GameplayOptions.'); + + add(new Checkbox(getNameID('downscroll'), getDescID('downscroll'), 'downscroll')); + add(new Checkbox(getNameID('ghostTapping'), getDescID('ghostTapping'), 'ghostTapping')); + add(new Checkbox(getNameID('naughtyness'), getDescID('naughtyness'), 'naughtyness')); + add(new Checkbox(getNameID('camZoomOnBeat'), getDescID('camZoomOnBeat'), 'camZoomOnBeat')); + add(new Checkbox(getNameID('autoPause'), getDescID('autoPause'), 'autoPause', __changeAutoPause)); + add(offsetSetting = new NumOption(getNameID('songOffset'), getDescID('songOffset'), -999, 999, 1, 'songOffset', __changeOffset)); + add(new SliderOption(getNameID('volumeMusic'), getDescID('volumeMusic'), 0, 1, 1, 5, 'volumeMusic', -1, __changeVolumeMusic)); + add(new SliderOption(getNameID('volumeSFX'), getDescID('volumeSFX'), 0, 1, 1, 5, 'volumeSFX')); + + add(new Separator()); add(new TextOption('optionsMenu.advanced', 'optionsTree.gameplay.advanced-desc', ' >', () -> - parent.add(new AdvancedGameplayOptions('optionsMenu.advanced', 'optionsTree.gameplay.advanced-desc')))); + parent.addMenu(new AdvancedGameplayOptions()))); } - private function __changeOffset(offset) - Conductor.songOffset = offset; + private function __changeAutoPause() FlxG.autoPause = Options.autoPause; + private function __changeOffset(offset:Float) Conductor.songOffset = offset; + private function __changeVolumeMusic(value:Float) FlxG.sound.defaultMusicGroup.volume = value; var __lastBeat:Int = 0; var __lastSongBeat:Int = 0; - override function update(elapsed) { + override function update(elapsed:Float) { super.update(elapsed); - FlxG.camera.zoom = CoolUtil.fpsLerp(FlxG.camera.zoom, 1, 0.04); + FlxG.camera.zoom = CoolUtil.fpsLerp(FlxG.camera.zoom, 1, 0.1); if (offsetSetting.selected) { - FlxG.sound.music.volume = 0.5; if (__lastBeat != Conductor.curBeat) { FlxG.camera.zoom += 0.03; __lastBeat = Conductor.curBeat; @@ -72,27 +47,27 @@ class GameplayOptions extends OptionsScreen { __lastSongBeat = beat; } } - else - FlxG.sound.music.volume = 1; + } + + override function changeSelection(change:Int, force:Bool = false) { + super.changeSelection(change, force); + if (offsetSetting.selected) FlxG.sound.music.volume = 0.5; + else FlxG.sound.music.volume = 1; } override function close() { + super.close(); + FlxG.camera.zoom = 1; FlxG.sound.music.volume = 1; - super.close(); } } -class AdvancedGameplayOptions extends OptionsScreen { - public override function new(title:String, desc:String) { - super(title, desc, "GameplayOptions.Advanced."); - add(new Checkbox( - getName("streamedMusic"), - getDesc("streamedMusic"), - "streamedMusic")); - add(new Checkbox( - getName("streamedVocals"), - getDesc("streamedVocals"), - "streamedVocals")); +class AdvancedGameplayOptions extends TreeMenuScreen { + public function new() { + super('optionsMenu.advanced', 'optionsTree.gameplay.advanced-desc', 'GameplayOptions.Advanced.'); + + add(new Checkbox(getNameID('streamedMusic'), getDescID('streamedMusic'), 'streamedMusic')); + add(new Checkbox(getNameID('streamedVocals'), getDescID('streamedVocals'), 'streamedVocals')); } -} +} \ No newline at end of file diff --git a/source/funkin/options/categories/LanguageOptions.hx b/source/funkin/options/categories/LanguageOptions.hx index b3e64433..878e6741 100644 --- a/source/funkin/options/categories/LanguageOptions.hx +++ b/source/funkin/options/categories/LanguageOptions.hx @@ -3,17 +3,13 @@ package funkin.options.categories; class LanguageRadio extends RadioButton { public var langID:String; - public function new(name:String, langID:String) { + public function new(screen:TreeMenuScreen, name:String, langID:String) { + super(screen, name, "LanguageOptions.language-desc", null, langID, null, "languageSelector"); this.langID = langID; - super(name, "LanguageOptions.language-desc", null, langID, null, "languageSelector"); checked = langID == TU.curLanguage; } - override function reloadStrings() { - super.reloadStrings(); - } - override function set_rawDesc(v:String) { rawDesc = v; var config = TU.getConfig(langID); @@ -23,28 +19,25 @@ class LanguageRadio extends RadioButton { return v; } - override dynamic function onSet(value:Dynamic) { + override function select() { + var prev = checked; + super.select(); + if (prev == checked) return; + TranslationUtil.setLanguage(value); - if(FlxG.state is OptionsMenu) { - var menu:OptionsMenu = cast FlxG.state; - menu.reloadStrings(); - trace("Reloading strings"); - } + if (screen.parent == null) screen.reloadStrings(); + else screen.parent.reloadStrings(); } } -class LanguageOptions extends OptionsScreen { - public override function new(title:String, desc:String) { - super(title, desc, "LanguageOptions."); +class LanguageOptions extends TreeMenuScreen { + public override function new() { + super('optionsTree.language-name', 'optionsTree.language-desc', 'LanguageOptions.'); - var langArray:Array = TranslationUtil.foundLanguages; - - for(lang in langArray) { + for (lang in TranslationUtil.foundLanguages) { var split = lang.split("/"); - var langId = split.first(); - var langName = split.last(); - var radio = new LanguageRadio(langName, langId); - add(radio); + var langId = split.first(), langName = split.last(); + add(new LanguageRadio(this, langName, langId)); } } -} +} \ No newline at end of file diff --git a/source/funkin/options/categories/MiscOptions.hx b/source/funkin/options/categories/MiscOptions.hx index 95b55aef..e72bafc8 100644 --- a/source/funkin/options/categories/MiscOptions.hx +++ b/source/funkin/options/categories/MiscOptions.hx @@ -1,35 +1,26 @@ package funkin.options.categories; -class MiscOptions extends OptionsScreen { - public override function new(title:String, desc:String) { - super(title, desc, "MiscOptions."); - add(new Checkbox( - getName("devMode"), - getDesc("devMode"), - "devMode")); +class MiscOptions extends TreeMenuScreen { + public function new() { + super('optionsTree.miscellaneous-name', 'optionsTree.miscellaneous-desc', 'MiscOptions.'); + + add(new Checkbox(getNameID('devMode'), getDescID('devMode'), 'devMode')); + add(new Checkbox(getNameID('allowConfigWarning'), getDescID('allowConfigWarning'), 'allowConfigWarning')); #if UPDATE_CHECKING - add(new Checkbox( - getName("betaUpdates"), - getDesc("betaUpdates"), - "betaUpdates")); - add(new TextOption( - getName("checkForUpdates"), - getDesc("checkForUpdates"), - function() { - var report = funkin.backend.system.updating.UpdateUtil.checkForUpdates(); - if (report.newUpdate) { - FlxG.switchState(new funkin.backend.system.updating.UpdateAvailableScreen(report)); - } else { - CoolUtil.playMenuSFX(CANCEL); - updateDescText(TU.translate(prefix + "checkForUpdates-noUpdateFound")); - } + add(new Checkbox(getNameID('betaUpdates'), getDescID('betaUpdates'), 'betaUpdates')); + add(new TextOption(getNameID('checkForUpdates'), getDescID('checkForUpdates'), () -> { + var report = funkin.backend.system.updating.UpdateUtil.checkForUpdates(); + if (report.newUpdate) FlxG.switchState(new funkin.backend.system.updating.UpdateAvailableScreen(report)); + else { + CoolUtil.playMenuSFX(CANCEL); + updateDescText(translate('checkForUpdates-noUpdateFound')); + } })); #end - add(new TextOption( - getName("resetSaveData"), - getDesc("resetSaveData"), - function() { - // TODO: SAVE DATA RESETTING + + add(new Separator()); + add(new TextOption(getNameID('resetSaveData'), getDescID('resetSaveData'), () -> { + // TODO: SAVE DATA RESETTING })); } -} +} \ No newline at end of file diff --git a/source/funkin/options/categories/import.hx b/source/funkin/options/categories/import.hx index 49bf382e..cb4b5c3a 100644 --- a/source/funkin/options/categories/import.hx +++ b/source/funkin/options/categories/import.hx @@ -1,5 +1,7 @@ #if !macro package funkin.options.categories; +import funkin.options.TreeMenu; +import funkin.options.TreeMenuScreen; import funkin.options.type.*; #end \ No newline at end of file diff --git a/source/funkin/options/type/ArrayOption.hx b/source/funkin/options/type/ArrayOption.hx index 885d0335..52c33904 100644 --- a/source/funkin/options/type/ArrayOption.hx +++ b/source/funkin/options/type/ArrayOption.hx @@ -1,88 +1,64 @@ package funkin.options.type; -/** - * Option type that allows you to select an array of options. -**/ -class ArrayOption extends OptionType { - public var selectCallback:String->Void; - - private var __text:Alphabet; - private var __selectiontext:Alphabet; +class ArrayOption extends TextOption { + public var changedCallback:String->Void; public var options:Array; public var displayOptions:Array; - public var currentSelection:Int; - var optionName:String; - public var parent:Dynamic; + public var optionName:String; - private var rawText(default, set):String; + var __selectionText:Alphabet; - public var text(get, set):String; - private function get_text() {return __text.text;} - private function set_text(v:String) {return __text.text = v;} - - public function new(text:String, desc:String, options:Array, displayOptions:Array, optionName:String, ?selectCallback:String->Void = null, ?parent:Dynamic) { - super(desc); - this.selectCallback = selectCallback; - this.displayOptions = displayOptions; - this.options = options; - if (parent == null) - parent = Options; - - this.parent = parent; - - var fieldValue = Reflect.field(parent, optionName); - if(fieldValue != null) - this.currentSelection = CoolUtil.maxInt(0, options.indexOf(fieldValue)); - - this.optionName = optionName; - - add(__text = new Alphabet(100, 20, text, "bold")); - add(__selectiontext = new Alphabet(__text.width + 120, 20, formatTextOption(), "normal")); - - rawText = text; - } - - override function reloadStrings() { - super.reloadStrings(); - this.rawText = rawText; - } - - function set_rawText(v:String) { - rawText = v; - __text.text = TU.exists(rawText) ? TU.translate(rawText) : rawText; + override function set_text(v:String) { + super.set_text(v); + __selectionText.x = __text.x + __text.width + 12; return v; } - public override function draw() { - super.draw(); + public function new(text:String, desc:String, options:Array, displayOptions:Array, ?optionName:String, ?changedCallback:Dynamic->Void = null, ?parent:Dynamic) { + this.changedCallback = changedCallback; + this.displayOptions = displayOptions; + this.options = options; + this.optionName = optionName; + this.parent = parent = parent != null ? parent : Options; + + var fieldValue = Reflect.field(parent, optionName); + if (fieldValue != null) currentSelection = CoolUtil.maxInt(0, options.indexOf(fieldValue)); + + __selectionText = new Alphabet(0, 20, formatTextOption(), 'bold'); + super(text, desc); + add(__selectionText); } - public override function onChangeSelection(change:Float):Void - { - if(currentSelection <= 0 && change == -1 || currentSelection >= options.length - 1 && change == 1) return; - currentSelection += Math.round(change); - __selectiontext.text = formatTextOption(); - Reflect.setField(parent, optionName, options[currentSelection]); - if(selectCallback != null) - selectCallback(options[currentSelection]); + override function reloadStrings() { + __selectionText.text = formatTextOption(); + super.reloadStrings(); } - private function formatTextOption() { - var currentOptionString = ": "; - if((currentSelection > 0)) - currentOptionString += "< "; - else - currentOptionString += " "; + function formatTextOption() { + var s = ": "; - currentOptionString += displayOptions[currentSelection]; + if (currentSelection > 0) s += "< "; + else s += " "; - if(!(currentSelection >= options.length - 1)) - currentOptionString += " >"; + s += TU.exists(displayOptions[currentSelection]) ? TU.translate(displayOptions[currentSelection]) : displayOptions[currentSelection]; - return currentOptionString; + if (currentSelection < options.length - 1) s += " >"; + + return s; } + + override function changeSelection(change:Int) { + if (locked || currentSelection == (currentSelection = CoolUtil.boundInt(currentSelection + change, 0, options.length - 1))) return; + __selectionText.text = formatTextOption(); + CoolUtil.playMenuSFX(SCROLL); + + if (optionName != null) Reflect.setField(parent, optionName, options[currentSelection]); + if (changedCallback != null) changedCallback(options[currentSelection]); + } + + override function select() {} } \ No newline at end of file diff --git a/source/funkin/options/type/Checkbox.hx b/source/funkin/options/type/Checkbox.hx index 77947bd7..2e333420 100644 --- a/source/funkin/options/type/Checkbox.hx +++ b/source/funkin/options/type/Checkbox.hx @@ -10,25 +10,26 @@ class Checkbox extends TextOption { public var checked(default, set):Bool; public var parent:Dynamic; - public var optionName:String; private var offsets:Map = [ "unchecked" => FlxPoint.get(0, -70), - "checked" => FlxPoint.get(23, -30), + "checked" => FlxPoint.get(23, -32), "unchecking" => FlxPoint.get(25, -12), - "checking" => FlxPoint.get(35, 31) + "checking" => FlxPoint.get(35, 29) ]; - public function new(text:String, desc:String, optionName:String, ?parent:Dynamic) { - super(text, desc, null); + override function set_text(v:String) { + super.set_text(v); + checkbox.x = __text.x + __text.width + 26; + return v; + } - if (parent == null) - parent = Options; + public function new(text:String, desc:String, optionName:String, ?selectCallback:Void->Void, ?parent:Dynamic) { + this.optionName = optionName; + this.parent = parent = parent != null ? parent : Options; - this.parent = parent; - - checkbox = new FlxSprite(10, -40); + checkbox = new FlxSprite(10, -30); checkbox.frames = Paths.getFrames('menus/options/checkboxThingie'); checkbox.animation.addByPrefix("unchecked", "Check Box unselected0", 24); checkbox.animation.addByPrefix("checked", "Check Box Selected Static0", 24); @@ -37,65 +38,59 @@ class Checkbox extends TextOption { checkbox.antialiasing = true; checkbox.scale.set(0.75, 0.75); checkbox.updateHitbox(); - add(checkbox); - this.optionName = optionName; - if(optionName != null) { - checked = Reflect.field(parent, optionName); - } else { - checked = false; - } + //__text.x = 100; + + if (optionName != null) checked = Reflect.field(parent, optionName); + else checked = false; + + super(text, desc, selectCallback); + add(checkbox); } public var firstFrame:Bool = true; - public override function update(elapsed:Float) { - if(checkbox.animation.curAnim == null) { - checkbox.animation.play(checked ? "checked" : "unchecked", true); - } + override function update(elapsed:Float) { + if (checkbox.animation.curAnim == null) checkbox.animation.play(checked ? "checked" : "unchecked", true); super.update(elapsed); - switch(checkbox.animation.curAnim.name) { - case "unchecking": - if(checkbox.animation.curAnim.finished) { - checkbox.animation.play("unchecked", true); - } - case "checking": - if(checkbox.animation.curAnim.finished) { - checkbox.animation.play("checked", true); - } + + if (checkbox.animation.curAnim.finished) switch(checkbox.animation.curAnim.name) { + case "unchecking": checkbox.animation.play("unchecked", true); + case "checking": checkbox.animation.play("checked", true); } firstFrame = false; } override function draw() { - if(checkbox.animation.curAnim != null) { + if (checkbox.animation.curAnim != null) { var offset = offsets[checkbox.animation.curAnim.name]; - if (offset != null) - checkbox.frameOffset.set(offset.x, offset.y); + if (offset != null) checkbox.frameOffset.set(offset.x, offset.y); } super.draw(); } function set_checked(v:Bool) { - if(checked != v) { - checked = v; - if(!firstFrame) { - checkbox.animation.play(checked ? "checking" : "unchecking", true); - } - } + if (checked == (checked = v)) return v; + + if (!firstFrame) checkbox.animation.play(checked ? "checking" : "unchecking", true); return v; } - public override function onSelect() { + override function select() { + if (locked) return; + checked = !checked; - if(optionName != null) { - Reflect.setField(parent, optionName, checked); - } + if (optionName != null) Reflect.setField(parent, optionName, checked); + + if (checked) CoolUtil.playMenuSFX(CHECKED); + else CoolUtil.playMenuSFX(UNCHECKED); + + if (selectCallback != null) selectCallback(); } - public override function destroy() { + override function destroy() { super.destroy(); - for(e in offsets) e.put(); + for (e in offsets) e.put(); } } \ No newline at end of file diff --git a/source/funkin/options/type/EditorIconOption.hx b/source/funkin/options/type/EditorIconOption.hx deleted file mode 100644 index 5303fb01..00000000 --- a/source/funkin/options/type/EditorIconOption.hx +++ /dev/null @@ -1,13 +0,0 @@ -package funkin.options.type; - -import flixel.util.FlxColor; - -class EditorIconOption extends IconOption { - public var flashColor:FlxColor; - - public function new(name:String, desc:String, icon:String, callback:Void->Void, flashColor:FlxColor = 0) { - super(name, desc, icon, callback); - - this.flashColor = flashColor; - } -} \ No newline at end of file diff --git a/source/funkin/options/type/GithubIconOption.hx b/source/funkin/options/type/GithubIconOption.hx index be3567a8..16916b55 100644 --- a/source/funkin/options/type/GithubIconOption.hx +++ b/source/funkin/options/type/GithubIconOption.hx @@ -26,6 +26,7 @@ class GithubIconOption extends TextOption this.icon = new GithubUserIcon(user, size, waitUntilLoad); this.usePortrait = usePortrait; add(icon); + __text.x = 100; } } diff --git a/source/funkin/options/type/IconOption.hx b/source/funkin/options/type/IconOption.hx index b61c5c67..547c9f59 100644 --- a/source/funkin/options/type/IconOption.hx +++ b/source/funkin/options/type/IconOption.hx @@ -12,6 +12,8 @@ class IconOption extends TextOption { public function new(name:String, desc:String, icon:String, callback:Void->Void) { super(name, desc, callback); + __text.x = 100; + iconSpr = new HealthIcon(icon, false); iconSpr.setPosition(90 - iconSpr.width, (__text.height - iconSpr.height) / 2); iconSpr.setUnstretchedGraphicSize(150, 150, true); diff --git a/source/funkin/options/type/MeterOption.hx b/source/funkin/options/type/MeterOption.hx index 7902e09e..831a4adc 100644 --- a/source/funkin/options/type/MeterOption.hx +++ b/source/funkin/options/type/MeterOption.hx @@ -20,11 +20,7 @@ class MeterOption extends OptionType { public var barWidth(default, set):Int; - private var rawText(default, set):String; - - public var text(get, set):String; - private function get_text() {return __text.text;} - private function set_text(v:String) {return __text.text = v;} + override function set_text(v:String) return __text.text = text = v; private var __text:Alphabet; var optionName:String; @@ -43,7 +39,7 @@ class MeterOption extends OptionType { public function new(text:String, desc:String, min:Float, max:Float, changeVal:Float, optionName:String, barWidth:Int = 520, ?selectCallback:Float->Void = null, ?parent:Dynamic) { - super(desc); + super(text, desc); this.selectCallback = selectCallback; this.min = min; @@ -162,12 +158,6 @@ class MeterOption extends OptionType { x += temp * barScale; } - function set_rawText(v:String) { - rawText = v; - __text.text = TU.exists(rawText) ? TU.translate(rawText) : rawText; - return v; - } - override function draw() { lastX = x; @@ -196,7 +186,7 @@ class MeterOption extends OptionType { this.rawText = rawText; } - override function onChangeSelection(change:Float) { + override function changeSelection(change:Float) { if (currentSelection <= min && change == -1 || currentSelection >= max && change == 1) return; currentSelection = FlxMath.roundDecimal(currentSelection + (change * changeVal), FlxMath.getDecimals(changeVal)); diff --git a/source/funkin/options/type/NewOption.hx b/source/funkin/options/type/NewOption.hx index 28255b8c..85181412 100644 --- a/source/funkin/options/type/NewOption.hx +++ b/source/funkin/options/type/NewOption.hx @@ -10,8 +10,10 @@ class NewOption extends TextOption { public function new(name:String, desc:String, callback:Void->Void) { super(name, desc, callback); + itemHeight = 160; __text.color = FlxColor.LIME; + __text.x = 100; iconSpr = new FlxSprite().loadGraphic(Paths.image("editors/new")); iconSpr.setPosition(90 - iconSpr.width, (__text.height - iconSpr.height) / 2); diff --git a/source/funkin/options/type/NumOption.hx b/source/funkin/options/type/NumOption.hx index e570e197..953294ff 100644 --- a/source/funkin/options/type/NumOption.hx +++ b/source/funkin/options/type/NumOption.hx @@ -1,72 +1,53 @@ package funkin.options.type; -import flixel.effects.FlxFlicker; - /** * Option type that allows stepping through a number. **/ -class NumOption extends OptionType { - public var selectCallback:Float->Void; - - private var __text:Alphabet; - private var __number:Alphabet; +class NumOption extends TextOption { + public var changedCallback:Float->Void; public var min:Float; public var max:Float; + public var step:Float; - public var currentSelection:Float; - public var changeVal:Float; - - var optionName:String; + public var currentValue:Float; public var parent:Dynamic; + public var optionName:String; - private var rawText(default, set):String; + var __number:Alphabet; - public var text(get, set):String; - private function get_text() {return __text.text;} - private function set_text(v:String) {return __text.text = v;} - - public function new(text:String, desc:String, min:Float, max:Float, changeVal:Float, optionName:String, ?selectCallback:Float->Void = null, ?parent:Dynamic) { - super(desc); - this.selectCallback = selectCallback; - this.min = min; - this.max = max; - if (parent == null) - parent = Options; - - this.parent = parent; - - if(Reflect.field(parent, optionName) != null) - this.currentSelection = Reflect.field(parent, optionName); - this.changeVal = changeVal; - this.optionName = optionName; - - add(__text = new Alphabet(20, 20, "", "bold")); - add(__number = new Alphabet(0, 20, ': $currentSelection', "bold")); - rawText = text; - } - - override function reloadStrings() { - super.reloadStrings(); - this.rawText = rawText; - } - - function set_rawText(v:String) { - rawText = v; - __text.text = TU.exists(rawText) ? TU.translate(rawText) : rawText; + override function set_text(v:String) { + super.set_text(v); __number.x = __text.x + __text.width + 12; return v; } - override function onChangeSelection(change:Float):Void - { - if(currentSelection <= min && change == -1 || currentSelection >= max && change == 1) return; - currentSelection = FlxMath.roundDecimal(currentSelection + (change * changeVal), FlxMath.getDecimals(changeVal)); - __number.text = ': $currentSelection'; + public function new(text:String, desc:String, min:Float, max:Float, step:Float, optionName:String, ?changedCallback:Float->Void = null, ?parent:Dynamic) { + this.changedCallback = changedCallback; + this.min = min; + this.max = max; + this.step = step; + this.optionName = optionName; + this.parent = parent = parent != null ? parent : Options; - Reflect.setField(parent, optionName, currentSelection); - if(selectCallback != null) - selectCallback(currentSelection); + if (Reflect.field(parent, optionName) != null) currentValue = Reflect.field(parent, optionName); + + __number = new Alphabet(0, 20, ': $currentValue', 'bold'); + super(text, desc); + add(__number); } + + override function changeSelection(change:Int):Void { + if (locked) return; + if (currentValue == (currentValue = FlxMath.bound(currentValue + change, min, max))) return; + __number.text = ': $currentValue'; + + Reflect.setField(parent, optionName, currentValue); + if (changedCallback != null) changedCallback(currentValue); + + CoolUtil.playMenuSFX(SCROLL); + } + + override function select() {} } \ No newline at end of file diff --git a/source/funkin/options/type/OptionType.hx b/source/funkin/options/type/OptionType.hx index 44cd94e1..3d4d25f0 100644 --- a/source/funkin/options/type/OptionType.hx +++ b/source/funkin/options/type/OptionType.hx @@ -1,41 +1,64 @@ package funkin.options.type; import flixel.group.FlxSpriteGroup; +import flixel.util.FlxColor; import funkin.backend.system.Controls; +import funkin.options.TreeMenu.ITreeOption; /** * Base class for all option types. * Used in OptionsMenu. **/ -class OptionType extends FlxSpriteGroup { - public var controls(get, never):Controls; +class OptionType extends FlxSpriteGroup implements ITreeOption { public var selected:Bool = false; + public var locked(default, set):Bool; + + public var text(default, set):String; + public var rawText(default, set):String; public var desc:String; public var rawDesc(default, set):String; - public function new(desc:String) { + public var itemHeight:Float = 120; + + public var editorFlashColor:FlxColor = FlxColor.WHITE; + + public function new(text:String, desc:String) { super(); + rawText = text; rawDesc = desc; } + function set_locked(v:Bool) { + if (locked == (locked = v)) return v; + color = locked ? 0xFF7F7F7F : 0xFFFFFFFF; + return v; + } + + function set_text(v:String) return text = v; + function set_rawText(v:String) { + rawText = v; + text = TU.exists(rawText) ? TU.translate(rawText) : rawText; + return v; + } + function set_rawDesc(v:String) { rawDesc = v; - this.desc = TU.exists(rawDesc) ? TU.translate(rawDesc) : rawDesc; + desc = TU.exists(rawDesc) ? TU.translate(rawDesc) : rawDesc; return v; } public function reloadStrings() { - this.rawDesc = rawDesc; + rawText = rawText; + rawDesc = rawDesc; } - private function get_controls() {return PlayerSettings.solo.controls;} - - public function onSelect() {} - - public function onChangeSelection(change:Float) {} - public override function update(elapsed:Float) { super.update(elapsed); alpha = (selected ? 1 : 0.6); } + + public function changeSelection(change:Int) {} + public function select() {} + + override function get_height() return itemHeight; } \ No newline at end of file diff --git a/source/funkin/options/type/PortraitOption.hx b/source/funkin/options/type/PortraitOption.hx index a719b32d..b512a8f2 100644 --- a/source/funkin/options/type/PortraitOption.hx +++ b/source/funkin/options/type/PortraitOption.hx @@ -12,6 +12,8 @@ class PortraitOption extends TextOption { public function new(name:String, desc:String, callback:Void->Void, ?graphic:FlxGraphic, size:Int = 96, usePortrait:Bool = true) { super(name, desc, callback); + __text.x = 100; + if (graphic != null) addPortrait(graphic, size, usePortrait); } diff --git a/source/funkin/options/type/RadioButton.hx b/source/funkin/options/type/RadioButton.hx index d43240ac..b576c192 100644 --- a/source/funkin/options/type/RadioButton.hx +++ b/source/funkin/options/type/RadioButton.hx @@ -1,17 +1,19 @@ package funkin.options.type; import flixel.math.FlxPoint; +import funkin.options.TreeMenuScreen; class RadioButton extends TextOption { public var radio:FlxSprite; public var checked(default, set):Bool; public var parent:Dynamic; - public var value:Dynamic; - public var forId:String; - public var optionName:String; + public var screen:TreeMenuScreen; + public var forId:String; + public var value:Dynamic; + private var offsets:Map = [ "unchecked" => FlxPoint.get(0, -65), "checked" => FlxPoint.get(0, -65), @@ -19,26 +21,15 @@ class RadioButton extends TextOption { "checking" => FlxPoint.get(17, -40) ]; - public function new(text:String, desc:String, optionName:String, value:Dynamic, ?parent:Dynamic, ?forId:String, ?options:OptionsScreen) { - super(text, desc, null); + public function new(?screen:TreeMenuScreen, text:String, desc:String, optionName:String, value:Dynamic, ?selectCallback:Void->Void, ?parent:Dynamic, ?forId:String) { + super(text, desc, selectCallback); + this.screen = screen; + this.optionName = optionName; + this.parent = parent = parent != null ? parent : Options; + this.forId = forId = forId == null ? optionName : forId; + this.value = value; - if (parent == null) - parent = Options; - - if(forId == null) - forId = optionName; - - this.parent = parent; - this.forId = forId; - - if(options != null) { - if(FlxG.state is OptionsMenu) { - var menu:OptionsMenu = cast FlxG.state; - options = menu.optionsTree.members.last(); - } - } - - radio = new FlxSprite(10, -40); + radio = new FlxSprite(10, -23); radio.frames = Paths.getFrames('menus/options/radioCrank'); radio.animation.addByPrefix("unchecked", "Radio unselected0", 24); radio.animation.addByPrefix("checked", "Radio Selected Static0", 24); @@ -49,73 +40,58 @@ class RadioButton extends TextOption { radio.updateHitbox(); add(radio); - this.value = value; + __text.x = 100; - this.optionName = optionName; - if(optionName != null) { - checked = Reflect.field(parent, optionName) == value; - } else { - checked = false; - } + if (optionName != null) checked = Reflect.field(parent, optionName) == value; + else checked = false; } public var firstFrame:Bool = true; - public override function update(elapsed:Float) { - if(radio.animation.curAnim == null) { - radio.animation.play(checked ? "checked" : "unchecked", true); - } + override function update(elapsed:Float) { + if (radio.animation.curAnim == null) radio.animation.play(checked ? "checked" : "unchecked", true); super.update(elapsed); - switch(radio.animation.curAnim.name) { - case "unchecking": - if(radio.animation.curAnim.finished) { - radio.animation.play("unchecked", true); - trace("UNCHECKING finished"); - } - case "checking": - if(radio.animation.curAnim.finished) { - radio.animation.play("checked", true); - trace("CHECKING finished"); - } + + if (radio.animation.curAnim.finished) switch(radio.animation.curAnim.name) { + case "unchecking": radio.animation.play("unchecked", true); + case "checking": radio.animation.play("checked", true); } firstFrame = false; } override function draw() { - if(radio.animation.curAnim != null) { + if (radio.animation.curAnim != null) { var offset = offsets[radio.animation.curAnim.name]; - if (offset != null) - radio.frameOffset.set(offset.x, offset.y); + if (offset != null) radio.frameOffset.set(offset.x, offset.y); } super.draw(); } function set_checked(v:Bool) { - if(checked != v) { - checked = v; - if(!firstFrame) { - radio.animation.play(checked ? "checking" : "unchecking", true); - } - } + if (checked == (checked = v)) return v; + + if (!firstFrame) radio.animation.play(checked ? "checking" : "unchecking", true); return v; } - public override function onSelect() { + override function select() { + if (locked) return; + checked = true; - if(checked) { - onSet(value); + if (optionName != null) Reflect.setField(parent, optionName, value); + + CoolUtil.playMenuSFX(CHECKED); + + if (screen != null) { + for (item in screen.members) if (item != null && item != this && item is RadioButton) cast(item, RadioButton).checked = false; } + + if (selectCallback != null) selectCallback(); } - public dynamic function onSet(value:Dynamic) { - if(optionName != null) { - Reflect.setField(parent, optionName, value); - } - } - - public override function destroy() { + override function destroy() { super.destroy(); - for(e in offsets) e.put(); + for (e in offsets) e.put(); } } \ No newline at end of file diff --git a/source/funkin/options/type/Separator.hx b/source/funkin/options/type/Separator.hx new file mode 100644 index 00000000..716c3576 --- /dev/null +++ b/source/funkin/options/type/Separator.hx @@ -0,0 +1,30 @@ +package funkin.options.type; + +import flixel.math.FlxPoint; +import flixel.util.FlxDestroyUtil; +import flixel.FlxBasic; + +class Separator extends FlxSprite { + var separatorHeight:Float; + public function new(height = 67) { + super(); + separatorHeight = height; + } + + override function initVars() { + flixelType = OBJECT; + + offset = FlxPoint.get(); + origin = FlxPoint.get(); + scale = FlxPoint.get(1, 1); + scrollFactor = FlxPoint.get(1, 1); + + initMotionVars(); + } + + override function update(elapsed:Float) {} + override function draw() {} + + override function get_height():Float return separatorHeight; + override function get_width():Float return 600; +} \ No newline at end of file diff --git a/source/funkin/options/type/SliderOption.hx b/source/funkin/options/type/SliderOption.hx new file mode 100644 index 00000000..955b61d9 --- /dev/null +++ b/source/funkin/options/type/SliderOption.hx @@ -0,0 +1,88 @@ +package funkin.options.type; + +import funkin.menus.ui.Slider; +import funkin.options.TreeMenu.ITreeFloatOption; + +class SliderOption extends TextOption implements ITreeFloatOption { + public var changedCallback:Float->Void; + + public var min:Float; + public var max:Float; + public var step:Float; + + public var currentValue:Float; + + public var parent:Dynamic; + public var optionName:String; + + public var slider:Slider; + public var dynamicWidth:Bool; + + var __mouseControl:Bool; + + function getValue():Float return (currentValue - min) / (max - min); + + override function set_text(v:String) { + super.set_text(v); + slider.x = __text.x + __text.width + 30; + if (dynamicWidth) { + slider.barWidth = 1100 - __text.width; + slider.updateHitbox(); + } + return v; + } + + public function new(text:String, desc:String, min:Float, max:Float, step:Float, ?segments:Int, optionName:String, barWidth = -1, + ?changedCallback:Float->Void = null, ?parent:Dynamic) + { + this.changedCallback = changedCallback; + this.min = min; + this.max = max; + this.step = step; + this.optionName = optionName; + this.parent = parent = parent != null ? parent : Options; + + if (Reflect.field(parent, optionName) != null) currentValue = Reflect.field(parent, optionName); + + slider = new Slider(12, 0, getValue(), (dynamicWidth = barWidth < 0) ? 600 : barWidth, segments); + slider.scale.set(0.75, 0.75); + slider.updateHitbox(); + + super(text, desc); + add(slider); + slider.y = Math.floor(__text.y + (__text.height - slider.height) * 0.5 + 2); + } + + override function update(elapsed:Float) { + if (slider.selected = selected && !locked) { + if (__mouseControl && FlxG.mouse.justReleased) __mouseControl = false; + else if (FlxG.mouse.justPressed) __mouseControl = slider.overlapsPoint(FlxG.mouse.getPosition(@:privateAccess flixel.input.FlxPointer._cachedPoint), true); + + if (__mouseControl) { + var p = @:privateAccess flixel.input.FlxPointer._cachedPoint; + if (!FlxG.mouse.justPressed) FlxG.mouse.getPosition(p); + p.subtractPoint(camera.scroll); + + slider.getScreenPosition(_point, camera); + changeValue((FlxMath.remapToRange(p.x - _point.x, 0, slider.width, min, max) - currentValue) / step); + } + } + else + __mouseControl = false; + + slider.value = getValue(); + //__text.x = slider.x + slider.width + 30; + + super.update(elapsed); + } + + public function changeValue(change:Float):Void { + if (locked) return; + if (currentValue == (currentValue = FlxMath.bound(currentValue + change * step, min, max))) return; + + Reflect.setField(parent, optionName, currentValue); + if (changedCallback != null) changedCallback(currentValue); + } + + override function select() {} +} \ No newline at end of file diff --git a/source/funkin/options/type/TextOption.hx b/source/funkin/options/type/TextOption.hx index 2d2b5187..64b93b25 100644 --- a/source/funkin/options/type/TextOption.hx +++ b/source/funkin/options/type/TextOption.hx @@ -6,46 +6,35 @@ import flixel.effects.FlxFlicker; * Option type that has text. **/ class TextOption extends OptionType { + public var suffix(default, set):String; public var selectCallback:Void->Void; - private var __text:Alphabet; - private var rawText(default, set):String; - - public var text(get, set):String; - private function get_text() {return __text.text;} - private function set_text(v:String) {return __text.text = v;} - - /** - * If you change this afterwards you will need to call `reloadStrings()`, or manually set rawText to rawText. - **/ - public var suffix:String; - - public function new(text:String, desc:String, ?suffix:String = "", ?selectCallback:Void->Void = null) { - super(desc); - this.selectCallback = selectCallback; - add(__text = new Alphabet(100, 20, "", "bold")); - this.suffix = suffix; - rawText = text; + var __text:Alphabet; + override function set_text(v:String) { + __text.text = v + suffix; + return text = v; } - override function reloadStrings() { - super.reloadStrings(); - this.rawText = rawText; - } - - function set_rawText(v:String) { - rawText = v; - __text.text = (TU.exists(rawText) ? TU.translate(rawText) : rawText) + suffix; + function set_suffix(v:String) { + if (suffix == (suffix = v)) return v; + __text.text = text + suffix; return v; } - public override function draw() { - super.draw(); + public function new(text:String, desc:String, ?suffix:String = "", ?selectCallback:Void->Void = null) { + @:bypassAccessor this.suffix = suffix; + this.selectCallback = selectCallback; + + __text = new Alphabet(20, 20, "", "bold"); + + super(text, desc); + add(__text); } - public override function onSelect() { - super.onSelect(); + + override function select() { + if (locked) return; CoolUtil.playMenuSFX(CONFIRM); FlxFlicker.flicker(this, 1, Options.flashingMenu ? 0.06 : 0.15, true, false); - selectCallback(); + if (selectCallback != null) selectCallback(); } } \ No newline at end of file