From f20fc25a8270e931ba1dae426df9b314fdfc9ced Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sat, 26 Jul 2025 07:47:16 +0700 Subject: [PATCH] Options Rewrite WIP not functional yet --- assets/images/menus/options/meter.png | Bin 4221 -> 0 bytes assets/images/menus/options/meter.xml | 8 - assets/images/menus/options/slider.png | Bin 0 -> 8889 bytes assets/images/menus/options/slider.xml | 21 ++ source/funkin/backend/TurboControls.hx | 121 +++++++ source/funkin/backend/system/Controls.hx | 2 + source/funkin/backend/utils/CoolUtil.hx | 4 +- source/funkin/menus/ui/Alphabet.hx | 3 +- source/funkin/options/Options.hx | 3 +- source/funkin/options/OptionsMenu.hx | 149 ++------ source/funkin/options/OptionsTree.hx | 89 ----- source/funkin/options/TreeMenu.hx | 341 ++++++++++++------ source/funkin/options/TreeMenuScreen.hx | 130 +++++++ .../options/categories/AppearanceOptions.hx | 19 +- .../options/categories/GameplayOptions.hx | 4 +- source/funkin/options/categories/import.hx | 2 + source/funkin/options/type/ArrayOption.hx | 85 ++--- source/funkin/options/type/OptionType.hx | 32 +- source/funkin/options/type/SliderOption.hx | 35 ++ 19 files changed, 639 insertions(+), 409 deletions(-) delete mode 100644 assets/images/menus/options/meter.png delete mode 100644 assets/images/menus/options/meter.xml create mode 100644 assets/images/menus/options/slider.png create mode 100644 assets/images/menus/options/slider.xml create mode 100644 source/funkin/backend/TurboControls.hx delete mode 100644 source/funkin/options/OptionsTree.hx create mode 100644 source/funkin/options/TreeMenuScreen.hx create mode 100644 source/funkin/options/type/SliderOption.hx diff --git a/assets/images/menus/options/meter.png b/assets/images/menus/options/meter.png deleted file mode 100644 index 4607e9b08c89fcb35365c65c41da70cbe2dc4ab5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4221 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 TeEljG00000NkvXXu0mjfIWF!n 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 0000000000000000000000000000000000000000..e2bf219286036dff100b145a31d372071fba054c GIT binary patch literal 8889 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 literal 0 HcmV?d00001 diff --git a/assets/images/menus/options/slider.xml b/assets/images/menus/options/slider.xml new file mode 100644 index 00000000..facddef9 --- /dev/null +++ b/assets/images/menus/options/slider.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/funkin/backend/TurboControls.hx b/source/funkin/backend/TurboControls.hx new file mode 100644 index 00000000..2c1f4ec0 --- /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 ?? 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..0ccaac63 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/utils/CoolUtil.hx b/source/funkin/backend/utils/CoolUtil.hx index d24e153a..47510764 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'; 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/options/Options.hx b/source/funkin/options/Options.hx index a60d519d..bd39cd45 100644 --- a/source/funkin/options/Options.hx +++ b/source/funkin/options/Options.hx @@ -209,7 +209,8 @@ class Options applyKeybinds(); FlxG.game.stage.quality = (FlxG.enableAntialiasing = antialiasing) ? LOW : BEST; 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 ace34aad..9f35c36f 100644 --- a/source/funkin/options/OptionsMenu.hx +++ b/source/funkin/options/OptionsMenu.hx @@ -1,172 +1,87 @@ package funkin.options; -import funkin.backend.system.framerate.Framerate; -import funkin.options.TreeMenu; 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) -> OptionsScreen>; + 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 } ]; - var bg:FlxSprite; + var bg:FunkinSprite; - 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 FunkinSprite().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])); - } - } - }); - }]); + /*tree = [ + for (o in mainOptions) { - for (i in funkin.backend.assets.ModsFolder.getLoadedMods()) { - var xmlPath = Paths.xml('config/options/LIB_$i'); - - if (Paths.assetsTree.existsSpecific(xmlPath, "TEXT")) { - 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); } - } + ];*/ + } - public function reloadStrings() { - for(o in main.members) { - o.reloadStrings(); - } - optionsTree.reloadStrings(); - reloadLabels(); + 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); + bg.screenCenter(); } - public override function exit() { + override function onResize(width:Int, height:Int) { + super.onResize(width, height); + if (!UIState.resolutionAware) return; + + updateBG(); + } + + override function exit() { Options.save(); Options.applySettings(); super.exit(); } - - /** - * XML STUFF - */ - public function parseOptionsFromXML(xml:Access):Array { - var options:Array = []; - - for(node in xml.elements) { - if (!node.has.name) { - Logs.warn("An option node requires a name attribute."); - continue; - } - var name = node.getAtt("name"); - var desc = node.getAtt("desc").getDefault("optionsMenu.desc-missing"); - - switch(node.name) { - case "checkbox": - if (!node.has.id) { - Logs.warn("A checkbox option requires an \"id\" for option saving."); - continue; - } - options.push(new Checkbox(name, desc, node.att.id, FlxG.save.data)); - - case "number": - if (!node.has.id) { - Logs.warn("A number option requires an \"id\" for option saving."); - continue; - } - options.push(new NumOption(name, desc, Std.parseFloat(node.att.min), Std.parseFloat(node.att.max), Std.parseFloat(node.att.change), node.att.id, null, FlxG.save.data)); - case "choice": - if (!node.has.id) { - Logs.warn("A choice option requires an \"id\" for option saving."); - continue; - } - - var optionOptions:Array = []; - var optionDisplayOptions:Array = []; - - for(choice in node.elements) { - optionOptions.push(choice.att.value); - optionDisplayOptions.push(choice.att.name); - } - - if(optionOptions.length > 0) - 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))); - })); - } - } - - return options; - } } \ 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..ba818c36 100644 --- a/source/funkin/options/TreeMenu.hx +++ b/source/funkin/options/TreeMenu.hx @@ -1,148 +1,269 @@ package funkin.options; -import flixel.FlxState; -import flixel.tweens.FlxTween; -import flixel.util.typeLimit.OneOfTwo; -import flixel.util.typeLimit.OneOfThree; -import funkin.backend.FunkinText; -import funkin.editors.ui.UIState; -import funkin.menus.MainMenuState; -import funkin.options.type.OptionType; -import funkin.options.type.TextOption; +import flixel.util.FlxSignal; import funkin.backend.system.framerate.Framerate; +import funkin.editors.ui.UIState; + +interface ITreeOption { + var desc:String; + var selected:Bool; + + function changeSelection(change:Int); + function select():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 length(default, null):Int = 0; + public var lastMenu:Null; + 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; + + 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.camera.width - 8, 32); + titleLabel.borderSize = 1.25; + titleLabel.scrollFactor.set(); + descLabel = new FunkinText(4, 0, FlxG.camera.width - 8, 16); + descLabel.scrollFactor.set(); + } + override function createPost() { super.createPost(); + + if ((length = tree.length) != 0) { + updateMenuPositions(true); + tree.last().inputEnabled = true; + } + else + add(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); + updateLabels(); } - 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[position - 1], menu:TreeMenuScreen; + while (fromIndex < length) if ((menu = tree[fromIndex++]) != null) { + menu.x = last == null ? 0 : last.x + Math.max(FlxG.camera.width, last.width); + last = menu; + } - 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 (menuChangeTween != null && menuChangeTween.active) menuChangeTween.cancel(); + if (cameraScroll) FlxG.camera.scroll.x = lastMenu.x; + } - reloadLabels(); + 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 updateDesc(?customText:String) { + var last = tree.last(); + + 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 add(menu:TreeMenuScreen):TreeMenuScreen { + if (menu == null) return null; + if (tree.indexOf(menu) != -1) return menu; + + tree.push(menu); + menu.parent = this; + length++; + + var prev = tree[length - 1]; + menu.x = prev == null ? 0 : prev.x + Math.max(FlxG.camera.width, prev.width); + + menuChanged(); + menu.inputEnabled = true; + prev.inputEnabled = false; + + return menu; + } + + public function insert(position:Int, menu:TreeMenuScreen):TreeMenuScreen { + if (menu == null) return null; + if (tree.indexOf(menu) != -1) return menu; + + if (position < 0) position = length - ((-position - 1) % length); + tree.insert(position, menu); + menu.parent = this; + + var lastChanged = position >= length++; + updateMenuPositions(position, !lastChanged); + + if (lastChanged) { + menuChanged(); + menu.inputEnabled = true; + tree[length - 1].inputEnabled = false; + } + + return menu; + } + + public function pop():TreeMenuScreen { + (lastMenu = tree.pop(position)).parent = null; + length--; + + menuChanged(); + lastMenu.inputEnabled = false; + menu.inputEnabled = true; + + return lastMenu; + } + + public function remove(menu:TreeMenuScreen):TreeMenuScreen { + return if (menu == null) null; else removePosition(tree.indexOf(menu)); + } + + public function removePosition(position:Int):TreeMenuScreen { + if (position < 0 || position >= length) return null; + + destroyLastMenu(); + (lastMenu = tree.swapAndPop(position)).parent = null; + + if (position == --length) { + menuChanged(); + lastMenu.inputEnabled = false; + tree[length - 1].inputEnabled = true; + } + else { + updateMenuPositions(position, true); + updateLabels(); + } + + return lastMenu; + } + + public function menuChanged() { + if (length == 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; + lastMenu.transitioning = false; + }}); + + for (menu in tree) if (menu != null) menu.transitioning = true; + lastMenu.transitioning = true; } } - public function onMenuClose(m:OptionsScreen) { - CoolUtil.playMenuSFX(CANCEL); - } - - public function reloadLabels() { - var t = ""; - for(o in optionsTree.members) - t += '${o.name} > '; - pathLabel.text = t; - - var idk:OptionsScreen = optionsTree.members.last(); - if (idk.members.length > 0) updateDesc(idk.members[idk.curSelected].desc); - } - - 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 exit() { - FlxG.switchState((lastState != null) ? Type.createInstance(lastState, []) : new MainMenuState()); - lastState = null; - } - - var menuChangeTween:FlxTween; - public override function update(elapsed:Float) { + 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 < length) { + if ((menu = tree[i++]) == null || !menu.active || !menu.exists) continue; + if (i == length || menu.persistentUpdate || menu.transitioning) menu.update(elapsed); + } + + if (lastMenu != null) { + if (lastMenu.transitioning) lastMenu.update(elapsed); + else destroyLastMenu(); + } + + // 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(); + destroyLastMenu(); + } + + 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.width = width) - 8; } - 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(); + } + + public function destroyLastMenu() { + lastMenu = destroyMenus ? FlxDestroyUtil.destroy(lastMenu) : null; + } + + public function exit() { + if (exitCallback != null) return exitCallback(this); + + FlxG.switchState(new 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) { + this.parent = parent; + moves = false; + } -typedef OptionTypeDef = { - var type:Class; - var args:Array; -} + override function draw() { + var i = 0, menu:TreeMenuScreen; + while (i < parent.length) { + if ((menu = parent.tree[i++]) == null || !menu.active || !menu.exists) continue; + if (i == length || menu.persistentUpdate || menu.transitioning) menu.update(elapsed); + } + + if (parent.lastMenu != null) parent.lastMenu.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..7fa6e19f --- /dev/null +++ b/source/funkin/options/TreeMenuScreen.hx @@ -0,0 +1,130 @@ +package funkin.options; + +import flixel.util.FlxSignal; +import funkin.backend.system.Controls; +import funkin.backend.TurboControls; +import funkin.options.TreeMenu.ITreeOption; + +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 itemHeight:Float = 120; + + 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) { + if (rawName == (rawName = v)) return v; + name = TU.exists(rawName) ? TU.translate(rawName) : rawName; + return v; + } + + function set_rawDesc(v:String) { + if (rawDesc == (rawDesc = v)) return v; + desc = TU.exists(rawDesc) ? TU.translate(rawDesc) : rawDesc; + return v; + } + + public inline function getNameID(name):String return prefix + name + "-name"; + public inline function getDescID(name):String return prefix + name + "-desc"; + + public var controls(get, never):Controls; + inline function get_controls():Controls return PlayerSettings.solo.controls; + + var leftTurboControl:TurboControls = new TurboControls([Control.LEFT]); + var rightTurboControl:TurboControls = new TurboControls([Control.RIGHT]); + var upTurboControl:TurboControls = new TurboControls([Control.UP]); + var downTurboControl:TurboControls = new TurboControls([Control.DOWN]); + var turboBasics:Array; + + var curOption:ITreeOption; + var _firstFrame:Bool = true; + + public function new(name:String, desc:String, ?prefix:String, ?options:Array) { + super(); + + rawName = name; + rawDesc = desc; + + turboBasics = [leftTurboControl, rightTurboControl, upTurboControl, downTurboControl]; + } + + public function reloadStrings() { + name = TU.exists(rawName) ? TU.translate(rawName) : rawName; + desc = TU.exists(rawDesc) ? TU.translate(rawDesc) : rawDesc; + + for (object in members) if (object != null) object.reloadStrings(); + } + + override function update(elapsed:Float) { + super.update(elapsed); + + if (_firstFrame) { + _firstFrame = false; + if (members[curSelected] is ITreeOption) (curOption = cast members[curSelected]:ITreeOption).selected = true; + } + + if (inputEnabled) { + for (basic in turboBasics) basic.update(elapsed); + changeSelection((upTurboControl.activated ? -1 : 0) + (downTurboControl.activated ? 1 : 0) - FlxG.mouse.wheel); + + if (i > 0 && curOption != null) { + if (controls.ACCEPT || FLxG.mouse.justPressed) curOption.select(); + if (leftTurboControl.activated) curOption.changeSelection(-1); + if (rightTurboControl.activated) curOption.changeSelection(1); + } + + if (controls.BACK || FlxG.mouse.justPressedRight) close(); + } + + var i:Int = 0, object:FlxSprite; + while (i < length) if ((object = members[i++]) != null) { + var h = object.height; + object.y = CoolUtil.fpsLerp(object.y, y + (FlxG.camera.height - h) * 0.5 + (i - curSelected) * itemHeight, 0.25); + object.x = x - 50 + Math.abs(Math.cos((object.y + (FlxG.camera.height + h) * 0.5 - FlxG.camera.scroll.y) / (FlxG.camera.height * 1.25) * Math.PI)) * 150; + } + } + + public function close() { + onClose.dispatch(); + + if (parent == null) return destroy(); + else parent.remove(this); + + CoolUtil.playMenuSFX(CANCEL); + } + + public function changeSelection(change:Int, force:Bool = false) { + if (length == 0 || (change == 0 && !force)) return; + + curSelected = FlxMath.wrap(curSelected + change, 0, members.length - 1); + if (members[curSelected] is ITreeOption) (curOption = cast members[curSelected]:ITreeOption).selected = true; + updateMenuDesc(); + + CoolUtil.playMenuSFX(SCROLL); + } + + public function updateMenuDesc(?customTxt:String) { + if (parent != null) parent.updateDesc(customTxt); + } + + 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..0131a73d 100644 --- a/source/funkin/options/categories/AppearanceOptions.hx +++ b/source/funkin/options/categories/AppearanceOptions.hx @@ -1,8 +1,10 @@ package funkin.options.categories; -class AppearanceOptions extends OptionsScreen { - public override function new(title:String, desc:String) { - super(title, desc, "AppearanceOptions."); +class AppearanceOptions extends TreeMenuScreen { + public override function new() { + super('optionsTree.appearance-name', 'optionsTree.appearance-desc', "AppearanceOptions."); + + /* add(new NumOption( getName("framerate"), getDesc("framerate"), @@ -43,13 +45,12 @@ class AppearanceOptions extends OptionsScreen { "gpuOnlyBitmaps")); } #end + */ } - 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; } } \ No newline at end of file diff --git a/source/funkin/options/categories/GameplayOptions.hx b/source/funkin/options/categories/GameplayOptions.hx index 4e1f8a7e..3064a588 100644 --- a/source/funkin/options/categories/GameplayOptions.hx +++ b/source/funkin/options/categories/GameplayOptions.hx @@ -30,13 +30,13 @@ class GameplayOptions extends OptionsScreen { getName("autoPause"), getDesc("autoPause"), "autoPause")); - add(new MeterOption( + /*add(new MeterOption( getName("volumeSFX"), getDesc("volumeSFX"), 0, // minimum 1, // maximum 0.1, // change - "volumeSFX")); + "volumeSFX"));*/ add(offsetSetting = new NumOption( getName("songOffset"), getDesc("songOffset"), 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..81184aeb 100644 --- a/source/funkin/options/type/ArrayOption.hx +++ b/source/funkin/options/type/ArrayOption.hx @@ -1,88 +1,57 @@ 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; - 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 __text:Alphabet; + 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); + public function new(text:String, desc:String, options:Array, displayOptions:Array, ?optionName:String, ?selectCallback:String->Void = null, ?parent:Dynamic) { + super(text, 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.parent = parent = parent != null ? parent : Options; this.optionName = optionName; - add(__text = new Alphabet(100, 20, text, "bold")); - add(__selectiontext = new Alphabet(__text.width + 120, 20, formatTextOption(), "normal")); + var fieldValue = Reflect.field(parent, optionName); + if (fieldValue != null) currentSelection = CoolUtil.maxInt(0, options.indexOf(fieldValue)); - rawText = text; + add(__text = new Alphabet(20, 0, text, 'bold')); + add(__selectionText = new Alphabet(__text.x + __text.width + 12, 0, formatTextOption(), 'bold')); } override function reloadStrings() { super.reloadStrings(); - this.rawText = rawText; - } - - function set_rawText(v:String) { - rawText = v; - __text.text = TU.exists(rawText) ? TU.translate(rawText) : rawText; - return v; - } - - public override function draw() { - super.draw(); - } - - public override function onChangeSelection(change:Float):Void - { - if(currentSelection <= 0 && change == -1 || currentSelection >= options.length - 1 && change == 1) return; - currentSelection += Math.round(change); + __selectiontext.x = __text.x + __text.width + 12; __selectiontext.text = formatTextOption(); - Reflect.setField(parent, optionName, options[currentSelection]); - if(selectCallback != null) - selectCallback(options[currentSelection]); } - private function formatTextOption() { - var currentOptionString = ": "; - if((currentSelection > 0)) - currentOptionString += "< "; - else - currentOptionString += " "; + override function changeSelection(change:Int) { + currentSelection = FlxMath.wrap(currentSelection + change, 0, options.length - 1); + __selectionText.text = formatTextOption(); + CoolUtil.playMenuSFX(SCROLL); - currentOptionString += displayOptions[currentSelection]; + if (selectCallback != null) selectCallback(options[currentSelection]); + } - if(!(currentSelection >= options.length - 1)) - currentOptionString += " >"; + function formatTextOption() { + var s = ": "; - return currentOptionString; + if (currentSelection > 0) s += "< "; + else s += " "; + + s += TU.exists(displayOptions[currentSelection]) ? TU.translate(displayOptions[currentSelection]) : displayOptions[currentSelection]; + + if (currentSelection < options.length - 1) s += " >"; + + return s; } } \ No newline at end of file diff --git a/source/funkin/options/type/OptionType.hx b/source/funkin/options/type/OptionType.hx index 44cd94e1..7429a64a 100644 --- a/source/funkin/options/type/OptionType.hx +++ b/source/funkin/options/type/OptionType.hx @@ -2,40 +2,48 @@ package funkin.options.type; import flixel.group.FlxSpriteGroup; 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 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 function new(text:String, desc:String) { super(); - rawDesc = desc; + reloadStrings(); + } + + 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() {} } \ 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..37f4f4fc --- /dev/null +++ b/source/funkin/options/type/SliderOption.hx @@ -0,0 +1,35 @@ +package funkin.options.type; + +class SliderOption extends OptionType { + public var selectCallback:Float->Void; + + public var min:Float; + public var max:Float; + public var segmentVal:Float; + + public var currentSelection:Float; + + public var parent:Dynamic; + + //public var barWidth(get, set):Int; + + public var text(get, set):String; + private function get_text() {return __text.text;} + private function set_text(v:String) {return __text.text = v;} + private var rawText(default, set):String; + + var __text:Alphabet; + var optionName:String; + + public function new(text:String, desc:String, min:Float, max:Float, segmentVal:Float, optionName:String, barWidth:Int = 520, + ?selectCallback:Float->Void = null, ?parent:Dynamic) + { + super(desc); + } + + function set_rawText(v:String) { + rawText = v; + __text.text = TU.exists(rawText) ? TU.translate(rawText) : rawText; + return v; + } +} \ No newline at end of file