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..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