From fab4d1d7884c41f38a08f38944eeab8b296d00d8 Mon Sep 17 00:00:00 2001 From: Chronic x3 <126361810+chronicsilly@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:38:20 +0100 Subject: [PATCH 01/40] Seperate pretty print options for the Chart, Character, and Stage editors. The editors that save as xmls have pretty print enabled by default btw --- assets/languages/en/Options.xml | 10 ++++++++-- assets/languages/pl/Options.xml | 3 --- source/funkin/editors/DebugOptions.hx | 14 +++++++++++--- .../funkin/editors/character/CharacterEditor.hx | 3 ++- source/funkin/editors/charter/Charter.hx | 16 ++++++++-------- source/funkin/editors/stage/StageEditor.hx | 4 ++-- source/funkin/game/Character.hx | 13 +++++++------ source/funkin/options/Options.hx | 6 +++++- 8 files changed, 43 insertions(+), 26 deletions(-) diff --git a/assets/languages/en/Options.xml b/assets/languages/en/Options.xml index f0ec9ea7..2b5a6c00 100644 --- a/assets/languages/en/Options.xml +++ b/assets/languages/en/Options.xml @@ -157,8 +157,14 @@ Editor SFXs If checked, will play sound effects when working on editors (ex: will play sfxs when checking checkboxes...) - Pretty Print - If checked, the saved files from the editor will be formatted to be easily viewable (does not apply to xmls...) + Chart Pretty Print + If checked, the saved files from the chart editor will be formatted to be easily viewable + + Character Pretty Print + If checked, the saved files from the character editor will be formatted to be easily viewable + + Stage Pretty Print + If checked, the saved files from the stage editor will be formatted to be easily viewable Intensive Blur If checked, will use more intensive blur that may be laggier but look better. diff --git a/assets/languages/pl/Options.xml b/assets/languages/pl/Options.xml index 0549951f..0415b455 100644 --- a/assets/languages/pl/Options.xml +++ b/assets/languages/pl/Options.xml @@ -157,9 +157,6 @@ If checked, only musics will have streamed audio, ALSO freeing a LOT of memory w Dźwięki Edytorów Jeśli włączone, niektóre elementy edytorów będą grać dźwięki. (Na przykład: dźwięki pól wyboru...) - Ładny Druk - Jeśli włączone, zapisane pliki z edytorów będą używać formatowania łatwiejszego do odczytania. (opcja nie dotyczy plików xml, tylko po angielsku...) - Mocniejszy Rozmaz Jeśli włączone, gra użyje mocniejszego rozmazu, który kosztuje wydajność, ale wygląda lepiej. diff --git a/source/funkin/editors/DebugOptions.hx b/source/funkin/editors/DebugOptions.hx index 93d85992..25375a3b 100644 --- a/source/funkin/editors/DebugOptions.hx +++ b/source/funkin/editors/DebugOptions.hx @@ -49,9 +49,17 @@ class DebugOptionsScreen extends OptionsScreen { getDesc("editorSFX"), "editorSFX")); add(new Checkbox( - getName("editorPrettyPrint"), - getDesc("editorPrettyPrint"), - "editorPrettyPrint")); + getName("editorCharterPrettyPrint"), + getDesc("editorCharterPrettyPrint"), + "editorCharterPrettyPrint")); + add(new Checkbox( + getName("editorCharacterPrettyPrint"), + getDesc("editorCharacterPrettyPrint"), + "editorCharacterPrettyPrint")); + add(new Checkbox( + getName("editorStagePrettyPrint"), + getDesc("editorStagePrettyPrint"), + "editorStagePrettyPrint")); add(new Checkbox( getName("intensiveBlur"), getDesc("intensiveBlur"), diff --git a/source/funkin/editors/character/CharacterEditor.hx b/source/funkin/editors/character/CharacterEditor.hx index 5885db7f..9fb0f469 100644 --- a/source/funkin/editors/character/CharacterEditor.hx +++ b/source/funkin/editors/character/CharacterEditor.hx @@ -484,7 +484,8 @@ class CharacterEditor extends UIState { button.anim ]); - return "\n" + Printer.print(charXML, true); + var xmlThingYea:String = "\n" + Printer.print(charXML, Options.editorCharacterPrettyPrint); + return Options.editorCharacterPrettyPrint ? xmlThingYea : xmlThingYea.replace("\n", ""); } var clipboard:FlxPoint = FlxPoint.get(); diff --git a/source/funkin/editors/charter/Charter.hx b/source/funkin/editors/charter/Charter.hx index 0865f0a9..c7e20fc5 100644 --- a/source/funkin/editors/charter/Charter.hx +++ b/source/funkin/editors/charter/Charter.hx @@ -788,9 +788,9 @@ class Charter extends UIState { var songPath:String = '${Paths.getAssetsRoot()}/songs/${__song.toLowerCase()}'; if (Options.charterAutoSavesSeparateFolder) - Chart.save(songPath, PlayState.SONG, __autoSaveLocation, {saveMetaInChart: true, saveLocalEvents: true, saveGlobalEvents: true, folder: "autosaves", prettyPrint: Options.editorPrettyPrint}); + Chart.save(songPath, PlayState.SONG, __autoSaveLocation, {saveMetaInChart: true, saveLocalEvents: true, saveGlobalEvents: true, folder: "autosaves", prettyPrint: Options.editorCharterPrettyPrint}); else // These two chart saves are particular, to avoid any kind of loss, stuff like meta, global and local events will be save all together - Nex - Chart.save(songPath, PlayState.SONG, __diff.toLowerCase(), {saveMetaInChart: true, saveLocalEvents: true, saveGlobalEvents: true, prettyPrint: Options.editorPrettyPrint}); + Chart.save(songPath, PlayState.SONG, __diff.toLowerCase(), {saveMetaInChart: true, saveLocalEvents: true, saveGlobalEvents: true, prettyPrint: Options.editorCharterPrettyPrint}); undos.save(); } autoSaveNotif.cancelled = false; @@ -1480,7 +1480,7 @@ class Charter extends UIState { } public static function saveChartAs(shouldBuild:Bool = true, withEvents:Bool = true) { - saveAs(Chart.filterChartForSaving(PlayState.SONG, false, withEvents, false), null, Options.editorPrettyPrint ? Flags.JSON_PRETTY_PRINT : null, { + saveAs(Chart.filterChartForSaving(PlayState.SONG, false, withEvents, false), null, Options.editorCharterPrettyPrint ? Flags.JSON_PRETTY_PRINT : null, { defaultSaveFile: '${__diff.toLowerCase()}.json' }, null, shouldBuild); if (undos != null) undos.save(); @@ -1492,7 +1492,7 @@ class Charter extends UIState { var data = {events: Chart.filterChartForSaving(PlayState.SONG, false, false, true).events}; var path = '${Paths.getAssetsRoot()}/songs/${__song.toLowerCase()}/events.json'; - if (data.events != null && data.events.length > 0) CoolUtil.safeSaveFile(path, Json.stringify(data, null, Options.editorPrettyPrint ? Flags.JSON_PRETTY_PRINT : null)); + if (data.events != null && data.events.length > 0) CoolUtil.safeSaveFile(path, Json.stringify(data, null, Options.editorCharterPrettyPrint ? Flags.JSON_PRETTY_PRINT : null)); else if (FileSystem.exists(path)) FileSystem.deleteFile(path); // Instead of replacing with a useless empty file, deletes the file directly - Nex #else saveEventsAs(shouldBuild); @@ -1503,7 +1503,7 @@ class Charter extends UIState { if (shouldBuild && instance != null) instance.buildChart(); var data = {events: Chart.filterChartForSaving(PlayState.SONG, false, false, true).events}; - saveAs(data, null, Options.editorPrettyPrint ? Flags.JSON_PRETTY_PRINT : null, { + saveAs(data, null, Options.editorCharterPrettyPrint ? Flags.JSON_PRETTY_PRINT : null, { defaultSaveFile: 'events.json' }, null, false); } @@ -1527,13 +1527,13 @@ class Charter extends UIState { } public static function saveLegacyChartAs(shouldBuild:Bool = true) { - saveAs(FNFLegacyParser.encode(PlayState.SONG), null, Options.editorPrettyPrint ? Flags.JSON_PRETTY_PRINT : null, { + saveAs(FNFLegacyParser.encode(PlayState.SONG), null, Options.editorCharterPrettyPrint ? Flags.JSON_PRETTY_PRINT : null, { defaultSaveFile: '${__song.toLowerCase().replace(" ", "-")}${__diff.toLowerCase() == Flags.DEFAULT_DIFFICULTY ? "" : '-${__diff.toLowerCase()}'}.json', }, null, shouldBuild); } public static function savePsychChartAs(shouldBuild:Bool = true) { - saveAs(PsychParser.encode(PlayState.SONG), null, Options.editorPrettyPrint ? Flags.JSON_PRETTY_PRINT : null, { + saveAs(PsychParser.encode(PlayState.SONG), null, Options.editorCharterPrettyPrint ? Flags.JSON_PRETTY_PRINT : null, { defaultSaveFile: '${__song.toLowerCase().replace(" ", "-")}${__diff.toLowerCase() == Flags.DEFAULT_DIFFICULTY ? "" : '-${__diff.toLowerCase()}'}.json', }, null, shouldBuild); } @@ -1551,7 +1551,7 @@ class Charter extends UIState { #if sys public static function saveTo(path:String, separateEvents:Bool = false, shouldBuild:Bool = true) { if (shouldBuild && instance != null) instance.buildChart(); - Chart.save(path, PlayState.SONG, __diff.toLowerCase(), {saveMetaInChart: false, saveLocalEvents: !separateEvents, prettyPrint: Options.editorPrettyPrint}); + Chart.save(path, PlayState.SONG, __diff.toLowerCase(), {saveMetaInChart: false, saveLocalEvents: !separateEvents, prettyPrint: Options.editorCharterPrettyPrint}); } #end #end diff --git a/source/funkin/editors/stage/StageEditor.hx b/source/funkin/editors/stage/StageEditor.hx index c865b8d7..0ed80aee 100644 --- a/source/funkin/editors/stage/StageEditor.hx +++ b/source/funkin/editors/stage/StageEditor.hx @@ -724,8 +724,8 @@ class StageEditor extends UIState { } } - var xmlThingYea:String = "\n" + Printer.print(xml, Options.editorPrettyPrint); - return Options.editorPrettyPrint ? xmlThingYea : xmlThingYea.replace("\n", ""); + var xmlThingYea:String = "\n" + Printer.print(xml, Options.editorStagePrettyPrint); + return Options.editorStagePrettyPrint ? xmlThingYea : xmlThingYea.replace("\n", ""); } function _edit_undo(_) { diff --git a/source/funkin/game/Character.hx b/source/funkin/game/Character.hx index 15a9729f..32008f18 100644 --- a/source/funkin/game/Character.hx +++ b/source/funkin/game/Character.hx @@ -429,12 +429,12 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset var realFlipped:Bool = isPlayer ? !__baseFlipped : __baseFlipped; if (realFlipped) xml.set("flipX", "true"); - xml.set("icon", getIcon()); + if (icon != curCharacter) xml.set("icon", getIcon()); if (gameOverCharacter != Character.FALLBACK_DEAD_CHARACTER) xml.set("gameOverChar", gameOverCharacter); if (iconColor != null) xml.set("color", iconColor.toWebString()); - xml.set("sprite", sprite); + if (sprite != curCharacter) xml.set("sprite", sprite); if (scale.x != 1) xml.set("scale", Std.string(FlxMath.roundDecimal(scale.x, 4))); if (!antialiasing) xml.set("antialiasing", antialiasing == true ? "true" : "false"); @@ -453,12 +453,12 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset animXml.set("name", anim.name); animXml.set("anim", anim.anim); - animXml.set("loop", Std.string(anim.loop)); - animXml.set("fps", Std.string(FlxMath.roundDecimal(anim.fps, 2))); + if (anim.loop) animXml.set("loop", Std.string(anim.loop)); + if (FlxMath.roundDecimal(anim.fps, 2) != 24) animXml.set("fps", Std.string(FlxMath.roundDecimal(anim.fps, 2))); var offset:FlxPoint = getAnimOffset(anim.name); - animXml.set("x", Std.string(FlxMath.roundDecimal(offset.x, 2))); - animXml.set("y", Std.string(FlxMath.roundDecimal(offset.y, 2))); + if (FlxMath.roundDecimal(offset.x, 2) != 0) animXml.set("x", Std.string(FlxMath.roundDecimal(offset.x, 2))); + if (FlxMath.roundDecimal(offset.y, 2) != 0) animXml.set("y", Std.string(FlxMath.roundDecimal(offset.y, 2))); offset.putWeak(); if (anim.indices.length > 0) @@ -474,6 +474,7 @@ class Character extends FunkinSprite implements IBeatReceiver implements IOffset } this.xml = new Access(xml); + return xml; } diff --git a/source/funkin/options/Options.hx b/source/funkin/options/Options.hx index a60d519d..0e31a51a 100644 --- a/source/funkin/options/Options.hx +++ b/source/funkin/options/Options.hx @@ -55,7 +55,11 @@ class Options */ public static var intensiveBlur:Bool = true; public static var editorSFX:Bool = true; - public static var editorPrettyPrint:Bool = false; + + public static var editorCharterPrettyPrint:Bool = false; + public static var editorCharacterPrettyPrint:Bool = true; + public static var editorStagePrettyPrint:Bool = true; + public static var editorsResizable:Bool = true; public static var bypassEditorsResize:Bool = false; public static var maxUndos:Int = 120; From 16e5c323901c7f36f41b25ec1e9aa912ae02fb59 Mon Sep 17 00:00:00 2001 From: Chronic x3 <126361810+chronicsilly@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:49:52 +0100 Subject: [PATCH 02/40] EditorPicker if it didn't mess muscle memory up --- source/funkin/editors/EditorPicker.hx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/source/funkin/editors/EditorPicker.hx b/source/funkin/editors/EditorPicker.hx index ff349203..7ddd06f8 100644 --- a/source/funkin/editors/EditorPicker.hx +++ b/source/funkin/editors/EditorPicker.hx @@ -8,11 +8,6 @@ class EditorPicker extends MusicBeatSubstate { // Name is for backwards compatibility, don't use it, use id instead public var options:Array = [ - { - name: "Alphabet Editor", - id: "alphabet", - state: funkin.editors.alphabet.AlphabetSelection - }, { name: "Chart Editor", id: "chart", @@ -28,6 +23,11 @@ class EditorPicker extends MusicBeatSubstate { id: "stage", state: funkin.editors.stage.StageSelection }, + { + name: "Alphabet Editor", + id: "alphabet", + state: funkin.editors.alphabet.AlphabetSelection + }, #if (debug || debug_ui) { name: "UI Debug State", From 077bf8e116d54fa44906420f5d4829fb78c57b14 Mon Sep 17 00:00:00 2001 From: Chronic x3 <126361810+chronicsilly@users.noreply.github.com> Date: Fri, 25 Jul 2025 20:46:55 +0100 Subject: [PATCH 03/40] Debug options are now in the options menu Also disappear/reappear depending on the devMode bool's state --- source/funkin/editors/EditorPicker.hx | 5 --- source/funkin/options/OptionsMenu.hx | 28 +++++++++++++++ source/funkin/options/OptionsScreen.hx | 4 +++ .../categories}/DebugOptions.hx | 36 +++---------------- 4 files changed, 37 insertions(+), 36 deletions(-) rename source/funkin/{editors => options/categories}/DebugOptions.hx (65%) diff --git a/source/funkin/editors/EditorPicker.hx b/source/funkin/editors/EditorPicker.hx index 7ddd06f8..ea751908 100644 --- a/source/funkin/editors/EditorPicker.hx +++ b/source/funkin/editors/EditorPicker.hx @@ -42,11 +42,6 @@ class EditorPicker extends MusicBeatSubstate { onClick: function() { CoolUtil.openURL(Flags.URL_WIKI); } - }, - { - name: "Debug Options", - id: "debugOptions", - state: DebugOptions } ]; diff --git a/source/funkin/options/OptionsMenu.hx b/source/funkin/options/OptionsMenu.hx index ace34aad..cba1c9af 100644 --- a/source/funkin/options/OptionsMenu.hx +++ b/source/funkin/options/OptionsMenu.hx @@ -42,11 +42,21 @@ class OptionsMenu extends TreeMenu { desc: 'optionsTree.miscellaneous-desc', suffix: " >", state: MiscOptions + }, + { + name: "Debug Options", + desc: "debugOptions", + suffix: " >", + state: DebugOptions } ]; var bg:FlxSprite; + var addedDebugOptions:Bool = !Options.devMode; + var debugModeButton:OptionType; + var debugModeIndex:Int = mainOptions.length - 1; + public override function create() { super.create(); @@ -99,6 +109,12 @@ class OptionsMenu extends TreeMenu { if (access != null) for (o in parseOptionsFromXML(access)) main.add(o); } } + + doDebugOptionThing(); + } + + public override function onMenuClose(m:OptionsScreen) { + doDebugOptionThing(); } public function reloadStrings() { @@ -115,6 +131,18 @@ class OptionsMenu extends TreeMenu { super.exit(); } + function doDebugOptionThing() { + if ((Options.devMode && !addedDebugOptions) || (!Options.devMode && addedDebugOptions)) { + trace((addedDebugOptions ? "remove" : "add") + " option"); + addedDebugOptions = !addedDebugOptions; + if (debugModeButton == null) debugModeButton = main.members[debugModeIndex]; + if (addedDebugOptions) + main.add(debugModeButton); + else + main.remove(debugModeButton); + } + } + /** * XML STUFF */ diff --git a/source/funkin/options/OptionsScreen.hx b/source/funkin/options/OptionsScreen.hx index 53dd5b06..bb71aec9 100644 --- a/source/funkin/options/OptionsScreen.hx +++ b/source/funkin/options/OptionsScreen.hx @@ -113,6 +113,10 @@ class OptionsScreen extends FlxTypedSpriteGroup { CoolUtil.playMenuSFX(SCROLL); curSelected = FlxMath.wrap(curSelected + sel, 0, members.length-1); + if (members[curSelected] == null) { + changeSelection(sel, true); + return; + } members[curSelected].selected = true; updateMenuDesc(); } diff --git a/source/funkin/editors/DebugOptions.hx b/source/funkin/options/categories/DebugOptions.hx similarity index 65% rename from source/funkin/editors/DebugOptions.hx rename to source/funkin/options/categories/DebugOptions.hx index 25375a3b..16676890 100644 --- a/source/funkin/editors/DebugOptions.hx +++ b/source/funkin/options/categories/DebugOptions.hx @@ -1,41 +1,15 @@ -package funkin.editors; +package funkin.options.categories; import funkin.backend.utils.NativeAPI; -import funkin.options.OptionsScreen; -import funkin.options.TreeMenu; -import funkin.options.type.*; -class DebugOptions extends TreeMenu { - public override function create() { - super.create(); +class DebugOptions extends OptionsScreen { + public override function new(title:String, desc:String) { + super(title, desc, "DebugOptions."); - 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 + function() NativeAPI.allocConsole())); add(new Checkbox( getName("editorsResizable"), getDesc("editorsResizable"), 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 04/40] 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 8424e9c998e3ee2c323f0152e5f8c011159b8599 Mon Sep 17 00:00:00 2001 From: Kolziak Date: Sat, 26 Jul 2025 13:15:18 +0200 Subject: [PATCH 05/40] keybinds for cameras --- assets/languages/en/Editors.xml | 2 ++ assets/languages/pl/Editors.xml | 4 ++- source/funkin/editors/charter/Charter.hx | 39 ++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/assets/languages/en/Editors.xml b/assets/languages/en/Editors.xml index 86ca86ac..3fe1239c 100644 --- a/assets/languages/en/Editors.xml +++ b/assets/languages/en/Editors.xml @@ -225,6 +225,8 @@ Go back to the start Go forward to the end + Add camera on Opponent + Add camera on Player Mute instrumental Mute voices diff --git a/assets/languages/pl/Editors.xml b/assets/languages/pl/Editors.xml index 2e306386..d41bd2cf 100644 --- a/assets/languages/pl/Editors.xml +++ b/assets/languages/pl/Editors.xml @@ -2,7 +2,7 @@ Autozapisywanie za {0} sekund{1} - ((Anulowane)! + (Anulowane)! Ok Anuluj @@ -225,6 +225,8 @@ Wróć do początku Idź do końca + Dodaj kamerę na Oponenta + Dodaj kamerę na Gracza Wycisz Instrumental Wycisz Wokale diff --git a/source/funkin/editors/charter/Charter.hx b/source/funkin/editors/charter/Charter.hx index 84d1dd57..244a784c 100644 --- a/source/funkin/editors/charter/Charter.hx +++ b/source/funkin/editors/charter/Charter.hx @@ -1853,6 +1853,34 @@ class Charter extends UIState { return bookmarks; } + function _opponent_camera_add(_) { + var __event:CharterEvent = null; + + __event = new CharterEvent(curStepFloat, [{ + name: "Camera Movement", + params:[0], + time: Conductor.getTimeForStep(curStepFloat) + }], true); + __event.refreshEventIcons(); + __event.global = true; + rightEventsGroup.add(__event); + undos.addToUndo(CEditEvent(__event, [], __event.events)); + + } + function _player_camera_add(_) { + var __event:CharterEvent = null; + + __event = new CharterEvent(curStepFloat, [{ + name: "Camera Movement", + params:[1], + time: Conductor.getTimeForStep(curStepFloat) + }], true); + __event.refreshEventIcons(); + rightEventsGroup.add(__event); + undos.addToUndo(CEditEvent(__event, [], __event.events)); + + } + function _bookmarks_add(_) { var addBookmarkAt = function(name:String, color:FlxColor, daStep:Float) { @@ -1965,6 +1993,17 @@ class Charter extends UIState { onSelect: _song_end }, null, + { + label: translate("song.addOpponentCamera"), + keybind: [Q], + onSelect: _opponent_camera_add + }, + { + label: translate("song.addPlayerCamera"), + keybind: [E], + onSelect: _player_camera_add + }, + null, { label: translate("bookmarks.addBookmarkHere"), onSelect: _bookmarks_add 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 06/40] 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 07/40] 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 08/40] 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 09/40] 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 10/40] 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 11/40] 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 12/40] 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 13/40] 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 14/40] 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 15/40] 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 16/40] 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 17/40] 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 18/40] 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 19/40] 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 20/40] 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 21/40] 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 22/40] 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 8524395c74ee0e103f55402bf094607acb4cb01f Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 27 Jul 2025 19:09:27 +0700 Subject: [PATCH 23/40] Fix UIAudioPlayer --- source/flixel/sound/FlxSound.hx | 2 +- source/funkin/editors/ui/UIAudioPlayer.hx | 23 +++++++++++------------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/source/flixel/sound/FlxSound.hx b/source/flixel/sound/FlxSound.hx index 6e83609e..146cdf1a 100644 --- a/source/flixel/sound/FlxSound.hx +++ b/source/flixel/sound/FlxSound.hx @@ -880,7 +880,7 @@ class FlxSound extends FlxBasic { cleanup(false, true); startSound(time); } - else if (playing) { + else { _source.offset = 0; _source.currentTime = time + _offset; } diff --git a/source/funkin/editors/ui/UIAudioPlayer.hx b/source/funkin/editors/ui/UIAudioPlayer.hx index 4b87dd4d..da1707b7 100644 --- a/source/funkin/editors/ui/UIAudioPlayer.hx +++ b/source/funkin/editors/ui/UIAudioPlayer.hx @@ -107,20 +107,17 @@ class UIAudioPlayer extends UIButton { var spritePos:FlxPoint = sprite.getScreenPosition(FlxPoint.get(), __lastDrawCameras[0]); - if (((mousePos.x > (spritePos.x)) && (mousePos.x < (spritePos.x) + timeBar.barWidth)) - && ((mousePos.y > (spritePos.y)) && (mousePos.y < (spritePos.y) + timeBar.barHeight))) { - if (FlxG.mouse.justPressed) { - draggingObj = sprite; - wasPlaying = draggingObj == timeBar ? sound.playing : false; - } + if (((mousePos.x > spritePos.x) && (mousePos.x < spritePos.x + sprite.barWidth)) + && ((mousePos.y > spritePos.y) && (mousePos.y < spritePos.y + sprite.barHeight))) { + if (FlxG.mouse.justPressed) { + if ((draggingObj = sprite) == timeBar && (wasPlaying = sound.playing)) sound.pause(); + } if (FlxG.mouse.pressed) { - if (draggingObj == timeBar) { - if (sound.playing) sound.pause(); + if (draggingObj == timeBar) sound.time = FlxMath.remapToRange(mousePos.x - spritePos.x, 0, timeBar.barWidth, 0, sound.length); - } else if (draggingObj == volumeBar) { + else if (draggingObj == volumeBar) sound.volume = FlxMath.remapToRange(mousePos.x - spritePos.x, 0, volumeBar.barWidth, 0, 1); - } } } spritePos.put(); @@ -128,8 +125,10 @@ class UIAudioPlayer extends UIButton { mousePos.put(); if (FlxG.mouse.released) { - if (draggingObj == timeBar && wasPlaying) - sound.play(wasPlaying = false, sound.time); + if (draggingObj == timeBar && wasPlaying) { + sound.resume(); + wasPlaying = false; + } draggingObj = null; } } From 235d23a2f207e0aafb31efc8e61039965fe63c16 Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 27 Jul 2025 19:28:50 +0700 Subject: [PATCH 24/40] Fix UIAudioPlayer again... and AudioSource.time --- source/funkin/editors/ui/UIAudioPlayer.hx | 6 +++--- source/lime/_internal/backend/native/NativeAudioSource.hx | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/source/funkin/editors/ui/UIAudioPlayer.hx b/source/funkin/editors/ui/UIAudioPlayer.hx index da1707b7..bd7aaec7 100644 --- a/source/funkin/editors/ui/UIAudioPlayer.hx +++ b/source/funkin/editors/ui/UIAudioPlayer.hx @@ -29,9 +29,9 @@ class UIAudioPlayer extends UIButton { public function new(x:Float, y:Float, bytes:Bytes) { sound = FlxG.sound.load(Sound.fromAudioBuffer(AudioBuffer.fromBytes(bytes))); - super(x, y, null, function () { + super(x, y, null, () -> { if (sound.playing) sound.pause(); - else sound.play(false, sound.time); + else sound.play(); }, 58 - 16, 58 - 16); playingSprite = new FlxSprite(x + ((58 - 16)/2) - 8, y + ((58 - 16)/2) - 8).loadGraphic(Paths.image('editors/ui/audio-buttons'), true, 16, 16); @@ -126,7 +126,7 @@ class UIAudioPlayer extends UIButton { if (FlxG.mouse.released) { if (draggingObj == timeBar && wasPlaying) { - sound.resume(); + sound.play(); wasPlaying = false; } draggingObj = null; diff --git a/source/lime/_internal/backend/native/NativeAudioSource.hx b/source/lime/_internal/backend/native/NativeAudioSource.hx index 478ed150..4332fba4 100644 --- a/source/lime/_internal/backend/native/NativeAudioSource.hx +++ b/source/lime/_internal/backend/native/NativeAudioSource.hx @@ -394,6 +394,7 @@ class NativeAudioSource { AL.sourcePlay(handle); } } + else completed = value + 1 < getLength(); if (!playing && streamed) bufferTimes[STREAM_MAX_BUFFERS - (requestBuffers = queuedBuffers = 1)] = value / 1000; 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 25/40] 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 d36c912e9397fb1965c5faf86f482ab2c955ad5e Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Sun, 27 Jul 2025 20:17:30 +0700 Subject: [PATCH 26/40] FIX AGAIN --- source/funkin/editors/ui/UIAudioPlayer.hx | 2 +- source/lime/_internal/backend/native/NativeAudioSource.hx | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/source/funkin/editors/ui/UIAudioPlayer.hx b/source/funkin/editors/ui/UIAudioPlayer.hx index bd7aaec7..b919f5f4 100644 --- a/source/funkin/editors/ui/UIAudioPlayer.hx +++ b/source/funkin/editors/ui/UIAudioPlayer.hx @@ -31,7 +31,7 @@ class UIAudioPlayer extends UIButton { super(x, y, null, () -> { if (sound.playing) sound.pause(); - else sound.play(); + else sound.play(sound.time + 1 >= sound.length ? 0 : sound.time); }, 58 - 16, 58 - 16); playingSprite = new FlxSprite(x + ((58 - 16)/2) - 8, y + ((58 - 16)/2) - 8).loadGraphic(Paths.image('editors/ui/audio-buttons'), true, 16, 16); diff --git a/source/lime/_internal/backend/native/NativeAudioSource.hx b/source/lime/_internal/backend/native/NativeAudioSource.hx index 4332fba4..2414d990 100644 --- a/source/lime/_internal/backend/native/NativeAudioSource.hx +++ b/source/lime/_internal/backend/native/NativeAudioSource.hx @@ -136,9 +136,9 @@ class NativeAudioSource { public function play() { if (playing || disposed) return; + var t = completed ? 0 : getCurrentTime(); playing = true; - if (completed) setCurrentTime(0); - else setCurrentTime(getCurrentTime()); + setCurrentTime(t); } public function pause() { @@ -394,7 +394,8 @@ class NativeAudioSource { AL.sourcePlay(handle); } } - else completed = value + 1 < getLength(); + else + completed = false; if (!playing && streamed) bufferTimes[STREAM_MAX_BUFFERS - (requestBuffers = queuedBuffers = 1)] = value / 1000; From c6fc648ea3b4246e3877b4ebbf002ca7dabe9f4e Mon Sep 17 00:00:00 2001 From: Frakits Date: Sun, 27 Jul 2025 16:20:08 +0300 Subject: [PATCH 27/40] fix minor major bug --- source/funkin/editors/charter/ChartCreationScreen.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/editors/charter/ChartCreationScreen.hx b/source/funkin/editors/charter/ChartCreationScreen.hx index c212481e..11032828 100644 --- a/source/funkin/editors/charter/ChartCreationScreen.hx +++ b/source/funkin/editors/charter/ChartCreationScreen.hx @@ -260,7 +260,7 @@ class StrumLineButton extends UIButton { members.push(scrollSpeedStepper); addLabelOn(scrollSpeedStepper, TU.translate("charterStrumLine.scrollSpeed")); - usesChartScrollSpeed = new UICheckbox(scrollSpeedStepper.x + 104, typeDropdown.y + 135, TU.translate("charterStrumLine.useChartScrollSpeed"), strumLine.scrollSpeed == null); + usesChartScrollSpeed = new UICheckbox(scrollSpeedStepper.x + 104, typeDropdown.y + 135, TU.translate("charterStrumLine.useChartScrollSpeed"), true); usesChartScrollSpeed.onChecked = function(b) { if(b) { scrollSpeedStepper.value = subState.scrollSpeedTextBox.value; From 94fbedb24e2cad0ac5e987681844c52de45b5fea Mon Sep 17 00:00:00 2001 From: Frakits Date: Sun, 27 Jul 2025 17:29:44 +0300 Subject: [PATCH 28/40] Revert "fix minor major bug" This reverts commit c6fc648ea3b4246e3877b4ebbf002ca7dabe9f4e. --- source/funkin/editors/charter/ChartCreationScreen.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/editors/charter/ChartCreationScreen.hx b/source/funkin/editors/charter/ChartCreationScreen.hx index 11032828..c212481e 100644 --- a/source/funkin/editors/charter/ChartCreationScreen.hx +++ b/source/funkin/editors/charter/ChartCreationScreen.hx @@ -260,7 +260,7 @@ class StrumLineButton extends UIButton { members.push(scrollSpeedStepper); addLabelOn(scrollSpeedStepper, TU.translate("charterStrumLine.scrollSpeed")); - usesChartScrollSpeed = new UICheckbox(scrollSpeedStepper.x + 104, typeDropdown.y + 135, TU.translate("charterStrumLine.useChartScrollSpeed"), true); + usesChartScrollSpeed = new UICheckbox(scrollSpeedStepper.x + 104, typeDropdown.y + 135, TU.translate("charterStrumLine.useChartScrollSpeed"), strumLine.scrollSpeed == null); usesChartScrollSpeed.onChecked = function(b) { if(b) { scrollSpeedStepper.value = subState.scrollSpeedTextBox.value; From c936fa7f45b4981d91af13dc35553b19b709cd7b Mon Sep 17 00:00:00 2001 From: Kolze <53307233+KolzeYT@users.noreply.github.com> Date: Sun, 27 Jul 2025 23:10:16 +0200 Subject: [PATCH 29/40] fixed event icons not updating after changing the character --- source/funkin/editors/charter/CharterStrumlineScreen.hx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/source/funkin/editors/charter/CharterStrumlineScreen.hx b/source/funkin/editors/charter/CharterStrumlineScreen.hx index e4b1b318..ce7aaad9 100644 --- a/source/funkin/editors/charter/CharterStrumlineScreen.hx +++ b/source/funkin/editors/charter/CharterStrumlineScreen.hx @@ -144,6 +144,9 @@ class CharterStrumlineScreen extends UISubstateWindow { saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, TU.translate("editor.saveClose"), function() { saveStrumline(); + for (grp in [Charter.instance.leftEventsGroup, Charter.instance.rightEventsGroup]) { + for (e in grp.members) e.refreshEventIcons(); + } close(); }, 125); add(saveButton); @@ -284,4 +287,4 @@ class CharacterButton extends UIButton { super.update(elapsed); } -} \ No newline at end of file +} From 756026745b116a4db58d4a40ddfe312b56e3fa51 Mon Sep 17 00:00:00 2001 From: Kolziak Date: Sun, 27 Jul 2025 23:11:11 +0200 Subject: [PATCH 30/40] Revert "fixed event icons not updating after changing the character" This reverts commit c936fa7f45b4981d91af13dc35553b19b709cd7b. --- source/funkin/editors/charter/CharterStrumlineScreen.hx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/source/funkin/editors/charter/CharterStrumlineScreen.hx b/source/funkin/editors/charter/CharterStrumlineScreen.hx index ce7aaad9..e4b1b318 100644 --- a/source/funkin/editors/charter/CharterStrumlineScreen.hx +++ b/source/funkin/editors/charter/CharterStrumlineScreen.hx @@ -144,9 +144,6 @@ class CharterStrumlineScreen extends UISubstateWindow { saveButton = new UIButton(windowSpr.x + windowSpr.bWidth - 20 - 125, windowSpr.y + windowSpr.bHeight - 16 - 32, TU.translate("editor.saveClose"), function() { saveStrumline(); - for (grp in [Charter.instance.leftEventsGroup, Charter.instance.rightEventsGroup]) { - for (e in grp.members) e.refreshEventIcons(); - } close(); }, 125); add(saveButton); @@ -287,4 +284,4 @@ class CharacterButton extends UIButton { super.update(elapsed); } -} +} \ No newline at end of file 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 31/40] 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 32/40] 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 33/40] 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 34/40] 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 35/40] 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 36/40] 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; } From 50aa23c797aa42aca494c43cfbbb839a6914383a Mon Sep 17 00:00:00 2001 From: HeroEyad Date: Mon, 28 Jul 2025 22:38:59 +0300 Subject: [PATCH 37/40] zzz --- source/funkin/backend/assets/ZipFolderLibrary.hx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/source/funkin/backend/assets/ZipFolderLibrary.hx b/source/funkin/backend/assets/ZipFolderLibrary.hx index ea4dab25..68ef537a 100644 --- a/source/funkin/backend/assets/ZipFolderLibrary.hx +++ b/source/funkin/backend/assets/ZipFolderLibrary.hx @@ -27,11 +27,8 @@ class ZipFolderLibrary extends AssetLibrary implements IModsAssetLibrary { this.libName = libName; this.basePath = basePath; - - if(modName == null) - this.modName = libName; - else - this.modName = modName; + + this.modName = (modName == null) ? libName : modName; zip = SysZip.openFromFile(basePath); zip.read(); From 5b1c4653fedb50738e0851a03575bdca2b161998 Mon Sep 17 00:00:00 2001 From: Jamextreme140 Date: Tue, 29 Jul 2025 01:52:46 -0700 Subject: [PATCH 38/40] "BetaWarningState" import redirect + small fix (#16) --- source/funkin/backend/scripting/Script.hx | 7 +++++-- source/funkin/backend/system/Main.hx | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/source/funkin/backend/scripting/Script.hx b/source/funkin/backend/scripting/Script.hx index 1e0db90d..55b22053 100644 --- a/source/funkin/backend/scripting/Script.hx +++ b/source/funkin/backend/scripting/Script.hx @@ -112,9 +112,9 @@ class Script extends FlxBasic implements IFlxDestroyable { * if you wanna modify it, please edit `hscript.Interp.importRedirects` directly. **/ public static function getDefaultImportRedirects():Map { - var redirects:Map = [ - ]; + var redirects:Map = []; + // Events final events = "funkin.backend.scripting.events."; redirects[events + "CharacterNodeEvent"] = events + "character.CharacterNodeEvent"; redirects[events + "CharacterXMLEvent"] = events + "character.CharacterXMLEvent"; @@ -146,6 +146,9 @@ class Script extends FlxBasic implements IFlxDestroyable { redirects[events + "StageNodeEvent"] = events + "stage.StageNodeEvent"; redirects[events + "StageXMLEvent"] = events + "stage.StageXMLEvent"; + // Old State Names + redirects["funkin.menus.BetaWarningState"] = "funkin.menus.WarningState"; + return redirects; } diff --git a/source/funkin/backend/system/Main.hx b/source/funkin/backend/system/Main.hx index f5844278..d2d3a1bf 100644 --- a/source/funkin/backend/system/Main.hx +++ b/source/funkin/backend/system/Main.hx @@ -11,6 +11,7 @@ import flixel.math.FlxRect; import flixel.system.ui.FlxSoundTray; import funkin.backend.assets.AssetsLibraryList; import funkin.backend.assets.ModsFolder; +import funkin.backend.assets.AssetSource; import funkin.backend.system.framerate.SystemInfo; import funkin.backend.system.modules.*; import funkin.editors.SaveWarning; From a6aa196d1b5488ee174cf42006dc318ea06161fa Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Tue, 29 Jul 2025 19:13:50 +0700 Subject: [PATCH 39/40] Fix a compilation error that came out of nowhere? this happens previously with new-conductor too so like this is concerning but not too much of a big deal --- source/funkin/backend/TurboControls.hx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/source/funkin/backend/TurboControls.hx b/source/funkin/backend/TurboControls.hx index b8bedf22..0990aaa2 100644 --- a/source/funkin/backend/TurboControls.hx +++ b/source/funkin/backend/TurboControls.hx @@ -11,8 +11,8 @@ 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 static var DEFAULT_DELAY:Float = 0.4; + public static var DEFAULT_INTERVAL:Float = 1 / 18; public var delay:Float; public var interval:Float; @@ -22,10 +22,10 @@ class TurboBasic extends FlxBasic { var time:Float = 0; - public function new(delay = DEFAULT_DELAY, interval = DEFAULT_INTERVAL, allPress = false) { + public function new(?delay:Float, ?interval:Float, allPress = false) { super(); - this.delay = delay; - this.interval = interval; + this.delay = delay != null ? delay : DEFAULT_DELAY; + this.interval = interval != null ? interval : DEFAULT_INTERVAL; this.allPress = allPress; } From 40eafeb7abe65a4f9d5a872ca244707660852dfe Mon Sep 17 00:00:00 2001 From: Frakits Date: Tue, 29 Jul 2025 20:18:50 +0300 Subject: [PATCH 40/40] who the fuck choose Q and E istg --- source/funkin/editors/charter/Charter.hx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/funkin/editors/charter/Charter.hx b/source/funkin/editors/charter/Charter.hx index 70b49569..445c30bc 100644 --- a/source/funkin/editors/charter/Charter.hx +++ b/source/funkin/editors/charter/Charter.hx @@ -1995,12 +1995,12 @@ class Charter extends UIState { null, { label: translate("song.addOpponentCamera"), - keybind: [Q], + keybind: [O], onSelect: _opponent_camera_add }, { label: translate("song.addPlayerCamera"), - keybind: [E], + keybind: [P], onSelect: _player_camera_add }, null,