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 01/25] 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 From ef6cdd5371939ff330aeb43b6e107ecab8203be8 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sat, 26 Jul 2025 18:53:25 +0700 Subject: [PATCH 02/25] Options Rewrite WIP Appearance Restructure --- assets/languages/en/Options.xml | 32 ++-- source/funkin/backend/system/Controls.hx | 2 +- source/funkin/editors/DebugOptions.hx | 64 +------- source/funkin/editors/EditorTreeMenu.hx | 6 + source/funkin/menus/MainMenuState.hx | 4 +- source/funkin/menus/PauseSubState.hx | 4 +- .../funkin/menus/credits/CreditsCodename.hx | 136 --------------- source/funkin/menus/credits/CreditsMain.hx | 106 ------------ source/funkin/menus/ui/Slider.hx | 5 + source/funkin/options/Options.hx | 15 +- source/funkin/options/OptionsMenu.hx | 107 +++++++++++- source/funkin/options/OptionsScreen.hx | 8 +- source/funkin/options/OptionsTree.hx | 91 ++++++++++ source/funkin/options/TreeMenu.hx | 155 ++++++++++-------- source/funkin/options/TreeMenuScreen.hx | 82 ++++++--- .../options/categories/AppearanceOptions.hx | 85 +++++----- .../options/categories/GameplayOptions.hx | 90 +--------- .../options/categories/LanguageOptions.hx | 2 +- .../funkin/options/categories/MiscOptions.hx | 33 +--- source/funkin/options/type/ArrayOption.hx | 48 +++--- source/funkin/options/type/Checkbox.hx | 69 ++++---- source/funkin/options/type/MeterOption.hx | 16 +- source/funkin/options/type/NumOption.hx | 80 ++++----- source/funkin/options/type/OptionType.hx | 14 +- source/funkin/options/type/RadioButton.hx | 4 +- source/funkin/options/type/Separator.hx | 30 ++++ source/funkin/options/type/SliderOption.hx | 13 +- source/funkin/options/type/TextOption.hx | 47 ++---- 28 files changed, 586 insertions(+), 762 deletions(-) delete mode 100644 source/funkin/menus/credits/CreditsCodename.hx delete mode 100644 source/funkin/menus/credits/CreditsMain.hx create mode 100644 source/funkin/menus/ui/Slider.hx create mode 100644 source/funkin/options/OptionsTree.hx create mode 100644 source/funkin/options/type/Separator.hx diff --git a/assets/languages/en/Options.xml b/assets/languages/en/Options.xml index f0ec9ea7..ad8a02f0 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. @@ -62,8 +63,15 @@ 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. + + 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 + Following option is locked for Quality Option, to unlock set Quality to CUSTOM Colored Healthbar If unchecked, the game will use the original red/green health bar from the week 6 fnf game. @@ -71,17 +79,19 @@ 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. + + 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. + 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. + 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. + 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. + diff --git a/source/funkin/backend/system/Controls.hx b/source/funkin/backend/system/Controls.hx index 0ccaac63..5f6f8f98 100644 --- a/source/funkin/backend/system/Controls.hx +++ b/source/funkin/backend/system/Controls.hx @@ -191,7 +191,7 @@ class Controls extends FlxActionSet setKeyboardScheme(scheme, false); } - public function getActionFromControl(control:Control):FLxAction return macro_getActionFromControl(control); + public function getActionFromControl(control:Control):FlxAction return macro_getActionFromControl(control); public function getKeyName(control:Control, idx:Int = 0):String { diff --git a/source/funkin/editors/DebugOptions.hx b/source/funkin/editors/DebugOptions.hx index 93d85992..0f3dc9fd 100644 --- a/source/funkin/editors/DebugOptions.hx +++ b/source/funkin/editors/DebugOptions.hx @@ -10,71 +10,9 @@ class DebugOptions extends TreeMenu { super.create(); FlxG.camera.fade(0xFF000000, 0.5, true); - - var bg:FlxSprite = 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(); - bg.antialiasing = true; - add(bg); - - main = new DebugOptionsScreen(); } } class DebugOptionsScreen extends OptionsScreen { - public override function new() { - prefix = "DebugOptions."; - super(translate("title"), translate("desc"), prefix); - #if windows - add(new TextOption( - getName("showConsole"), - getDesc("showConsole"), - function() { - NativeAPI.allocConsole(); - })); - #end - 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("editorPrettyPrint"), - getDesc("editorPrettyPrint"), - "editorPrettyPrint")); - 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")); - } + } \ No newline at end of file diff --git a/source/funkin/editors/EditorTreeMenu.hx b/source/funkin/editors/EditorTreeMenu.hx index 7bf5a0fb..b4904866 100644 --- a/source/funkin/editors/EditorTreeMenu.hx +++ b/source/funkin/editors/EditorTreeMenu.hx @@ -9,6 +9,8 @@ class EditorTreeMenu extends TreeMenu { public var bgType:String = "default"; public var bgMovement:FlxPoint = new FlxPoint(); + public var main:OptionsScreen; + public override function create() { super.create(); @@ -31,4 +33,8 @@ class EditorTreeMenu extends TreeMenu { public override function exit() { FlxG.switchState(new MainMenuState()); } + + public function onMenuChange() { + + } } \ No newline at end of file diff --git a/source/funkin/menus/MainMenuState.hx b/source/funkin/menus/MainMenuState.hx index 1a68f4a2..1a931781 100644 --- a/source/funkin/menus/MainMenuState.hx +++ b/source/funkin/menus/MainMenuState.hx @@ -6,7 +6,7 @@ import flixel.tweens.FlxTween; import funkin.backend.FunkinText; import funkin.backend.scripting.events.menu.MenuChangeEvent; import funkin.backend.scripting.events.NameEvent; -import funkin.menus.credits.CreditsMain; +//import funkin.menus.credits.CreditsMain; import funkin.options.OptionsMenu; import lime.app.Application; @@ -168,7 +168,7 @@ class MainMenuState extends MusicBeatState { case 'story mode': FlxG.switchState(new StoryMenuState()); case 'freeplay': FlxG.switchState(new FreeplayState()); - case 'donate', 'credits': FlxG.switchState(new CreditsMain()); // kept donate for not breaking scripts, if you don't want donate to bring you to the credits menu, thats easy softcodable - Nex + //case 'donate', 'credits': FlxG.switchState(new CreditsMain()); // kept donate for not breaking scripts, if you don't want donate to bring you to the credits menu, thats easy softcodable - Nex case 'options': FlxG.switchState(new OptionsMenu()); } }); 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 deleted file mode 100644 index 70e172dc..00000000 --- a/source/funkin/menus/credits/CreditsCodename.hx +++ /dev/null @@ -1,136 +0,0 @@ -package funkin.menus.credits; - -import flixel.text.FlxText; -import flixel.util.FlxColor; -import funkin.backend.system.github.GitHub; -import funkin.backend.system.github.GitHubContributor.CreditsGitHubContributor; -import funkin.options.PlayerSettings; -import funkin.options.type.GithubIconOption; - -using StringTools; - -class CreditsCodename extends funkin.options.OptionsScreen { - 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")); - 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) { - super.update(elapsed); - - if (_downloadingSteps == 2) { - _downloadingSteps = 0; - _canReset = true; - displayList(); - } else if (_downloadingSteps == 1) { - _downloadingSteps = 0; - _canReset = true; - updateMenuDesc(); - } - 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; - else _downloadingSteps = 1; - }); - } - - public override function updateMenuDesc(?txt:String) { - 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 = [ - 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() { - super.close(); - for (frmt in contribFormats) parent.treeParent.pathDesc.removeFormat(frmt.format); - } - - public function checkUpdate():Bool { - var curTime:Float = Date.now().getTime(); - if(Options.lastUpdated != null && curTime < Options.lastUpdated + 120000) return false; // Fuck you Github rate limits - Nex - Options.lastUpdated = curTime; - - error = false; - var idk = GitHub.getContributors(Flags.REPO_OWNER, Flags.REPO_NAME, function(e) { - error = true; - var errMsg:String = 'Error while trying to download contributors list:\n${CoolUtil.removeIP(e.message)}'; - Logs.error(errMsg.replace('\n', ' ')); - funkin.backend.utils.NativeAPI.showMessageBox("Codename Engine Warning", errMsg, MSG_WARNING); - }); - if(error) return false; - if((idk is Array)) { - var contributors:Array = []; - for(e in idk) contributors.push({ - login: e.login, - avatar_url: e.avatar_url, - html_url: e.html_url, - id: e.id, - contributions: e.contributions - }); - Options.contributors = contributors; - } - Logs.verbose('[CreditsCodename] Contributors list Updated!'); - - var errorOnMain:Bool = false; - var idk2 = GitHub.getOrganizationMembers(Flags.REPO_OWNER, function(e) { - errorOnMain = true; - var errMsg:String = 'Error while trying to download ${Flags.REPO_OWNER} members list:\n${CoolUtil.removeIP(e.message)}'; - Logs.error(errMsg.replace('\n', ' ')); - funkin.backend.utils.NativeAPI.showMessageBox("Codename Engine Warning", errMsg, MSG_WARNING); - }); - if(!errorOnMain) { - Options.mainDevs = [for(m in idk2) m.id]; - Logs.verbose('[CreditsCodename] Main Devs list Updated!'); - } - - return true; - } - - public function displayList() { - //if (curSelected > Options.contributors.length - 1) changeSelection(-(curSelected - (Options.contributors.length - 1))); - if (curSelected > Options.contributors.length - 1) curSelected = Options.contributors.length - 1; - changeSelection(0, true); - - while (members.length > 0) { - members[0].destroy(); - remove(members[0], true); - } - - totalContributions = 0; - for(c in Options.contributors) totalContributions += c.contributions; - for(c in Options.contributors) { - var text = TU.translate("credits.totalContributions", [c.contributions, totalContributions, FlxMath.roundDecimal(c.contributions / totalContributions * 100, 2)]); - var opt:GithubIconOption = new GithubIconOption(c, text); - if(Options.mainDevs.contains(c.id)) { - opt.desc += TU.translate("credits.mainDev"); - @:privateAccess opt.__text.color = Flags.MAIN_DEVS_COLOR; - } - add(opt); - } - - updateMenuDesc(); - } -} \ No newline at end of file diff --git a/source/funkin/menus/credits/CreditsMain.hx b/source/funkin/menus/credits/CreditsMain.hx deleted file mode 100644 index 76485cbf..00000000 --- a/source/funkin/menus/credits/CreditsMain.hx +++ /dev/null @@ -1,106 +0,0 @@ -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.type.*; -import haxe.xml.Access; - -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(); - bg.antialiasing = true; - add(bg); - - for (i in funkin.backend.assets.ModsFolder.getLoadedMods()) { - var xmlPath = Paths.xml('config/credits/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('[CreditsMain] Error while parsing credits.xml: ${Std.string(e)}', ERROR); - if (access != null) for (c in parseCreditsFromXML(access)) items.push(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"]); - } - - /** - * 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"); - - if (node.name == "github") { - if (!node.has.user) { - Logs.warn("A github node requires a user attribute.", "CreditsMain"); - continue; - } - - var username = node.getAtt("user"); - var user:CreditsGitHubContributor = { // Kind of forcing - login: username, - html_url: 'https://github.com/$username', - avatar_url: 'https://github.com/$username.png' - }; - var opt:GithubIconOption = new GithubIconOption(user, desc, null, - node.has.customName ? node.att.customName : null, - node.has.size ? Std.parseInt(node.att.size) : 96, - node.has.portrait ? node.att.portrait.toLowerCase() == "false" ? false : true : true - ); - if (node.has.color) - @:privateAccess opt.__text.color = FlxColor.fromString(node.att.color); - credsMenus.push(opt); - } else { - if (!node.has.name) { - Logs.warn("A credit node requires a name attribute.", "CreditsMain"); - continue; - } - var name = node.getAtt("name"); - - switch(node.name) { - case "credit": - var opt:PortraitOption = new PortraitOption(name, desc, function() if(node.has.url) CoolUtil.openURL(node.att.url), - node.has.icon && Paths.assetsTree.existsSpecific(Paths.image('credits/${node.att.icon}'), "IMAGE", source) ? - FlxG.bitmap.add(Paths.image('credits/${node.att.icon}')) : null, node.has.size ? Std.parseInt(node.att.size) : 96, - node.has.portrait ? node.att.portrait.toLowerCase() == "false" ? false : true : true - ); - if (node.has.color) - @:privateAccess opt.__text.color = FlxColor.fromString(node.att.color); - credsMenus.push(opt); - - case "menu": - credsMenus.push(new TextOption(name + " >", desc, function() { - optionsTree.add(new OptionsScreen(name, desc, parseCreditsFromXML(node, source))); - })); - } - } - } - - return credsMenus; - } -} diff --git a/source/funkin/menus/ui/Slider.hx b/source/funkin/menus/ui/Slider.hx new file mode 100644 index 00000000..45360ce7 --- /dev/null +++ b/source/funkin/menus/ui/Slider.hx @@ -0,0 +1,5 @@ +package funkin.menus.ui; + +class Slider extends FlxSprite { + +} \ No newline at end of file diff --git a/source/funkin/options/Options.hx b/source/funkin/options/Options.hx index bd39cd45..9551c51c 100644 --- a/source/funkin/options/Options.hx +++ b/source/funkin/options/Options.hx @@ -44,6 +44,7 @@ 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; #if MODCHARTING_FEATURES public static var modchartingHoldSubdivisions:Int = 4; #end @@ -207,7 +208,19 @@ 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.game.stage.quality = (FlxG.enableAntialiasing = antialiasing) ? BEST : LOW; FlxG.autoPause = autoPause; if (FlxG.updateFramerate < framerate) FlxG.drawFramerate = FlxG.updateFramerate = framerate; else FlxG.updateFramerate = FlxG.drawFramerate = framerate; diff --git a/source/funkin/options/OptionsMenu.hx b/source/funkin/options/OptionsMenu.hx index 9f35c36f..ae184e0c 100644 --- a/source/funkin/options/OptionsMenu.hx +++ b/source/funkin/options/OptionsMenu.hx @@ -1,12 +1,15 @@ package funkin.options; +import haxe.xml.Access; +import flixel.util.typeLimit.OneOfThree; +import funkin.editors.ui.UIState; import funkin.options.categories.*; import funkin.options.type.*; typedef OptionCategory = { var name:String; var desc:String; - var ?state:OneOfThree, (name:String, desc:String) -> OptionsScreen>; + var ?state:OneOfThree, (name:String, desc:String) -> TreeMenuScreen>; var ?substate:OneOfThree, (name:String, desc:String) -> MusicBeatSubstate>; var ?suffix:String; } @@ -16,8 +19,8 @@ class OptionsMenu extends TreeMenu { { // name and desc are actually the translations ids! - Nex name: 'optionsTree.controls-name', desc: 'optionsTree.controls-desc', - suffix: ''; - substate: funkin.options.keybinds.KeybindsOptions; + suffix: '', + substate: funkin.options.keybinds.KeybindsOptions }, { name: 'optionsTree.gameplay-name', @@ -43,7 +46,7 @@ class OptionsMenu extends TreeMenu { } ]; - var bg:FunkinSprite; + var bg:FlxSprite; override function create() { super.create(); @@ -52,23 +55,54 @@ class OptionsMenu extends TreeMenu { DiscordUtil.call("onMenuLoaded", ["Options Menu"]); - add(bg = new FunkinSprite().loadAnimatedGraphic(Paths.image('menus/menuBGBlue'))); + add(bg = new FlxSprite().loadAnimatedGraphic(Paths.image('menus/menuBGBlue'))); bg.antialiasing = true; bg.scrollFactor.set(); updateBG(); - /*tree = [ - for (o in mainOptions) { + 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])); + } + } + })])); + 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 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.scale.x = bg.scale.y = Math.max(scaleX, scaleY) * 1.15; bg.screenCenter(); } @@ -84,4 +118,59 @@ class OptionsMenu extends TreeMenu { 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 TreeMenuScreen(name, desc, parseOptionsFromXML(node))); + })); + } + } + + return options; + } + */ } \ No newline at end of file diff --git a/source/funkin/options/OptionsScreen.hx b/source/funkin/options/OptionsScreen.hx index 53dd5b06..672feb40 100644 --- a/source/funkin/options/OptionsScreen.hx +++ b/source/funkin/options/OptionsScreen.hx @@ -1,3 +1,5 @@ +// REMOVE THIS WHEN COMPLETE + package funkin.options; import funkin.options.type.OptionType; @@ -92,13 +94,13 @@ class OptionsScreen extends FlxTypedSpriteGroup { if (members.length > 0) { members[curSelected].selected = true; if (controls.ACCEPT || (FlxG.mouse.justReleased && Main.timeSinceFocus > 0.25)) { - members[curSelected].onSelect(); + members[curSelected].select(); onSelect(members[curSelected]); } if (controls.LEFT_P) - members[curSelected].onChangeSelection(-1); + members[curSelected].changeSelection(-1); if (controls.RIGHT_P) - members[curSelected].onChangeSelection(1); + members[curSelected].changeSelection(1); } if (controls.BACK || FlxG.mouse.justReleasedRight) close(); diff --git a/source/funkin/options/OptionsTree.hx b/source/funkin/options/OptionsTree.hx new file mode 100644 index 00000000..1f0bfcc7 --- /dev/null +++ b/source/funkin/options/OptionsTree.hx @@ -0,0 +1,91 @@ +// REMOVE THIS WHEN COMPLETE + +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 ba818c36..befed063 100644 --- a/source/funkin/options/TreeMenu.hx +++ b/source/funkin/options/TreeMenu.hx @@ -1,6 +1,10 @@ package funkin.options; +import flixel.tweens.FlxTween; +import flixel.util.FlxArrayUtil; +import flixel.util.FlxColor; import flixel.util.FlxSignal; +import funkin.backend.FunkinText; import funkin.backend.system.framerate.Framerate; import funkin.editors.ui.UIState; @@ -8,7 +12,8 @@ interface ITreeOption { var desc:String; var selected:Bool; - function changeSelection(change:Int); + function reloadStrings():Void; + function changeSelection(change:Int):Void; function select():Void; } @@ -16,9 +21,9 @@ class TreeMenu extends UIState { public var onMenuClosed:FlxTypedSignalVoid> = new FlxTypedSignal(); public var onMenuChanged:FlxTypedSignalVoid> = new FlxTypedSignal(); - public var tree(default, null):Array; - public var length(default, null):Int = 0; - public var lastMenu:Null; + public var tree(default, null):Array = []; + public var treeLength(default, null):Int = 0; + public var previousMenus:Array = []; public var destroyMenus:Bool = true; public var exitCallback:TreeMenu->Void; @@ -27,10 +32,13 @@ class TreeMenu extends UIState { public var descLabel:FunkinText; public var bgLabel:FlxSprite; - var menuChangeTween:FlxTween; - var _drawer:TreeMenuDrawer; + public var optionsTree:OptionsTree; - public function new(exitCallback:TreeMenu->Void, + var menuChangeTween:FlxTween; + var __drawer:TreeMenuDrawer; + var __treeCreated:Bool = false; + + public function new(?exitCallback:TreeMenu->Void, scriptsAllowed:Bool = true, ?scriptName:String) { super(scriptsAllowed, scriptName); @@ -44,40 +52,44 @@ class TreeMenu extends UIState { bgLabel.alpha = 0.25; bgLabel.scrollFactor.set(); - titleLabel = new FunkinText(4, 4, FlxG.camera.width - 8, 32); + titleLabel = new FunkinText(4, 4, FlxG.width - 8, 32); titleLabel.borderSize = 1.25; titleLabel.scrollFactor.set(); - descLabel = new FunkinText(4, 0, FlxG.camera.width - 8, 16); + descLabel = new FunkinText(4, 0, FlxG.width - 8, 16); descLabel.scrollFactor.set(); } override function createPost() { super.createPost(); - if ((length = tree.length) != 0) { + if ((treeLength = 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")); + 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(__drawer = new TreeMenuDrawer(this)); add(bgLabel); add(titleLabel); add(descLabel); updateLabels(); + + __treeCreated = true; } 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; + 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; } - if (menuChangeTween != null && menuChangeTween.active) menuChangeTween.cancel(); - if (cameraScroll) FlxG.camera.scroll.x = lastMenu.x; + if (cameraScroll && last != null) { + if (menuChangeTween != null && menuChangeTween.active) menuChangeTween.cancel(); + FlxG.camera.scroll.x = last.x; + } } public function updateLabels() { @@ -96,100 +108,102 @@ class TreeMenu extends UIState { 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; + descLabel.text += "\n" + cast(last.members[last.curSelected], ITreeOption).desc; - bgLabel.scale.set(FLxG.width, descLabel.y + descLabel.height + 2); + bgLabel.scale.set(FlxG.width, descLabel.y + descLabel.height + 2); bgLabel.updateHitbox(); Framerate.offset.y = bgLabel.height + 2; } - public function add(menu:TreeMenuScreen):TreeMenuScreen { + 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; - length++; + treeLength++; - var prev = tree[length - 1]; - menu.x = prev == null ? 0 : prev.x + Math.max(FlxG.camera.width, prev.width); + var prev = tree[treeLength - 2]; + menu.x = prev == null ? 0 : prev.x + Math.max(FlxG.width, prev.width); + destroyPreviousMenus(); menuChanged(); menu.inputEnabled = true; - prev.inputEnabled = false; + if (prev != null) prev.inputEnabled = false; return menu; } - public function insert(position:Int, menu:TreeMenuScreen):TreeMenuScreen { + public function insertMenu(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); + if (position < 0) position = treeLength - ((-position - 1) % treeLength); + tree.insert(position, menu); + if (!__treeCreated) return menu; + menu.parent = this; - var lastChanged = position >= length++; + var lastChanged = position >= treeLength++; updateMenuPositions(position, !lastChanged); if (lastChanged) { + destroyPreviousMenus(); menuChanged(); menu.inputEnabled = true; - tree[length - 1].inputEnabled = false; + if (treeLength > 0) tree[treeLength - 2].inputEnabled = false; } return menu; } - public function pop():TreeMenuScreen { - (lastMenu = tree.pop(position)).parent = null; - length--; + public function popMenu():TreeMenuScreen + return removeMenuPosition(treeLength - 1); - menuChanged(); - lastMenu.inputEnabled = false; - menu.inputEnabled = true; + public function removeMenu(menu:TreeMenuScreen):TreeMenuScreen + return if (menu == null) null; else removeMenuPosition(tree.indexOf(menu)); - return lastMenu; - } + public function removeMenuPosition(position:Int):TreeMenuScreen { + if (position < 0 || position >= treeLength || treeLength == 0) return null; - public function remove(menu:TreeMenuScreen):TreeMenuScreen { - return if (menu == null) null; else removePosition(tree.indexOf(menu)); - } + tree[position] = tree[tree.length - 1]; + var menu = tree.pop(); + menu.parent = null; - public function removePosition(position:Int):TreeMenuScreen { - if (position < 0 || position >= length) return null; + if (!__treeCreated) return menu; - destroyLastMenu(); - (lastMenu = tree.swapAndPop(position)).parent = null; - - if (position == --length) { + previousMenus.push(menu); + if (position == --treeLength) { menuChanged(); - lastMenu.inputEnabled = false; - tree[length - 1].inputEnabled = true; + menu.inputEnabled = false; + if (treeLength > 0) tree[treeLength - 1].inputEnabled = true; } else { updateMenuPositions(position, true); updateLabels(); } - return lastMenu; + onMenuClosed.dispatch(menu); + return menu; } public function menuChanged() { - if (length == 0) exit(); + 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; - lastMenu.transitioning = false; + for (menu in previousMenus) if (menu != null) menu.transitioning = false; }}); for (menu in tree) if (menu != null) menu.transitioning = true; - lastMenu.transitioning = true; + for (menu in previousMenus) if (menu != null) menu.transitioning = true; } } @@ -197,14 +211,18 @@ class TreeMenu extends UIState { super.update(elapsed); var i = 0, menu:TreeMenuScreen; - while (i < length) { + while (i < treeLength) { if ((menu = tree[i++]) == null || !menu.active || !menu.exists) continue; - if (i == length || menu.persistentUpdate || menu.transitioning) menu.update(elapsed); + if (i == treeLength || menu.persistentUpdate || menu.transitioning) menu.update(elapsed); } - if (lastMenu != null) { - if (lastMenu.transitioning) lastMenu.update(elapsed); - else destroyLastMenu(); + 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. @@ -213,7 +231,7 @@ class TreeMenu extends UIState { override function destroy() { super.destroy(); - destroyLastMenu(); + destroyPreviousMenus(); } override function onResize(width:Int, height:Int) { @@ -226,21 +244,23 @@ class TreeMenu extends UIState { } updateMenuPositions(true); - descLabel.width = titleLabel.width = (bgLabel.width = width) - 8; + descLabel.width = titleLabel.width = (bgLabel.scale.x = width) - 8; + bgLabel.updateHitbox(); } public function reloadStrings() { for (menu in tree) if (menu != null) menu.reloadStrings(); } - public function destroyLastMenu() { - lastMenu = destroyMenus ? FlxDestroyUtil.destroy(lastMenu) : null; + public function destroyPreviousMenus() { + for (menu in previousMenus) menu.destroy(); + previousMenus.resize(0); } public function exit() { if (exitCallback != null) return exitCallback(this); - FlxG.switchState(new MainMenuState()); + FlxG.switchState(new funkin.menus.MainMenuState()); } public function updateAll(elapsed:Float) { @@ -253,17 +273,18 @@ class TreeMenu extends UIState { final class TreeMenuDrawer extends FlxBasic { public var parent:TreeMenu; public function new(parent:TreeMenu) { + super(); this.parent = parent; - moves = false; } override function draw() { var i = 0, menu:TreeMenuScreen; - while (i < parent.length) { + while (i < parent.treeLength) { if ((menu = parent.tree[i++]) == null || !menu.active || !menu.exists) continue; - if (i == length || menu.persistentUpdate || menu.transitioning) menu.update(elapsed); + if (i == parent.treeLength || menu.persistentUpdate || menu.transitioning) menu.draw(); } - if (parent.lastMenu != null) parent.lastMenu.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 index 7fa6e19f..b03ab959 100644 --- a/source/funkin/options/TreeMenuScreen.hx +++ b/source/funkin/options/TreeMenuScreen.hx @@ -1,9 +1,11 @@ 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.type.Separator; class TreeMenuScreen extends FlxSpriteGroup { public var persistentUpdate:Bool = false; @@ -15,7 +17,6 @@ class TreeMenuScreen extends FlxSpriteGroup { 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; @@ -28,74 +29,97 @@ class TreeMenuScreen extends FlxSpriteGroup { 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; + rawName = v; + name = TU.exists(v) ? TU.translate(v) : v; return v; } function set_rawDesc(v:String) { - if (rawDesc == (rawDesc = v)) return v; - desc = TU.exists(rawDesc) ? TU.translate(rawDesc) : rawDesc; + 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]); - var rightTurboControl:TurboControls = new TurboControls([Control.RIGHT]); + var leftTurboControl:TurboControls = new TurboControls([Control.LEFT], 0.2, 1 / 30); + var rightTurboControl:TurboControls = new TurboControls([Control.RIGHT], 0.2, 1 / 30); var upTurboControl:TurboControls = new TurboControls([Control.UP]); var downTurboControl:TurboControls = new TurboControls([Control.DOWN]); - var turboBasics:Array; + var turboBasics:Array; var curOption:ITreeOption; - var _firstFrame:Bool = true; + var __firstFrame:Bool = true; - public function new(name:String, desc:String, ?prefix:String, ?options:Array) { + 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() { - name = TU.exists(rawName) ? TU.translate(rawName) : rawName; - desc = TU.exists(rawDesc) ? TU.translate(rawDesc) : rawDesc; + rawName = rawName; + rawDesc = rawDesc; - for (object in members) if (object != null) object.reloadStrings(); + for (object in members) if (object != null && object is ITreeOption) cast(object, ITreeOption).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 (__firstFrame) { + __firstFrame = false; + if (members[curSelected] is ITreeOption) (curOption = cast members[curSelected]).selected = true; + updateItems(true); + return; } 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 (length > 0 && curOption != null) { + if (controls.ACCEPT || (FlxG.mouse.justPressed && FlxG.mouse.overlaps(members[curSelected]))) curOption.select(); if (leftTurboControl.activated) curOption.changeSelection(-1); if (rightTurboControl.activated) curOption.changeSelection(1); } - if (controls.BACK || FlxG.mouse.justPressedRight) close(); + 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; } - 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; + updateItem(); + y += itemHeight; + } + + y = initY; + i = curSelected; + while (i-- > 0) if ((object = members[i]) != null) { + y -= itemHeight; + updateItem(); } } @@ -103,7 +127,7 @@ class TreeMenuScreen extends FlxSpriteGroup { onClose.dispatch(); if (parent == null) return destroy(); - else parent.remove(this); + else parent.removeMenu(this); CoolUtil.playMenuSFX(CANCEL); } @@ -111,8 +135,12 @@ class TreeMenuScreen extends FlxSpriteGroup { 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; + 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; updateMenuDesc(); CoolUtil.playMenuSFX(SCROLL); diff --git a/source/funkin/options/categories/AppearanceOptions.hx b/source/funkin/options/categories/AppearanceOptions.hx index 0131a73d..125509a5 100644 --- a/source/funkin/options/categories/AppearanceOptions.hx +++ b/source/funkin/options/categories/AppearanceOptions.hx @@ -1,51 +1,24 @@ package funkin.options.categories; class AppearanceOptions extends TreeMenuScreen { - public override function new() { + public function new() { super('optionsTree.appearance-name', 'optionsTree.appearance-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 - */ + add(new NumOption(getNameID('framerate'), getDescID('framerate'), + 30, 240, 1, + 'framerate', __changeFPS + )); + add(new Checkbox(getNameID('flashingMenu'), getDescID('flashingMenu'), 'flashingMenu')); + add(new ArrayOption(getNameID('quality'), getDescID('quality'), + [0, 1, 2], [getID('quality-low'), getID('quality-high'), getID('quality-custom')], + 'quality', __changeQuality, null + )); + 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(value:Float) { @@ -53,4 +26,30 @@ class AppearanceOptions extends TreeMenuScreen { if (FlxG.updateFramerate < framerate) FlxG.drawFramerate = FlxG.updateFramerate = framerate; else FlxG.updateFramerate = FlxG.drawFramerate = framerate; } + + private function __changeQuality(value:Dynamic) { + var antialiasing = value == 0 ? false : (value == 1 ? true : Options.antialiasing); + FlxG.game.stage.quality = (FlxG.enableAntialiasing = antialiasing) ? BEST : LOW; + } +} + +class AdvancedAppearanceOptions extends TreeMenuScreen { + public function new() { + super('optionsMenu.advanced', 'optionsTree.appearance.advanced-desc', "AppearanceOptions.Advanced."); + + for (option in [ + new Checkbox(getNameID('antialiasing'), getDescID('antialiasing'), 'antialiasing', __changeAntialiasing), + new Checkbox(getNameID('lowMemoryMode'), getDescID('lowMemoryMode'), 'lowMemoryMode'), + new Checkbox(getNameID('gameplayShaders'), getDescID('gameplayShaders'), 'gameplayShaders') + ]) { + add(option); + if (option.locked = Options.quality != 2) option.rawDesc = 'AppearanceOptions.quality-optionDisabled-desc'; + } + + add(new Checkbox(getNameID('gpuOnlyBitmaps'), getDescID('gpuOnlyBitmaps'), 'gpuOnlyBitmaps')); + } + + 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/GameplayOptions.hx b/source/funkin/options/categories/GameplayOptions.hx index 3064a588..0abcbae1 100644 --- a/source/funkin/options/categories/GameplayOptions.hx +++ b/source/funkin/options/categories/GameplayOptions.hx @@ -3,96 +3,8 @@ package funkin.options.categories; import flixel.util.FlxTimer; import funkin.backend.system.Conductor; -class GameplayOptions extends OptionsScreen { - 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 - add(new TextOption('optionsMenu.advanced', 'optionsTree.gameplay.advanced-desc', ' >', () -> - parent.add(new AdvancedGameplayOptions('optionsMenu.advanced', 'optionsTree.gameplay.advanced-desc')))); - } - - private function __changeOffset(offset) - Conductor.songOffset = offset; - - var __lastBeat:Int = 0; - var __lastSongBeat:Int = 0; - - override function update(elapsed) { - super.update(elapsed); - FlxG.camera.zoom = CoolUtil.fpsLerp(FlxG.camera.zoom, 1, 0.04); - - if (offsetSetting.selected) { - FlxG.sound.music.volume = 0.5; - if (__lastBeat != Conductor.curBeat) { - FlxG.camera.zoom += 0.03; - __lastBeat = Conductor.curBeat; - } - - var beat = Math.floor(Conductor.getTimeInBeats(FlxG.sound.music.time)); - if (__lastSongBeat != beat) { - __metronome.replay(); - __lastSongBeat = beat; - } - } - else - FlxG.sound.music.volume = 1; - } - - override function close() { - FlxG.camera.zoom = 1; - FlxG.sound.music.volume = 1; - super.close(); - } +class GameplayOptions extends TreeMenuScreen { } 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")); - } } diff --git a/source/funkin/options/categories/LanguageOptions.hx b/source/funkin/options/categories/LanguageOptions.hx index b3e64433..d166643e 100644 --- a/source/funkin/options/categories/LanguageOptions.hx +++ b/source/funkin/options/categories/LanguageOptions.hx @@ -33,7 +33,7 @@ class LanguageRadio extends RadioButton { } } -class LanguageOptions extends OptionsScreen { +class LanguageOptions extends TreeMenuScreen { public override function new(title:String, desc:String) { super(title, desc, "LanguageOptions."); diff --git a/source/funkin/options/categories/MiscOptions.hx b/source/funkin/options/categories/MiscOptions.hx index 95b55aef..ffa8eadd 100644 --- a/source/funkin/options/categories/MiscOptions.hx +++ b/source/funkin/options/categories/MiscOptions.hx @@ -1,35 +1,4 @@ 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")); - #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")); - } - })); - #end - add(new TextOption( - getName("resetSaveData"), - getDesc("resetSaveData"), - function() { - // TODO: SAVE DATA RESETTING - })); - } +class MiscOptions extends TreeMenuScreen { } diff --git a/source/funkin/options/type/ArrayOption.hx b/source/funkin/options/type/ArrayOption.hx index 81184aeb..b143e16b 100644 --- a/source/funkin/options/type/ArrayOption.hx +++ b/source/funkin/options/type/ArrayOption.hx @@ -1,7 +1,7 @@ package funkin.options.type; -class ArrayOption extends OptionType { - public var selectCallback:String->Void; +class ArrayOption extends TextOption { + public var changedCallback:String->Void; public var options:Array; public var displayOptions:Array; @@ -10,36 +10,32 @@ class ArrayOption extends OptionType { public var parent:Dynamic; public var optionName:String; - var __text:Alphabet; var __selectionText:Alphabet; - 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; + override function set_text(v:String) { + super.set_text(v); + __selectionText.x = __text.x + __text.width + 12; + return v; + } + + 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.parent = parent = parent != null ? parent : 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)); - - add(__text = new Alphabet(20, 0, text, 'bold')); - add(__selectionText = new Alphabet(__text.x + __text.width + 12, 0, formatTextOption(), 'bold')); + + __selectionText = new Alphabet(0, 20, formatTextOption(), 'bold'); + super(text, desc); + add(__selectionText); } override function reloadStrings() { - super.reloadStrings(); - __selectiontext.x = __text.x + __text.width + 12; - __selectiontext.text = formatTextOption(); - } - - override function changeSelection(change:Int) { - currentSelection = FlxMath.wrap(currentSelection + change, 0, options.length - 1); __selectionText.text = formatTextOption(); - CoolUtil.playMenuSFX(SCROLL); - - if (selectCallback != null) selectCallback(options[currentSelection]); + super.reloadStrings(); } function formatTextOption() { @@ -54,4 +50,16 @@ class ArrayOption extends OptionType { return s; } + + override function changeSelection(change:Int) { + if (locked) return; + currentSelection = FlxMath.wrap(currentSelection + change, 0, options.length - 1); + __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..5ac65391 100644 --- a/source/funkin/options/type/Checkbox.hx +++ b/source/funkin/options/type/Checkbox.hx @@ -10,25 +10,21 @@ 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); + public function new(text:String, desc:String, optionName:String, ?selectCallback:Void->Void = null, ?parent:Dynamic) { + super(text, desc); + this.optionName = optionName; + this.parent = parent = parent != null ? parent : Options; - if (parent == null) - parent = Options; - - this.parent = parent; - - checkbox = new FlxSprite(10, -40); + add(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,61 +33,52 @@ 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; + + var fieldValue = Reflect.field(parent, optionName); + checked = fieldValue != null && fieldValue; } public var firstFrame:Bool = true; public override function update(elapsed:Float) { - if(checkbox.animation.curAnim == null) { - checkbox.animation.play(checked ? "checked" : "unchecked", true); - } + 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) { + if (checked != v) { checked = v; - if(!firstFrame) { - checkbox.animation.play(checked ? "checking" : "unchecking", true); - } + if (!firstFrame) checkbox.animation.play(checked ? "checking" : "unchecking", true); } return v; } - public override function onSelect() { + public 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() { 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/NumOption.hx b/source/funkin/options/type/NumOption.hx index e570e197..70848275 100644 --- a/source/funkin/options/type/NumOption.hx +++ b/source/funkin/options/type/NumOption.hx @@ -1,72 +1,54 @@ 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 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)); + 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; + + var fieldValue = Reflect.field(parent, optionName); + if (fieldValue != null) currentSelection = fieldValue; + + __number = new Alphabet(0, 20, ': $currentSelection', 'bold'); + super(text, desc); + add(__number); + } + + override function changeSelection(change:Int):Void { + if (locked) return; + if (currentSelection == (currentSelection = FlxMath.bound(currentSelection + change, min, max))) return; __number.text = ': $currentSelection'; Reflect.setField(parent, optionName, currentSelection); - if(selectCallback != null) - selectCallback(currentSelection); + if (changedCallback != null) changedCallback(currentSelection); + + 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 7429a64a..41f4302c 100644 --- a/source/funkin/options/type/OptionType.hx +++ b/source/funkin/options/type/OptionType.hx @@ -10,15 +10,25 @@ import funkin.options.TreeMenu.ITreeOption; **/ 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 var itemHeight:Float = 120; + public function new(text:String, desc:String) { super(); - reloadStrings(); + 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; @@ -46,4 +56,6 @@ class OptionType extends FlxSpriteGroup implements ITreeOption { 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/RadioButton.hx b/source/funkin/options/type/RadioButton.hx index d43240ac..ac48eafa 100644 --- a/source/funkin/options/type/RadioButton.hx +++ b/source/funkin/options/type/RadioButton.hx @@ -101,7 +101,9 @@ class RadioButton extends TextOption { return v; } - public override function onSelect() { + public override function select() { + if (locked) return; + checked = true; if(checked) { onSet(value); 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 index 37f4f4fc..5831a47d 100644 --- a/source/funkin/options/type/SliderOption.hx +++ b/source/funkin/options/type/SliderOption.hx @@ -13,10 +13,7 @@ class SliderOption extends OptionType { //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; + override function set_text(v:String) return __text.text = text = v; var __text:Alphabet; var optionName:String; @@ -24,12 +21,6 @@ class SliderOption extends OptionType { 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; + super(text, desc); } } \ No newline at end of file diff --git a/source/funkin/options/type/TextOption.hx b/source/funkin/options/type/TextOption.hx index 2d2b5187..74ccfc2b 100644 --- a/source/funkin/options/type/TextOption.hx +++ b/source/funkin/options/type/TextOption.hx @@ -6,46 +6,29 @@ import flixel.effects.FlxFlicker; * Option type that has text. **/ class TextOption extends OptionType { + public var suffix: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; + var __text:Alphabet; + override function set_text(v:String) { + __text.text = v + suffix; + return text = v; + } 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; + this.selectCallback = selectCallback; + + __text = new Alphabet(20, 20, "", "bold"); + + super(text, desc); + add(__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) + suffix; - return v; - } - - public override function draw() { - super.draw(); - } - 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 From 83460a86cb198a48c007ccf83f172d2786c2dd79 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sat, 26 Jul 2025 18:56:54 +0700 Subject: [PATCH 03/25] forgot --- source/funkin/backend/TurboControls.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/backend/TurboControls.hx b/source/funkin/backend/TurboControls.hx index 2c1f4ec0..b8bedf22 100644 --- a/source/funkin/backend/TurboControls.hx +++ b/source/funkin/backend/TurboControls.hx @@ -106,7 +106,7 @@ class TurboButtons extends TurboBasic { 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; + this.gamepad = gamepad != null ? gamepad : FlxG.gamepads.firstActive; } override function get_pressed() { From 45721b8cf33ec491297ef5bd2bd61fa3eb8f0b4b Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sat, 26 Jul 2025 19:03:18 +0700 Subject: [PATCH 04/25] actions can you please work --- source/funkin/options/type/ArrayOption.hx | 3 +-- source/funkin/options/type/Checkbox.hx | 2 +- source/funkin/options/type/NumOption.hx | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/source/funkin/options/type/ArrayOption.hx b/source/funkin/options/type/ArrayOption.hx index b143e16b..52c33904 100644 --- a/source/funkin/options/type/ArrayOption.hx +++ b/source/funkin/options/type/ArrayOption.hx @@ -52,8 +52,7 @@ class ArrayOption extends TextOption { } override function changeSelection(change:Int) { - if (locked) return; - currentSelection = FlxMath.wrap(currentSelection + change, 0, options.length - 1); + if (locked || currentSelection == (currentSelection = CoolUtil.boundInt(currentSelection + change, 0, options.length - 1))) return; __selectionText.text = formatTextOption(); CoolUtil.playMenuSFX(SCROLL); diff --git a/source/funkin/options/type/Checkbox.hx b/source/funkin/options/type/Checkbox.hx index 5ac65391..0659314a 100644 --- a/source/funkin/options/type/Checkbox.hx +++ b/source/funkin/options/type/Checkbox.hx @@ -37,7 +37,7 @@ class Checkbox extends TextOption { __text.x = 100; var fieldValue = Reflect.field(parent, optionName); - checked = fieldValue != null && fieldValue; + checked = fieldValue != null ? fieldValue : false; } public var firstFrame:Bool = true; diff --git a/source/funkin/options/type/NumOption.hx b/source/funkin/options/type/NumOption.hx index 70848275..a98ac732 100644 --- a/source/funkin/options/type/NumOption.hx +++ b/source/funkin/options/type/NumOption.hx @@ -32,7 +32,7 @@ class NumOption extends TextOption { this.parent = parent = parent != null ? parent : Options; var fieldValue = Reflect.field(parent, optionName); - if (fieldValue != null) currentSelection = fieldValue; + currentSelection = fieldValue != null ? fieldValue : 0; __number = new Alphabet(0, 20, ': $currentSelection', 'bold'); super(text, desc); From ea85c71523fbf5145b4a8f9becb7ef59f57dfe4e Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sat, 26 Jul 2025 19:11:18 +0700 Subject: [PATCH 05/25] please actions --- source/funkin/options/type/Checkbox.hx | 3 ++- source/funkin/options/type/NumOption.hx | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/source/funkin/options/type/Checkbox.hx b/source/funkin/options/type/Checkbox.hx index 0659314a..5241686c 100644 --- a/source/funkin/options/type/Checkbox.hx +++ b/source/funkin/options/type/Checkbox.hx @@ -37,7 +37,8 @@ class Checkbox extends TextOption { __text.x = 100; var fieldValue = Reflect.field(parent, optionName); - checked = fieldValue != null ? fieldValue : false; + if (optionName != null) checked = Reflect.field(parent, optionName); + else checked = false; } public var firstFrame:Bool = true; diff --git a/source/funkin/options/type/NumOption.hx b/source/funkin/options/type/NumOption.hx index a98ac732..6590afd9 100644 --- a/source/funkin/options/type/NumOption.hx +++ b/source/funkin/options/type/NumOption.hx @@ -31,8 +31,7 @@ class NumOption extends TextOption { this.optionName = optionName; this.parent = parent = parent != null ? parent : Options; - var fieldValue = Reflect.field(parent, optionName); - currentSelection = fieldValue != null ? fieldValue : 0; + if (Reflect.field(parent, optionName) != null) currentSelection = Reflect.field(parent, optionName); __number = new Alphabet(0, 20, ': $currentSelection', 'bold'); super(text, desc); From 3dd23f069071e514ee7d7bdf9e61dc66900d3fba Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sat, 26 Jul 2025 20:45:40 +0700 Subject: [PATCH 06/25] Options Rewrite WIP All Options Done --- assets/languages/en/Options.xml | 3 + assets/languages/pl/Options.xml | 22 ++-- source/funkin/backend/system/MainState.hx | 2 +- source/funkin/options/Options.hx | 1 + source/funkin/options/TreeMenu.hx | 7 +- source/funkin/options/TreeMenuScreen.hx | 6 +- .../options/categories/AppearanceOptions.hx | 4 +- .../options/categories/GameplayOptions.hx | 63 ++++++++++- .../options/categories/LanguageOptions.hx | 37 +++---- .../funkin/options/categories/MiscOptions.hx | 24 ++++- source/funkin/options/type/Checkbox.hx | 21 ++-- source/funkin/options/type/IconOption.hx | 2 + source/funkin/options/type/NewOption.hx | 1 + source/funkin/options/type/PortraitOption.hx | 2 + source/funkin/options/type/RadioButton.hx | 102 +++++++----------- 15 files changed, 181 insertions(+), 116 deletions(-) diff --git a/assets/languages/en/Options.xml b/assets/languages/en/Options.xml index ad8a02f0..341559cb 100644 --- a/assets/languages/en/Options.xml +++ b/assets/languages/en/Options.xml @@ -147,6 +147,9 @@ Developer Mode If checked, you will be able to access developer features like certain keybinds, editors, developer options, state reloads, console and more. + Allow Mod Configs Warning Popup + 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 0549951f..de4512d7 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/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/options/Options.hx b/source/funkin/options/Options.hx index 9551c51c..39d03594 100644 --- a/source/funkin/options/Options.hx +++ b/source/funkin/options/Options.hx @@ -45,6 +45,7 @@ class Options 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 diff --git a/source/funkin/options/TreeMenu.hx b/source/funkin/options/TreeMenu.hx index befed063..9a8b933a 100644 --- a/source/funkin/options/TreeMenu.hx +++ b/source/funkin/options/TreeMenu.hx @@ -89,6 +89,9 @@ class TreeMenu extends UIState { if (cameraScroll && last != null) { if (menuChangeTween != null && menuChangeTween.active) menuChangeTween.cancel(); FlxG.camera.scroll.x = last.x; + + for (menu in tree) if (menu != null) menu.transitioning = false; + for (menu in previousMenus) if (menu != null) menu.transitioning = false; } } @@ -203,8 +206,9 @@ class TreeMenu extends UIState { }}); for (menu in tree) if (menu != null) menu.transitioning = true; - for (menu in previousMenus) if (menu != null) menu.transitioning = true; } + + for (menu in previousMenus) if (menu != null) menu.transitioning = true; } override function update(elapsed:Float) { @@ -250,6 +254,7 @@ class TreeMenu extends UIState { public function reloadStrings() { for (menu in tree) if (menu != null) menu.reloadStrings(); + updateLabels(); } public function destroyPreviousMenus() { diff --git a/source/funkin/options/TreeMenuScreen.hx b/source/funkin/options/TreeMenuScreen.hx index b03ab959..5c8a1d42 100644 --- a/source/funkin/options/TreeMenuScreen.hx +++ b/source/funkin/options/TreeMenuScreen.hx @@ -48,8 +48,8 @@ class TreeMenuScreen extends FlxSpriteGroup { 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 / 30); - var rightTurboControl:TurboControls = new TurboControls([Control.RIGHT], 0.2, 1 / 30); + 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; @@ -126,6 +126,8 @@ class TreeMenuScreen extends FlxSpriteGroup { public function close() { onClose.dispatch(); + if (curOption != null) curOption.selected = false; + if (parent == null) return destroy(); else parent.removeMenu(this); diff --git a/source/funkin/options/categories/AppearanceOptions.hx b/source/funkin/options/categories/AppearanceOptions.hx index 125509a5..57e48e03 100644 --- a/source/funkin/options/categories/AppearanceOptions.hx +++ b/source/funkin/options/categories/AppearanceOptions.hx @@ -2,7 +2,7 @@ package funkin.options.categories; class AppearanceOptions extends TreeMenuScreen { public function new() { - super('optionsTree.appearance-name', 'optionsTree.appearance-desc', "AppearanceOptions."); + super('optionsTree.appearance-name', 'optionsTree.appearance-desc', 'AppearanceOptions.'); add(new NumOption(getNameID('framerate'), getDescID('framerate'), 30, 240, 1, @@ -35,7 +35,7 @@ class AppearanceOptions extends TreeMenuScreen { class AdvancedAppearanceOptions extends TreeMenuScreen { public function new() { - super('optionsMenu.advanced', 'optionsTree.appearance.advanced-desc', "AppearanceOptions.Advanced."); + super('optionsMenu.advanced', 'optionsTree.appearance.advanced-desc', 'AppearanceOptions.Advanced.'); for (option in [ new Checkbox(getNameID('antialiasing'), getDescID('antialiasing'), 'antialiasing', __changeAntialiasing), diff --git a/source/funkin/options/categories/GameplayOptions.hx b/source/funkin/options/categories/GameplayOptions.hx index 0abcbae1..3faa11db 100644 --- a/source/funkin/options/categories/GameplayOptions.hx +++ b/source/funkin/options/categories/GameplayOptions.hx @@ -4,7 +4,66 @@ import flixel.util.FlxTimer; import funkin.backend.system.Conductor; class GameplayOptions extends TreeMenuScreen { + var __metronome = FlxG.sound.load(Paths.sound('editors/charter/metronome')); + var offsetSetting:NumOption; + + 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')); + add(offsetSetting = new NumOption(getNameID('songOffset'), getDescID('songOffset'), -999, 999, 1, 'songOffset', __changeOffset)); + + add(new Separator()); + add(new TextOption('optionsMenu.advanced', 'optionsTree.gameplay.advanced-desc', ' >', () -> + parent.addMenu(new AdvancedGameplayOptions()))); + } + + private function __changeOffset(offset:Float) Conductor.songOffset = offset; + + var __lastBeat:Int = 0; + var __lastSongBeat:Int = 0; + + override function update(elapsed:Float) { + super.update(elapsed); + FlxG.camera.zoom = CoolUtil.fpsLerp(FlxG.camera.zoom, 1, 0.1); + + if (offsetSetting.selected) { + if (__lastBeat != Conductor.curBeat) { + FlxG.camera.zoom += 0.03; + __lastBeat = Conductor.curBeat; + } + + var beat = Math.floor(Conductor.getTimeInBeats(FlxG.sound.music.time)); + if (__lastSongBeat != beat) { + __metronome.replay(); + __lastSongBeat = beat; + } + } + } + + 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; + } } -class AdvancedGameplayOptions extends OptionsScreen { -} +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 d166643e..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 TreeMenuScreen { - public override function new(title:String, desc:String) { - super(title, desc, "LanguageOptions."); + 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 ffa8eadd..e72bafc8 100644 --- a/source/funkin/options/categories/MiscOptions.hx +++ b/source/funkin/options/categories/MiscOptions.hx @@ -1,4 +1,26 @@ package funkin.options.categories; 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(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 Separator()); + add(new TextOption(getNameID('resetSaveData'), getDescID('resetSaveData'), () -> { + // TODO: SAVE DATA RESETTING + })); + } +} \ No newline at end of file diff --git a/source/funkin/options/type/Checkbox.hx b/source/funkin/options/type/Checkbox.hx index 5241686c..592b7723 100644 --- a/source/funkin/options/type/Checkbox.hx +++ b/source/funkin/options/type/Checkbox.hx @@ -19,8 +19,8 @@ class Checkbox extends TextOption { "checking" => FlxPoint.get(35, 29) ]; - public function new(text:String, desc:String, optionName:String, ?selectCallback:Void->Void = null, ?parent:Dynamic) { - super(text, desc); + public function new(text:String, desc:String, optionName:String, ?selectCallback:Void->Void, ?parent:Dynamic) { + super(text, desc, selectCallback); this.optionName = optionName; this.parent = parent = parent != null ? parent : Options; @@ -36,14 +36,13 @@ class Checkbox extends TextOption { __text.x = 100; - var fieldValue = Reflect.field(parent, optionName); if (optionName != null) checked = Reflect.field(parent, optionName); else checked = false; } public var firstFrame:Bool = true; - public override function update(elapsed:Float) { + override function update(elapsed:Float) { if (checkbox.animation.curAnim == null) checkbox.animation.play(checked ? "checked" : "unchecked", true); super.update(elapsed); @@ -64,15 +63,15 @@ class Checkbox extends TextOption { } 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 select() { + override function select() { if (locked) return; + checked = !checked; if (optionName != null) Reflect.setField(parent, optionName, checked); @@ -82,8 +81,8 @@ class Checkbox extends TextOption { 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/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/NewOption.hx b/source/funkin/options/type/NewOption.hx index 28255b8c..07f4fdf0 100644 --- a/source/funkin/options/type/NewOption.hx +++ b/source/funkin/options/type/NewOption.hx @@ -12,6 +12,7 @@ class NewOption extends TextOption { super(name, desc, callback); __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/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 ac48eafa..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,75 +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 select() { + 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 From b15e3bb5aab5126943478801da72f03dcb4e3996 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 27 Jul 2025 08:12:11 +0700 Subject: [PATCH 07/25] Options Rewrite WIP All Editor Menus Done --- assets/images/editors/bgs/default.png | Bin 0 -> 52219 bytes assets/languages/en/Editors.xml | 13 ++ source/funkin/editors/DebugOptions.hx | 45 ++++- source/funkin/editors/EditorTreeMenu.hx | 61 +++++-- .../editors/alphabet/AlphabetSelection.hx | 60 +++---- .../editors/character/CharacterSelection.hx | 83 ++++----- .../editors/charter/CharterSelection.hx | 169 ++++++------------ source/funkin/editors/stage/StageSelection.hx | 69 ++++--- source/funkin/menus/MainMenuState.hx | 4 +- .../funkin/menus/credits/CreditsCodename.hx | 135 ++++++++++++++ source/funkin/menus/credits/CreditsMain.hx | 103 +++++++++++ source/funkin/options/OptionsMenu.hx | 12 +- source/funkin/options/OptionsScreen.hx | 141 --------------- source/funkin/options/OptionsTree.hx | 91 ---------- source/funkin/options/TreeMenu.hx | 6 +- .../funkin/options/type/EditorIconOption.hx | 13 -- .../funkin/options/type/GithubIconOption.hx | 1 + source/funkin/options/type/NewOption.hx | 1 + source/funkin/options/type/OptionType.hx | 3 + source/funkin/options/type/TextOption.hx | 8 +- 20 files changed, 505 insertions(+), 513 deletions(-) create mode 100644 assets/images/editors/bgs/default.png create mode 100644 source/funkin/menus/credits/CreditsCodename.hx create mode 100644 source/funkin/menus/credits/CreditsMain.hx delete mode 100644 source/funkin/options/OptionsScreen.hx delete mode 100644 source/funkin/options/OptionsTree.hx delete mode 100644 source/funkin/options/type/EditorIconOption.hx diff --git a/assets/images/editors/bgs/default.png b/assets/images/editors/bgs/default.png new file mode 100644 index 0000000000000000000000000000000000000000..ba4754edfd4941acb0401f4f954918439bcc8c59 GIT binary patch literal 52219 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 UJKEvkw*UYD07*qoM6N<$f~VWW)&Kwi literal 0 HcmV?d00001 diff --git a/assets/languages/en/Editors.xml b/assets/languages/en/Editors.xml index 86ca86ac..e62add53 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. @@ -435,6 +437,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! + @@ -442,6 +449,7 @@ Press ACCEPT to edit this typeface. New Typeface + Press ACCEPT to create a new typeface. Select a typeface to edit @@ -554,6 +562,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/source/funkin/editors/DebugOptions.hx b/source/funkin/editors/DebugOptions.hx index 0f3dc9fd..e0293319 100644 --- a/source/funkin/editors/DebugOptions.hx +++ b/source/funkin/editors/DebugOptions.hx @@ -1,18 +1,57 @@ package funkin.editors; import funkin.backend.utils.NativeAPI; -import funkin.options.OptionsScreen; import funkin.options.TreeMenu; +import funkin.options.TreeMenuScreen; import funkin.options.type.*; class DebugOptions extends TreeMenu { - public override function create() { + var bg:FlxSprite; + + override function create() { super.create(); + UIState.setResolutionAware(); + + add(bg = new FlxSprite().loadAnimatedGraphic(Paths.image('menus/menuBGBlue'))); + bg.antialiasing = true; + bg.scrollFactor.set(); + updateBG(); + + addMenu(new TreeMenuScreen('DebugOptions.title', 'DebugOptions.desc')); FlxG.camera.fade(0xFF000000, 0.5, true); } + + 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(); + } } -class DebugOptionsScreen extends OptionsScreen { +class DebugOptionsScreen extends TreeMenuScreen { + public function new() { + super('DebugOptions.title', 'DebugOptions.desc', 'DebugOptions.'); + #if windows + add(new TextOption(getNameID("showConsole"), getDescID("showConsole"), () -> 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("editorPrettyPrint"), getDescID("editorPrettyPrint"), "editorPrettyPrint")); + 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/editors/EditorTreeMenu.hx b/source/funkin/editors/EditorTreeMenu.hx index b4904866..afe6dd18 100644 --- a/source/funkin/editors/EditorTreeMenu.hx +++ b/source/funkin/editors/EditorTreeMenu.hx @@ -1,28 +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 var main:OptionsScreen; - - public override function create() { + override function create() { super.create(); - UIState.setResolutionAware(); - FlxG.camera.fade(0xFF000000, 0.5, true); + } - bg = new FlxBackdrop(); + override function createPost() { + super.createPost(); + add(bg = new FlxBackdrop()); bg.loadGraphic(Paths.image('editors/bgs/${bgType}')); bg.antialiasing = true; setBackgroundRotation(-5); - add(bg); } public inline function setBackgroundRotation(rotation:Float) { @@ -30,11 +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()); } - public function onMenuChange() { - + 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..9d937a83 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,14 +57,14 @@ 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); 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..fda0f1c3 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.color = 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/MainMenuState.hx b/source/funkin/menus/MainMenuState.hx index 1a931781..1a68f4a2 100644 --- a/source/funkin/menus/MainMenuState.hx +++ b/source/funkin/menus/MainMenuState.hx @@ -6,7 +6,7 @@ import flixel.tweens.FlxTween; import funkin.backend.FunkinText; import funkin.backend.scripting.events.menu.MenuChangeEvent; import funkin.backend.scripting.events.NameEvent; -//import funkin.menus.credits.CreditsMain; +import funkin.menus.credits.CreditsMain; import funkin.options.OptionsMenu; import lime.app.Application; @@ -168,7 +168,7 @@ class MainMenuState extends MusicBeatState { case 'story mode': FlxG.switchState(new StoryMenuState()); case 'freeplay': FlxG.switchState(new FreeplayState()); - //case 'donate', 'credits': FlxG.switchState(new CreditsMain()); // kept donate for not breaking scripts, if you don't want donate to bring you to the credits menu, thats easy softcodable - Nex + case 'donate', 'credits': FlxG.switchState(new CreditsMain()); // kept donate for not breaking scripts, if you don't want donate to bring you to the credits menu, thats easy softcodable - Nex case 'options': FlxG.switchState(new OptionsMenu()); } }); diff --git a/source/funkin/menus/credits/CreditsCodename.hx b/source/funkin/menus/credits/CreditsCodename.hx new file mode 100644 index 00000000..5448825c --- /dev/null +++ b/source/funkin/menus/credits/CreditsCodename.hx @@ -0,0 +1,135 @@ +package funkin.menus.credits; + +import flixel.text.FlxText; +import flixel.util.FlxColor; +import funkin.backend.system.github.GitHub; +import funkin.backend.system.github.GitHubContributor.CreditsGitHubContributor; +import funkin.options.PlayerSettings; +import funkin.options.type.GithubIconOption; + +using StringTools; + +class CreditsCodename extends funkin.options.TreeMenuScreen { + public var error:Bool = false; + public var totalContributions:Int = 0; + public var contribFormats:Array = []; + + 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; + override function update(elapsed:Float) { + super.update(elapsed); + + if (_downloadingSteps == 2) { + _downloadingSteps = 0; + _canReset = true; + displayList(); + } else if (_downloadingSteps == 1) { + _downloadingSteps = 0; + _canReset = true; + updateMenuDesc(); + } + else if (_canReset && PlayerSettings.solo.controls.RESET) tryUpdating(); + } + + public function tryUpdating(forceDisplaying:Bool = false) { + updateMenuDesc(TU.translate("credits.downloadingList")); + _canReset = false; + Main.execAsync(() -> { + if (checkUpdate() || forceDisplaying) _downloadingSteps = 2; + else _downloadingSteps = 1; + }); + } + + public override function updateMenuDesc(?txt:String) { + if (!_canReset) return; + super.updateMenuDesc(txt); + updateMarkup(); + } + + public function updateMarkup() { + 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 destroy() { + super.destroy(); + for (frmt in contribFormats) parent.descLabel.removeFormat(frmt.format); + } + + public function checkUpdate():Bool { + var curTime:Float = Date.now().getTime(); + if(Options.lastUpdated != null && curTime < Options.lastUpdated + 120000) return false; // Fuck you Github rate limits - Nex + Options.lastUpdated = curTime; + + error = false; + var idk = GitHub.getContributors(Flags.REPO_OWNER, Flags.REPO_NAME, function(e) { + error = true; + var errMsg:String = 'Error while trying to download contributors list:\n${CoolUtil.removeIP(e.message)}'; + Logs.error(errMsg.replace('\n', ' ')); + funkin.backend.utils.NativeAPI.showMessageBox("Codename Engine Warning", errMsg, MSG_WARNING); + }); + if(error) return false; + if((idk is Array)) { + var contributors:Array = []; + for(e in idk) contributors.push({ + login: e.login, + avatar_url: e.avatar_url, + html_url: e.html_url, + id: e.id, + contributions: e.contributions + }); + Options.contributors = contributors; + } + Logs.verbose('[CreditsCodename] Contributors list Updated!'); + + var errorOnMain:Bool = false; + var idk2 = GitHub.getOrganizationMembers(Flags.REPO_OWNER, function(e) { + errorOnMain = true; + var errMsg:String = 'Error while trying to download ${Flags.REPO_OWNER} members list:\n${CoolUtil.removeIP(e.message)}'; + Logs.error(errMsg.replace('\n', ' ')); + funkin.backend.utils.NativeAPI.showMessageBox("Codename Engine Warning", errMsg, MSG_WARNING); + }); + if(!errorOnMain) { + Options.mainDevs = [for(m in idk2) m.id]; + Logs.verbose('[CreditsCodename] Main Devs list Updated!'); + } + + return true; + } + + public function displayList() { + //if (curSelected > Options.contributors.length - 1) changeSelection(-(curSelected - (Options.contributors.length - 1))); + if (curSelected > Options.contributors.length - 1) curSelected = Options.contributors.length - 1; + changeSelection(0, true); + + while (members.length > 0) { + members[0].destroy(); + remove(members[0], true); + } + + totalContributions = 0; + for(c in Options.contributors) totalContributions += c.contributions; + for(c in Options.contributors) { + var text = TU.translate("credits.totalContributions", [c.contributions, totalContributions, FlxMath.roundDecimal(c.contributions / totalContributions * 100, 2)]); + var opt:GithubIconOption = new GithubIconOption(c, text); + if(Options.mainDevs.contains(c.id)) { + opt.desc += TU.translate("credits.mainDev"); + @:privateAccess opt.__text.color = Flags.MAIN_DEVS_COLOR; + } + add(opt); + } + + updateMenuDesc(); + } +} \ No newline at end of file diff --git a/source/funkin/menus/credits/CreditsMain.hx b/source/funkin/menus/credits/CreditsMain.hx new file mode 100644 index 00000000..79182bcc --- /dev/null +++ b/source/funkin/menus/credits/CreditsMain.hx @@ -0,0 +1,103 @@ +package funkin.menus.credits; + +import flixel.util.FlxColor; +import funkin.backend.assets.AssetSource; +import funkin.backend.system.github.GitHubContributor.CreditsGitHubContributor; +import funkin.options.TreeMenu; +import funkin.options.TreeMenuScreen; +import funkin.options.type.*; +import haxe.xml.Access; + +class CreditsMain extends TreeMenu { + var bg:FlxSprite; + var items:Array = []; + + override function create() { + super.create(); + + DiscordUtil.call("onMenuLoaded", ["Credits Menu"]); + + add(bg = new FlxSprite().loadAnimatedGraphic(Paths.image('menus/menuBGBlue'))); + bg.antialiasing = true; + 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'); + + 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('[CreditsMain] Error while parsing credits.xml: ${Std.string(e)}', ERROR); + if (access != null) for (c in parseCreditsFromXML(access)) first.add(c); + } + } + + 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))); + } + + 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"); + + if (node.name == "github") { + if (!node.has.user) { + Logs.warn("A github node requires a user attribute.", "CreditsMain"); + continue; + } + + var username = node.getAtt("user"); + var user:CreditsGitHubContributor = { // Kind of forcing + login: username, + html_url: 'https://github.com/$username', + avatar_url: 'https://github.com/$username.png' + }; + var opt:GithubIconOption = new GithubIconOption(user, desc, null, + node.has.customName ? node.att.customName : null, + node.has.size ? Std.parseInt(node.att.size) : 96, + node.has.portrait ? node.att.portrait.toLowerCase() == "false" ? false : true : true + ); + if (node.has.color) + @:privateAccess opt.__text.color = FlxColor.fromString(node.att.color); + credsMenus.push(opt); + } else { + if (!node.has.name) { + Logs.warn("A credit node requires a name attribute.", "CreditsMain"); + continue; + } + var name = node.getAtt("name"); + + switch(node.name) { + case "credit": + var opt:PortraitOption = new PortraitOption(name, desc, function() if(node.has.url) CoolUtil.openURL(node.att.url), + node.has.icon && Paths.assetsTree.existsSpecific(Paths.image('credits/${node.att.icon}'), "IMAGE", source) ? + FlxG.bitmap.add(Paths.image('credits/${node.att.icon}')) : null, node.has.size ? Std.parseInt(node.att.size) : 96, + node.has.portrait ? node.att.portrait.toLowerCase() == "false" ? false : true : true + ); + if (node.has.color) + @:privateAccess opt.__text.color = FlxColor.fromString(node.att.color); + credsMenus.push(opt); + + case "menu": + 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/options/OptionsMenu.hx b/source/funkin/options/OptionsMenu.hx index ae184e0c..7522b87c 100644 --- a/source/funkin/options/OptionsMenu.hx +++ b/source/funkin/options/OptionsMenu.hx @@ -94,7 +94,7 @@ 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); } } } @@ -120,9 +120,8 @@ class OptionsMenu extends TreeMenu { } // XML STUFF - /* - public function parseOptionsFromXML(xml:Access):Array { - var options:Array = []; + public function parseOptionsFromXML(xml:Access):Array { + var options:Array = []; for(node in xml.elements) { if (!node.has.name) { @@ -164,13 +163,10 @@ 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 TreeMenuScreen(name, desc, parseOptionsFromXML(node))); - })); + options.push(new TextOption(name, desc, ' >', () -> addMenu(new TreeMenuScreen(name, desc, parseOptionsFromXML(node))))); } } return options; } - */ } \ No newline at end of file diff --git a/source/funkin/options/OptionsScreen.hx b/source/funkin/options/OptionsScreen.hx deleted file mode 100644 index 672feb40..00000000 --- a/source/funkin/options/OptionsScreen.hx +++ /dev/null @@ -1,141 +0,0 @@ -// REMOVE THIS WHEN COMPLETE - -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].select(); - onSelect(members[curSelected]); - } - if (controls.LEFT_P) - members[curSelected].changeSelection(-1); - if (controls.RIGHT_P) - members[curSelected].changeSelection(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); - 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 1f0bfcc7..00000000 --- a/source/funkin/options/OptionsTree.hx +++ /dev/null @@ -1,91 +0,0 @@ -// REMOVE THIS WHEN COMPLETE - -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 9a8b933a..f5c0e177 100644 --- a/source/funkin/options/TreeMenu.hx +++ b/source/funkin/options/TreeMenu.hx @@ -32,8 +32,6 @@ class TreeMenu extends UIState { public var descLabel:FunkinText; public var bgLabel:FlxSprite; - public var optionsTree:OptionsTree; - var menuChangeTween:FlxTween; var __drawer:TreeMenuDrawer; var __treeCreated:Bool = false; @@ -74,7 +72,9 @@ class TreeMenu extends UIState { add(bgLabel); add(titleLabel); add(descLabel); - updateLabels(); + + FlxG.camera.scroll.x = -FlxG.width; + menuChanged(); __treeCreated = true; } 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/NewOption.hx b/source/funkin/options/type/NewOption.hx index 07f4fdf0..85181412 100644 --- a/source/funkin/options/type/NewOption.hx +++ b/source/funkin/options/type/NewOption.hx @@ -10,6 +10,7 @@ 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; diff --git a/source/funkin/options/type/OptionType.hx b/source/funkin/options/type/OptionType.hx index 41f4302c..3d4d25f0 100644 --- a/source/funkin/options/type/OptionType.hx +++ b/source/funkin/options/type/OptionType.hx @@ -1,6 +1,7 @@ package funkin.options.type; import flixel.group.FlxSpriteGroup; +import flixel.util.FlxColor; import funkin.backend.system.Controls; import funkin.options.TreeMenu.ITreeOption; @@ -19,6 +20,8 @@ class OptionType extends FlxSpriteGroup implements ITreeOption { public var itemHeight:Float = 120; + public var editorFlashColor:FlxColor = FlxColor.WHITE; + public function new(text:String, desc:String) { super(); rawText = text; diff --git a/source/funkin/options/type/TextOption.hx b/source/funkin/options/type/TextOption.hx index 74ccfc2b..e859b668 100644 --- a/source/funkin/options/type/TextOption.hx +++ b/source/funkin/options/type/TextOption.hx @@ -6,7 +6,7 @@ import flixel.effects.FlxFlicker; * Option type that has text. **/ class TextOption extends OptionType { - public var suffix:String; + public var suffix(default, set):String; public var selectCallback:Void->Void; var __text:Alphabet; @@ -15,6 +15,12 @@ class TextOption extends OptionType { return text = v; } + function set_suffix(v:String) { + if (suffix == (suffix = v)) return v; + __text.text = text + suffix; + return v; + } + public function new(text:String, desc:String, ?suffix:String = "", ?selectCallback:Void->Void = null) { this.suffix = suffix; this.selectCallback = selectCallback; From 7cd6059c87a25a5e696bc191d711a15acba82555 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 27 Jul 2025 08:18:28 +0700 Subject: [PATCH 08/25] Whoops --- source/flixel/sound/FlxSound.hx | 1 + source/funkin/options/TreeMenuScreen.hx | 2 +- source/funkin/options/type/TextOption.hx | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/source/flixel/sound/FlxSound.hx b/source/flixel/sound/FlxSound.hx index 6e83609e..e40bc407 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/options/TreeMenuScreen.hx b/source/funkin/options/TreeMenuScreen.hx index 5c8a1d42..17b012e1 100644 --- a/source/funkin/options/TreeMenuScreen.hx +++ b/source/funkin/options/TreeMenuScreen.hx @@ -131,7 +131,7 @@ class TreeMenuScreen extends FlxSpriteGroup { if (parent == null) return destroy(); else parent.removeMenu(this); - CoolUtil.playMenuSFX(CANCEL); + CoolUtil.playMenuSFX(CANCEL).persist = true; } public function changeSelection(change:Int, force:Bool = false) { diff --git a/source/funkin/options/type/TextOption.hx b/source/funkin/options/type/TextOption.hx index e859b668..64b93b25 100644 --- a/source/funkin/options/type/TextOption.hx +++ b/source/funkin/options/type/TextOption.hx @@ -22,7 +22,7 @@ class TextOption extends OptionType { } public function new(text:String, desc:String, ?suffix:String = "", ?selectCallback:Void->Void = null) { - this.suffix = suffix; + @:bypassAccessor this.suffix = suffix; this.selectCallback = selectCallback; __text = new Alphabet(20, 20, "", "bold"); From 44e1ac3957d45d280441fb721514335e86700e42 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 27 Jul 2025 08:20:46 +0700 Subject: [PATCH 09/25] Oops again --- source/funkin/editors/EditorTreeMenu.hx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/funkin/editors/EditorTreeMenu.hx b/source/funkin/editors/EditorTreeMenu.hx index afe6dd18..1a7a0e37 100644 --- a/source/funkin/editors/EditorTreeMenu.hx +++ b/source/funkin/editors/EditorTreeMenu.hx @@ -15,11 +15,11 @@ class EditorTreeMenu extends funkin.options.TreeMenu { } override function createPost() { - super.createPost(); - add(bg = new FlxBackdrop()); + insert(0, bg = new FlxBackdrop()); bg.loadGraphic(Paths.image('editors/bgs/${bgType}')); bg.antialiasing = true; setBackgroundRotation(-5); + super.createPost(); } public inline function setBackgroundRotation(rotation:Float) { From 51dc821bbba0387cd6b5b3c5b53b5fda667bd0b6 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 27 Jul 2025 08:22:57 +0700 Subject: [PATCH 10/25] Fix Alphabet Strings, Charter flash color --- source/funkin/editors/alphabet/AlphabetSelection.hx | 2 +- source/funkin/editors/charter/CharterSelection.hx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/funkin/editors/alphabet/AlphabetSelection.hx b/source/funkin/editors/alphabet/AlphabetSelection.hx index 9d937a83..619b8a31 100644 --- a/source/funkin/editors/alphabet/AlphabetSelection.hx +++ b/source/funkin/editors/alphabet/AlphabetSelection.hx @@ -18,7 +18,7 @@ class AlphabetSelection extends EditorTreeMenu { class AlphabetSelectionScreen extends EditorTreeMenuScreen { public function new() { - super('editor.alphabet.name', 'editor.alphabet.selection.desc', 'editor.alphabet.selection', 'newTypeface', 'newTypefaceDesc', () -> { + 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) -> {}} ])); diff --git a/source/funkin/editors/charter/CharterSelection.hx b/source/funkin/editors/charter/CharterSelection.hx index fda0f1c3..85776f44 100644 --- a/source/funkin/editors/charter/CharterSelection.hx +++ b/source/funkin/editors/charter/CharterSelection.hx @@ -49,7 +49,7 @@ class CharterSelectionScreen extends EditorTreeMenuScreen { parent.addMenu(screen); }); opt.suffix = ' >'; - opt.color = s.color.getDefault(FlxColor.WHITE); + opt.editorFlashColor = s.color.getDefault(FlxColor.WHITE); return opt; } From c4cd1e8fb46d9f52aedaaea51e6a6778198e4a2a Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 27 Jul 2025 08:28:39 +0700 Subject: [PATCH 11/25] Fix Custom Checkbox Saves --- source/funkin/options/OptionsMenu.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/options/OptionsMenu.hx b/source/funkin/options/OptionsMenu.hx index 7522b87c..93cd0d82 100644 --- a/source/funkin/options/OptionsMenu.hx +++ b/source/funkin/options/OptionsMenu.hx @@ -137,7 +137,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) { From 3b06f1e130e76f76c0254e0099e3bd12e284f1f7 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 27 Jul 2025 08:32:35 +0700 Subject: [PATCH 12/25] Fix CreditsCodename Crashing on destroy --- source/funkin/menus/credits/CreditsCodename.hx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/funkin/menus/credits/CreditsCodename.hx b/source/funkin/menus/credits/CreditsCodename.hx index 5448825c..7051e2f6 100644 --- a/source/funkin/menus/credits/CreditsCodename.hx +++ b/source/funkin/menus/credits/CreditsCodename.hx @@ -62,9 +62,9 @@ class CreditsCodename extends funkin.options.TreeMenuScreen { ]); } - override function destroy() { - super.destroy(); + override function close() { for (frmt in contribFormats) parent.descLabel.removeFormat(frmt.format); + super.close(); } public function checkUpdate():Bool { From d8845d50ea76a739854b4a43de90dc11dff8980f Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 27 Jul 2025 08:37:09 +0700 Subject: [PATCH 13/25] Fix DebugOptions... --- source/funkin/editors/DebugOptions.hx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/source/funkin/editors/DebugOptions.hx b/source/funkin/editors/DebugOptions.hx index e0293319..67799bbc 100644 --- a/source/funkin/editors/DebugOptions.hx +++ b/source/funkin/editors/DebugOptions.hx @@ -10,16 +10,15 @@ class DebugOptions extends TreeMenu { override function create() { super.create(); - UIState.setResolutionAware(); add(bg = new FlxSprite().loadAnimatedGraphic(Paths.image('menus/menuBGBlue'))); bg.antialiasing = true; bg.scrollFactor.set(); - updateBG(); - addMenu(new TreeMenuScreen('DebugOptions.title', 'DebugOptions.desc')); + addMenu(new DebugOptionsScreen()); FlxG.camera.fade(0xFF000000, 0.5, true); + UIState.setResolutionAware(); } public function updateBG() { From eb9017732109d7406dc7ebf1dd731080bc830484 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 27 Jul 2025 08:38:59 +0700 Subject: [PATCH 14/25] Forgot to dowscale this texture --- assets/images/editors/bgs/default.png | Bin 52219 -> 19565 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/images/editors/bgs/default.png b/assets/images/editors/bgs/default.png index ba4754edfd4941acb0401f4f954918439bcc8c59..1b99f9a5ee9e57061a2ac6212fc0ce92718f57b6 100644 GIT binary patch literal 19565 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 CD6g;p literal 52219 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 UJKEvkw*UYD07*qoM6N<$f~VWW)&Kwi From d3ba0251f013356a8153d3a6cd377fd4565bca42 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 27 Jul 2025 10:43:42 +0700 Subject: [PATCH 15/25] Move Quality Option to Advanced instead --- .../options/categories/AppearanceOptions.hx | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/source/funkin/options/categories/AppearanceOptions.hx b/source/funkin/options/categories/AppearanceOptions.hx index 57e48e03..0f55e496 100644 --- a/source/funkin/options/categories/AppearanceOptions.hx +++ b/source/funkin/options/categories/AppearanceOptions.hx @@ -9,10 +9,6 @@ class AppearanceOptions extends TreeMenuScreen { 'framerate', __changeFPS )); add(new Checkbox(getNameID('flashingMenu'), getDescID('flashingMenu'), 'flashingMenu')); - add(new ArrayOption(getNameID('quality'), getDescID('quality'), - [0, 1, 2], [getID('quality-low'), getID('quality-high'), getID('quality-custom')], - 'quality', __changeQuality, null - )); add(new Checkbox(getNameID('colorHealthBar'), getDescID('colorHealthBar'), 'colorHealthBar')); add(new Checkbox(getNameID('week6PixelPerfect'), getDescID('week6PixelPerfect'), 'week6PixelPerfect')); @@ -26,27 +22,39 @@ class AppearanceOptions extends TreeMenuScreen { if (FlxG.updateFramerate < framerate) FlxG.drawFramerate = FlxG.updateFramerate = framerate; else FlxG.updateFramerate = FlxG.drawFramerate = framerate; } - - private function __changeQuality(value:Dynamic) { - var antialiasing = value == 0 ? false : (value == 1 ? true : Options.antialiasing); - FlxG.game.stage.quality = (FlxG.enableAntialiasing = antialiasing) ? BEST : LOW; - } } class AdvancedAppearanceOptions extends TreeMenuScreen { + var qualityOptions:Array = []; + public function new() { super('optionsMenu.advanced', 'optionsTree.appearance.advanced-desc', 'AppearanceOptions.Advanced.'); - for (option in [ + 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); - if (option.locked = Options.quality != 2) option.rawDesc = 'AppearanceOptions.quality-optionDisabled-desc'; - } 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() { From ca9ec15496d9980ce34deedcf87bef5d90b127fb Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 27 Jul 2025 10:44:15 +0700 Subject: [PATCH 16/25] OOps --- assets/languages/en/Options.xml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/assets/languages/en/Options.xml b/assets/languages/en/Options.xml index 341559cb..3eb3ca62 100644 --- a/assets/languages/en/Options.xml +++ b/assets/languages/en/Options.xml @@ -66,13 +66,6 @@ Flashing Menu If unchecked, will disable menu flashing when you select an option in the Main Menu, and other flashes will be slower. - 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 - Following option is locked for Quality Option, to unlock set Quality to CUSTOM - Colored Healthbar If unchecked, the game will use the original red/green health bar from the week 6 fnf game. @@ -80,6 +73,12 @@ If checked, Week 6 will have a pixel perfect effect to it enabled, aligning every pixel on the screen. + 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 + Antialiasing If unchecked, will disable antialiasing on every sprite. Can boost performances at the cost of sharper, more pixely sprites. From 180b47cd07802eb74dc17fef72529a59979d9d86 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 27 Jul 2025 10:44:20 +0700 Subject: [PATCH 17/25] eugh --- source/funkin/editors/alphabet/AlphabetSelection.hx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/funkin/editors/alphabet/AlphabetSelection.hx b/source/funkin/editors/alphabet/AlphabetSelection.hx index 619b8a31..bb274c8f 100644 --- a/source/funkin/editors/alphabet/AlphabetSelection.hx +++ b/source/funkin/editors/alphabet/AlphabetSelection.hx @@ -69,5 +69,7 @@ class AlphabetIconOption extends TextOption { } iconSpr.setPosition(90 - iconSpr.width - 20, (__text.height - iconSpr.height) / 2); add(iconSpr); + + __text.x = 100; } } \ No newline at end of file From f9d94bbebdf3e7304e283e285b428f45cdd9e8bb Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 27 Jul 2025 13:51:27 +0700 Subject: [PATCH 18/25] WIP Slider --- source/funkin/backend/utils/CoolUtil.hx | 2 +- source/funkin/menus/ui/Slider.hx | 113 +++++++++++++++++- source/funkin/options/TreeMenu.hx | 5 +- source/funkin/options/TreeMenuScreen.hx | 30 ++++- .../options/categories/GameplayOptions.hx | 4 +- source/funkin/options/type/NumOption.hx | 14 +-- source/funkin/options/type/SliderOption.hx | 54 +++++++-- 7 files changed, 195 insertions(+), 27 deletions(-) diff --git a/source/funkin/backend/utils/CoolUtil.hx b/source/funkin/backend/utils/CoolUtil.hx index 47510764..175e0eb0 100644 --- a/source/funkin/backend/utils/CoolUtil.hx +++ b/source/funkin/backend/utils/CoolUtil.hx @@ -528,7 +528,7 @@ final class CoolUtil case UNCHECKED: 'checkboxUnchecked'; case WARNING: 'warningMenu'; default: 'scroll'; - }), volume); + }), volume * Options.volumeSFX); } /** diff --git a/source/funkin/menus/ui/Slider.hx b/source/funkin/menus/ui/Slider.hx index 45360ce7..c60a740c 100644 --- a/source/funkin/menus/ui/Slider.hx +++ b/source/funkin/menus/ui/Slider.hx @@ -1,5 +1,116 @@ package funkin.menus.ui; +import flixel.animation.FlxAnimation; +import flixel.graphics.frames.FlxFramesCollection; +import flixel.graphics.frames.FlxFrame; +import flixel.math.FlxPoint; + class Slider extends FlxSprite { - + public var playSound:Bool = true; + public var selected:Bool = false; + public var value:Float; + public var segments: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 __curSegments:Int; + + public function new(?x:Float, ?y:Float, value:Float = 0.5, barWidth:Float = 500, segments:Int = 4) { + super(x, y); + this.value = value; + this.barWidth = barWidth; + this.segments = segments; + + frames = Paths.getFrames('menus/options/slider'); + } + + 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); + __animTime += elapsed; + + 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': 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); + } + } + } + + override function isSimpleRender(?camera:FlxCamera):Bool return false; + override function drawComplex(camera:FlxCamera) { + if (showSlider) { + var xOff = frame.sourceSize.x * 0.5 - barWidth * value, yOff = (frame.sourceSize.y - barHeight) * 0.5; + frameOffset.add(xOff, yOff); + super.drawComplex(camera); + frameOffset.subtract(xOff, yOff); + } + } + + 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); + + var anim; + barHeight = 0; + for (type in 0...3) { + if ((anim = getBarAnim(type, false)) != null) barHeight = Math.max(barHeight, frames.frames[anim.frames[0]].sourceSize.y); + if ((anim = getBarAnim(type, true)) != null) barHeight = Math.max(barHeight, frames.frames[anim.frames[0]].sourceSize.y); + } + + sliderResetFrameSize(); + resetSizeFromFrame(); + centerOrigin(); + + if (FlxG.renderBlit) { + dirty = true; + updateFramePixels(); + } + } + + override function set_frame(v:FlxFrame):FlxFrame { + super.set_frame(v); + sliderResetFrameSize(); + return v; + } + + function set_barWidth(v:Float):Float { + origin.x = _halfSize.x = 0.5 * (frameWidth = Std.int(barWidth = v)); + return v; + } } \ No newline at end of file diff --git a/source/funkin/options/TreeMenu.hx b/source/funkin/options/TreeMenu.hx index f5c0e177..33ae47c6 100644 --- a/source/funkin/options/TreeMenu.hx +++ b/source/funkin/options/TreeMenu.hx @@ -12,11 +12,14 @@ interface ITreeOption { var desc:String; var selected:Bool; - function reloadStrings():Void; function changeSelection(change:Int):Void; function select():Void; } +interface ITreeFloatOption extends ITreeOption { + function changeValue(change:Float):Void; +} + class TreeMenu extends UIState { public var onMenuClosed:FlxTypedSignalVoid> = new FlxTypedSignal(); public var onMenuChanged:FlxTypedSignalVoid> = new FlxTypedSignal(); diff --git a/source/funkin/options/TreeMenuScreen.hx b/source/funkin/options/TreeMenuScreen.hx index 17b012e1..3bec7899 100644 --- a/source/funkin/options/TreeMenuScreen.hx +++ b/source/funkin/options/TreeMenuScreen.hx @@ -5,6 +5,8 @@ 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 { @@ -55,6 +57,7 @@ class TreeMenuScreen extends FlxSpriteGroup { var turboBasics:Array; var curOption:ITreeOption; + var curFloatOption:ITreeFloatOption; var __firstFrame:Bool = true; public function new(name:String, desc:String, ?prefix:String, ?objects:Array) { @@ -72,7 +75,7 @@ class TreeMenuScreen extends FlxSpriteGroup { rawName = rawName; rawDesc = rawDesc; - for (object in members) if (object != null && object is ITreeOption) cast(object, ITreeOption).reloadStrings(); + for (object in members) if (object != null && object is OptionType) cast(object, OptionType).reloadStrings(); } override function update(elapsed:Float) { @@ -80,7 +83,10 @@ class TreeMenuScreen extends FlxSpriteGroup { if (__firstFrame) { __firstFrame = false; - if (members[curSelected] is ITreeOption) (curOption = cast members[curSelected]).selected = true; + if (members[curSelected] is ITreeOption) { + (curOption = cast members[curSelected]).selected = true; + if (curOption is ITreeFloatOption) curFloatOption = cast curOption; + } updateItems(true); return; } @@ -91,8 +97,14 @@ class TreeMenuScreen extends FlxSpriteGroup { if (length > 0 && curOption != null) { if (controls.ACCEPT || (FlxG.mouse.justPressed && FlxG.mouse.overlaps(members[curSelected]))) curOption.select(); - if (leftTurboControl.activated) curOption.changeSelection(-1); - if (rightTurboControl.activated) curOption.changeSelection(1); + 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(); @@ -142,7 +154,15 @@ class TreeMenuScreen extends FlxSpriteGroup { 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 (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); diff --git a/source/funkin/options/categories/GameplayOptions.hx b/source/funkin/options/categories/GameplayOptions.hx index 3faa11db..219ed322 100644 --- a/source/funkin/options/categories/GameplayOptions.hx +++ b/source/funkin/options/categories/GameplayOptions.hx @@ -14,8 +14,9 @@ class GameplayOptions extends TreeMenuScreen { 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')); + 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('volumeSFX'), getDescID('volumeSFX'), 0, 1, 1, 10, 'volumeSFX')); add(new Separator()); add(new TextOption('optionsMenu.advanced', 'optionsTree.gameplay.advanced-desc', ' >', () -> @@ -23,6 +24,7 @@ class GameplayOptions extends TreeMenuScreen { } private function __changeOffset(offset:Float) Conductor.songOffset = offset; + private function __changeAutoPause() FlxG.autoPause = Options.autoPause; var __lastBeat:Int = 0; var __lastSongBeat:Int = 0; diff --git a/source/funkin/options/type/NumOption.hx b/source/funkin/options/type/NumOption.hx index 6590afd9..953294ff 100644 --- a/source/funkin/options/type/NumOption.hx +++ b/source/funkin/options/type/NumOption.hx @@ -10,7 +10,7 @@ class NumOption extends TextOption { public var max:Float; public var step:Float; - public var currentSelection:Float; + public var currentValue:Float; public var parent:Dynamic; public var optionName:String; @@ -31,20 +31,20 @@ class NumOption extends TextOption { this.optionName = optionName; this.parent = parent = parent != null ? parent : Options; - if (Reflect.field(parent, optionName) != null) currentSelection = Reflect.field(parent, optionName); + if (Reflect.field(parent, optionName) != null) currentValue = Reflect.field(parent, optionName); - __number = new Alphabet(0, 20, ': $currentSelection', 'bold'); + __number = new Alphabet(0, 20, ': $currentValue', 'bold'); super(text, desc); add(__number); } override function changeSelection(change:Int):Void { if (locked) return; - if (currentSelection == (currentSelection = FlxMath.bound(currentSelection + change, min, max))) return; - __number.text = ': $currentSelection'; + if (currentValue == (currentValue = FlxMath.bound(currentValue + change, min, max))) return; + __number.text = ': $currentValue'; - Reflect.setField(parent, optionName, currentSelection); - if (changedCallback != null) changedCallback(currentSelection); + Reflect.setField(parent, optionName, currentValue); + if (changedCallback != null) changedCallback(currentValue); CoolUtil.playMenuSFX(SCROLL); } diff --git a/source/funkin/options/type/SliderOption.hx b/source/funkin/options/type/SliderOption.hx index 5831a47d..ed0ce5b8 100644 --- a/source/funkin/options/type/SliderOption.hx +++ b/source/funkin/options/type/SliderOption.hx @@ -1,26 +1,58 @@ package funkin.options.type; -class SliderOption extends OptionType { - public var selectCallback:Float->Void; +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 segmentVal:Float; + public var step:Float; - public var currentSelection:Float; + public var currentValue:Float; public var parent:Dynamic; + public var optionName:String; - //public var barWidth(get, set):Int; + public var slider:Slider; - override function set_text(v:String) return __text.text = text = v; + function getValue():Float return (currentValue - min) / (max - min); - 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) + public function new(text:String, desc:String, min:Float, max:Float, step:Float, ?segments:Int, optionName:String, barWidth = 600.0, + ?changedCallback:Float->Void = null, ?parent:Dynamic) { super(text, desc); + 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); + + add(slider = new Slider(16, 0, getValue(), barWidth, segments)); + slider.scale.set(0.75, 0.75); + slider.updateHitbox(); + slider.y = __text.y + (__text.height - slider.height) * 0.5; } + + override function update(elapsed:Float) { + slider.selected = selected && !locked; + 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 From d434568f5d464f2068632eef4d404db6aebc5fab Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 27 Jul 2025 20:00:38 +0700 Subject: [PATCH 19/25] whatever agahgh --- source/funkin/menus/ui/Slider.hx | 6 ++++-- source/funkin/options/OptionsMenu.hx | 21 ++++++++++++++++--- .../options/categories/GameplayOptions.hx | 2 +- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/source/funkin/menus/ui/Slider.hx b/source/funkin/menus/ui/Slider.hx index c60a740c..589dffd4 100644 --- a/source/funkin/menus/ui/Slider.hx +++ b/source/funkin/menus/ui/Slider.hx @@ -45,12 +45,13 @@ class Slider extends FlxSprite { switch (animation.curAnim.name) { case 'selected': if (!selected) playAnimOrDefault('deselect', 'unselected'); case 'unselected': if (selected) playAnimOrDefault('selecting', 'selected'); - case 'selecting': if (animation.finished) animation.play('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'); } } } @@ -85,6 +86,7 @@ class Slider extends FlxSprite { 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; barHeight = 0; @@ -105,7 +107,7 @@ class Slider extends FlxSprite { override function set_frame(v:FlxFrame):FlxFrame { super.set_frame(v); - sliderResetFrameSize(); + if (v != null) sliderResetFrameSize(); return v; } diff --git a/source/funkin/options/OptionsMenu.hx b/source/funkin/options/OptionsMenu.hx index 8fd3e238..6359a21f 100644 --- a/source/funkin/options/OptionsMenu.hx +++ b/source/funkin/options/OptionsMenu.hx @@ -88,8 +88,7 @@ class OptionsMenu extends TreeMenu { } })])); - tree.first().add(debugOption = new TextOption('optionsTree.debug-name', 'optionsTree.debug-desc', ' >', () -> addMenu(new DebugOptions()))); - debugOption.locked = !Options.devMode; + checkDebugOption(); for (i in funkin.backend.assets.ModsFolder.getLoadedMods()) { var xmlPath = Paths.xml('config/options/LIB_$i'); @@ -103,6 +102,22 @@ class OptionsMenu extends TreeMenu { } } + 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); + } + } + public function updateBG() { var scaleX:Float = FlxG.width / bg.width; var scaleY:Float = FlxG.height / bg.height; @@ -119,7 +134,7 @@ class OptionsMenu extends TreeMenu { override function menuChanged() { super.menuChanged(); - debugOption.locked = !Options.devMode; + checkDebugOption(); } override function exit() { diff --git a/source/funkin/options/categories/GameplayOptions.hx b/source/funkin/options/categories/GameplayOptions.hx index 219ed322..afb8fc51 100644 --- a/source/funkin/options/categories/GameplayOptions.hx +++ b/source/funkin/options/categories/GameplayOptions.hx @@ -16,7 +16,7 @@ class GameplayOptions extends TreeMenuScreen { 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('volumeSFX'), getDescID('volumeSFX'), 0, 1, 1, 10, 'volumeSFX')); + add(new SliderOption(getNameID('volumeSFX'), getDescID('volumeSFX'), 0, 1, 1, 6, 'volumeSFX')); add(new Separator()); add(new TextOption('optionsMenu.advanced', 'optionsTree.gameplay.advanced-desc', ' >', () -> From 7f8e4360eaa7e5626bbf15dc2fd3467227307833 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Mon, 28 Jul 2025 04:49:01 +0700 Subject: [PATCH 20/25] Sliders half finished --- assets/images/menus/options/slider.xml | 8 +- source/funkin/menus/ui/Slider.hx | 204 +++++++++++++++++- .../options/categories/GameplayOptions.hx | 2 +- 3 files changed, 198 insertions(+), 16 deletions(-) diff --git a/assets/images/menus/options/slider.xml b/assets/images/menus/options/slider.xml index facddef9..134049b9 100644 --- a/assets/images/menus/options/slider.xml +++ b/assets/images/menus/options/slider.xml @@ -1,11 +1,11 @@ - - + + - - + + diff --git a/source/funkin/menus/ui/Slider.hx b/source/funkin/menus/ui/Slider.hx index 589dffd4..68eba451 100644 --- a/source/funkin/menus/ui/Slider.hx +++ b/source/funkin/menus/ui/Slider.hx @@ -3,13 +3,16 @@ 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:Int; + public var segments(default, set):Int; public var barWidth(default, set):Float; public var barHeight(default, null):Float; @@ -17,15 +20,36 @@ class Slider extends FlxSprite { 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 = 4) { + 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) { @@ -35,7 +59,7 @@ class Slider extends FlxSprite { override function update(elapsed:Float) { super.update(elapsed); - __animTime += elapsed; + __frameTime = Math.floor((__animTime += elapsed) * barFramerate); if (animation.curAnim == null) { animation.play(selected ? 'selected' : 'unselected'); @@ -58,12 +82,158 @@ class Slider extends FlxSprite { override function isSimpleRender(?camera:FlxCamera):Bool return false; override function drawComplex(camera:FlxCamera) { - if (showSlider) { - var xOff = frame.sourceSize.x * 0.5 - barWidth * value, yOff = (frame.sourceSize.y - barHeight) * 0.5; - frameOffset.add(xOff, yOff); - super.drawComplex(camera); - frameOffset.subtract(xOff, yOff); + __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 + __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 != !__flipX) __barClipRect.set(0, 0, __valueWidth - x, frame.sourceSize.y); + else __barClipRect.set(__cornerWidth - __valueWidth + x, 0, __valueWidth - x, frame.sourceSize.y); + __barFrame = frame.clipTo(__barClipRect, __barFrame); + __barFrame.prepareMatrix(_matrix, ANGLE_0, flip != camera.flipX, __flipY != camera.flipY); + prepareDrawMatrix(camera, x, 0); + forceDrawFrame(camera, __barFrame); + } + + if (__valueWidth - x < __cornerWidth) { + anim = getBarAnim(1, false); + frame = frames.frames[anim.frames[__frameTime % anim.numFrames]]; + + if (flip != __flipX) __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 != camera.flipX, __flipY != camera.flipY); + prepareDrawMatrix(camera, 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, 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, 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, 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, x, 0); + forceDrawFrame(camera, __barFrame); + } + + return x + width; } function getBarAnim(type:Int, filled:Bool):FlxAnimation { @@ -88,11 +258,21 @@ class Slider extends FlxSprite { animation.addByPrefix('selecting', 'slider selecting0', 24, false); animation.addByPrefix('segment', 'slider segment0', 24, false); - var anim; + 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) barHeight = Math.max(barHeight, frames.frames[anim.frames[0]].sourceSize.y); - if ((anim = getBarAnim(type, true)) != null) barHeight = Math.max(barHeight, frames.frames[anim.frames[0]].sourceSize.y); + if ((anim = getBarAnim(type, false)) != null) checkSize(type); + if ((anim = getBarAnim(type, true)) != null) checkSize(type); } sliderResetFrameSize(); @@ -115,4 +295,6 @@ class Slider extends FlxSprite { 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/categories/GameplayOptions.hx b/source/funkin/options/categories/GameplayOptions.hx index afb8fc51..c7acbd0e 100644 --- a/source/funkin/options/categories/GameplayOptions.hx +++ b/source/funkin/options/categories/GameplayOptions.hx @@ -16,7 +16,7 @@ class GameplayOptions extends TreeMenuScreen { 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('volumeSFX'), getDescID('volumeSFX'), 0, 1, 1, 6, 'volumeSFX')); + add(new SliderOption(getNameID('volumeSFX'), getDescID('volumeSFX'), 0, 1, 1, 5, 'volumeSFX')); add(new Separator()); add(new TextOption('optionsMenu.advanced', 'optionsTree.gameplay.advanced-desc', ' >', () -> From 5ed5c57727c193d5d4d2edcb7cf910ef33c5fe60 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Mon, 28 Jul 2025 04:53:24 +0700 Subject: [PATCH 21/25] frakits told me to put them on the right --- source/funkin/options/type/Checkbox.hx | 14 +++++++++++--- source/funkin/options/type/SliderOption.hx | 16 ++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/source/funkin/options/type/Checkbox.hx b/source/funkin/options/type/Checkbox.hx index 592b7723..2e333420 100644 --- a/source/funkin/options/type/Checkbox.hx +++ b/source/funkin/options/type/Checkbox.hx @@ -19,12 +19,17 @@ class Checkbox extends TextOption { "checking" => FlxPoint.get(35, 29) ]; + override function set_text(v:String) { + super.set_text(v); + checkbox.x = __text.x + __text.width + 26; + return v; + } + public function new(text:String, desc:String, optionName:String, ?selectCallback:Void->Void, ?parent:Dynamic) { - super(text, desc, selectCallback); this.optionName = optionName; this.parent = parent = parent != null ? parent : Options; - add(checkbox = new FlxSprite(10, -30)); + 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); @@ -34,10 +39,13 @@ class Checkbox extends TextOption { checkbox.scale.set(0.75, 0.75); checkbox.updateHitbox(); - __text.x = 100; + //__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; diff --git a/source/funkin/options/type/SliderOption.hx b/source/funkin/options/type/SliderOption.hx index ed0ce5b8..1cdfc3a0 100644 --- a/source/funkin/options/type/SliderOption.hx +++ b/source/funkin/options/type/SliderOption.hx @@ -19,10 +19,15 @@ class SliderOption extends TextOption implements ITreeFloatOption { 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; + return v; + } + public function new(text:String, desc:String, min:Float, max:Float, step:Float, ?segments:Int, optionName:String, barWidth = 600.0, ?changedCallback:Float->Void = null, ?parent:Dynamic) { - super(text, desc); this.changedCallback = changedCallback; this.min = min; this.max = max; @@ -32,16 +37,19 @@ class SliderOption extends TextOption implements ITreeFloatOption { if (Reflect.field(parent, optionName) != null) currentValue = Reflect.field(parent, optionName); - add(slider = new Slider(16, 0, getValue(), barWidth, segments)); + slider = new Slider(12, 0, getValue(), barWidth, segments); slider.scale.set(0.75, 0.75); slider.updateHitbox(); - slider.y = __text.y + (__text.height - slider.height) * 0.5; + + super(text, desc); + add(slider); + slider.y = Math.floor(__text.y + (__text.height - slider.height) * 0.5 + 2); } override function update(elapsed:Float) { slider.selected = selected && !locked; slider.value = getValue(); - __text.x = slider.x + slider.width + 30; + //__text.x = slider.x + slider.width + 30; super.update(elapsed); } From 5cd09e3e34903c1eceb427b40bd086f17e148e8d Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Mon, 28 Jul 2025 04:53:30 +0700 Subject: [PATCH 22/25] Fix slider atlas a bit --- assets/images/menus/options/slider.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/images/menus/options/slider.xml b/assets/images/menus/options/slider.xml index 134049b9..81a9dd2b 100644 --- a/assets/images/menus/options/slider.xml +++ b/assets/images/menus/options/slider.xml @@ -2,8 +2,8 @@ - - + + From 5bcc2b82ca26a37aaecb91238563aa4bf683bafa Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:54:34 +0700 Subject: [PATCH 23/25] Complete Slider and Mouse Control on Sliders --- source/funkin/menus/ui/Slider.hx | 24 ++++++++++----------- source/funkin/options/TreeMenuScreen.hx | 18 +++++++++++++--- source/funkin/options/type/SliderOption.hx | 25 +++++++++++++++++++--- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/source/funkin/menus/ui/Slider.hx b/source/funkin/menus/ui/Slider.hx index 68eba451..c55d9724 100644 --- a/source/funkin/menus/ui/Slider.hx +++ b/source/funkin/menus/ui/Slider.hx @@ -99,7 +99,7 @@ class Slider extends FlxSprite { if (showSlider) { _frame.prepareMatrix(_matrix, ANGLE_0, __flipX != camera.flipX, __flipY != camera.flipY); - prepareDrawMatrix(camera, -_frame.sourceSize.x * 0.5 + __valueWidth, -(_frame.sourceSize.y - barHeight) * 0.5); + prepareDrawMatrix(camera, -_frame.sourceSize.x * 0.5 + (__flipX ? barWidth - __valueWidth : __valueWidth), -(_frame.sourceSize.y - barHeight) * 0.5); forceDrawFrame(camera, _frame); } } @@ -151,11 +151,11 @@ class Slider extends FlxSprite { anim = getBarAnim(1, true); frame = frames.frames[anim.frames[__frameTime % anim.numFrames]]; - if (!flip != !__flipX) __barClipRect.set(0, 0, __valueWidth - x, frame.sourceSize.y); - else __barClipRect.set(__cornerWidth - __valueWidth + x, 0, __valueWidth - x, frame.sourceSize.y); + 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 != camera.flipX, __flipY != camera.flipY); - prepareDrawMatrix(camera, x, 0); + __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); } @@ -163,11 +163,11 @@ class Slider extends FlxSprite { anim = getBarAnim(1, false); frame = frames.frames[anim.frames[__frameTime % anim.numFrames]]; - if (flip != __flipX) __barClipRect.set(0, 0, __cornerWidth - __valueWidth + x, frame.sourceSize.y); + 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 != camera.flipX, __flipY != camera.flipY); - prepareDrawMatrix(camera, x, 0); + __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); } @@ -187,7 +187,7 @@ class Slider extends FlxSprite { __barFrame = frame.clipTo(__barClipRect, __barFrame); __barFrame.prepareMatrix(_matrix, ANGLE_0, __flipX != camera.flipX, __flipY != camera.flipY); _matrix.scale(s, 1); - prepareDrawMatrix(camera, x, 0); + prepareDrawMatrix(camera, __flipX ? barWidth - x - width : x, 0); forceDrawFrame(camera, __barFrame); } @@ -200,7 +200,7 @@ class Slider extends FlxSprite { __barFrame = frame.clipTo(__barClipRect, __barFrame); __barFrame.prepareMatrix(_matrix, ANGLE_0, __flipX != camera.flipX, __flipY != camera.flipY); _matrix.scale(s, 1); - prepareDrawMatrix(camera, x, 0); + prepareDrawMatrix(camera, __flipX ? barWidth - x - width : x, 0); forceDrawFrame(camera, __barFrame); } @@ -218,7 +218,7 @@ class Slider extends FlxSprite { __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, x, 0); + prepareDrawMatrix(camera, __flipX ? barWidth - x - width : x, 0); forceDrawFrame(camera, __barFrame); } @@ -229,7 +229,7 @@ class Slider extends FlxSprite { __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, x, 0); + prepareDrawMatrix(camera, __flipX ? barWidth - x - width : x, 0); forceDrawFrame(camera, __barFrame); } diff --git a/source/funkin/options/TreeMenuScreen.hx b/source/funkin/options/TreeMenuScreen.hx index 3bec7899..33883fc0 100644 --- a/source/funkin/options/TreeMenuScreen.hx +++ b/source/funkin/options/TreeMenuScreen.hx @@ -93,10 +93,20 @@ class TreeMenuScreen extends FlxSpriteGroup { if (inputEnabled) { for (basic in turboBasics) basic.update(elapsed); - changeSelection((upTurboControl.activated ? -1 : 0) + (downTurboControl.activated ? 1 : 0) - FlxG.mouse.wheel); + + 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 || (FlxG.mouse.justPressed && FlxG.mouse.overlaps(members[curSelected]))) curOption.select(); + 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); @@ -172,9 +182,11 @@ class TreeMenuScreen extends FlxSpriteGroup { 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/type/SliderOption.hx b/source/funkin/options/type/SliderOption.hx index 1cdfc3a0..7ecb4a84 100644 --- a/source/funkin/options/type/SliderOption.hx +++ b/source/funkin/options/type/SliderOption.hx @@ -16,16 +16,20 @@ class SliderOption extends TextOption implements ITreeFloatOption { 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; return v; } - public function new(text:String, desc:String, min:Float, max:Float, step:Float, ?segments:Int, optionName:String, barWidth = 600.0, + 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; @@ -37,7 +41,7 @@ class SliderOption extends TextOption implements ITreeFloatOption { if (Reflect.field(parent, optionName) != null) currentValue = Reflect.field(parent, optionName); - slider = new Slider(12, 0, getValue(), barWidth, segments); + slider = new Slider(12, 0, getValue(), (dynamicWidth = barWidth < 0) ? 600 : barWidth, segments); slider.scale.set(0.75, 0.75); slider.updateHitbox(); @@ -47,7 +51,22 @@ class SliderOption extends TextOption implements ITreeFloatOption { } override function update(elapsed:Float) { - slider.selected = selected && !locked; + 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; From da29fc4a4d59c1661962e0da669a3b61f849b13d Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:59:58 +0700 Subject: [PATCH 24/25] shorten allowConfigWarning-name --- assets/languages/en/Options.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/languages/en/Options.xml b/assets/languages/en/Options.xml index e4ce1bdc..e286a5e5 100644 --- a/assets/languages/en/Options.xml +++ b/assets/languages/en/Options.xml @@ -149,7 +149,7 @@ Developer Mode If checked, you will be able to access developer features like certain keybinds, editors, developer options, state reloads, console and more. - Allow Mod Configs Warning Popup + 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 From 20cf90f5c25f6ffb058549b73ed4f9c2124f0e17 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Mon, 28 Jul 2025 19:07:01 +0700 Subject: [PATCH 25/25] Add Music Volume --- assets/languages/en/Options.xml | 3 +++ source/funkin/options/Options.hx | 2 ++ source/funkin/options/categories/GameplayOptions.hx | 4 +++- source/funkin/options/type/SliderOption.hx | 5 ++++- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/assets/languages/en/Options.xml b/assets/languages/en/Options.xml index e286a5e5..7145f793 100644 --- a/assets/languages/en/Options.xml +++ b/assets/languages/en/Options.xml @@ -112,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. diff --git a/source/funkin/options/Options.hx b/source/funkin/options/Options.hx index cbb90d1b..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; @@ -225,6 +226,7 @@ class Options gameplayShaders = true; } + FlxG.sound.defaultMusicGroup.volume = volumeMusic; FlxG.game.stage.quality = (FlxG.enableAntialiasing = antialiasing) ? BEST : LOW; FlxG.autoPause = autoPause; if (FlxG.updateFramerate < framerate) FlxG.drawFramerate = FlxG.updateFramerate = framerate; diff --git a/source/funkin/options/categories/GameplayOptions.hx b/source/funkin/options/categories/GameplayOptions.hx index c7acbd0e..7d66f351 100644 --- a/source/funkin/options/categories/GameplayOptions.hx +++ b/source/funkin/options/categories/GameplayOptions.hx @@ -16,6 +16,7 @@ class GameplayOptions extends TreeMenuScreen { 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()); @@ -23,8 +24,9 @@ class GameplayOptions extends TreeMenuScreen { parent.addMenu(new AdvancedGameplayOptions()))); } - private function __changeOffset(offset:Float) 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; diff --git a/source/funkin/options/type/SliderOption.hx b/source/funkin/options/type/SliderOption.hx index 7ecb4a84..955b61d9 100644 --- a/source/funkin/options/type/SliderOption.hx +++ b/source/funkin/options/type/SliderOption.hx @@ -25,7 +25,10 @@ class SliderOption extends TextOption implements ITreeFloatOption { override function set_text(v:String) { super.set_text(v); slider.x = __text.x + __text.width + 30; - if (dynamicWidth) slider.barWidth = 1100 - __text.width; + if (dynamicWidth) { + slider.barWidth = 1100 - __text.width; + slider.updateHitbox(); + } return v; }