Options Rewrite WIP not functional yet

This commit is contained in:
Ralty
2025-07-26 16:21:53 +07:00
parent 71242699ea
commit f20fc25a82
19 changed files with 639 additions and 409 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<TextureAtlas imagePath="meter.png" width="102" height="175">
<SubTexture name="meter empty corner0000.png" x="0" y="104" width="39" height="71"/>
<SubTexture name="meter empty piece0000.png" x="42" y="104" width="2" height="71"/>
<SubTexture name="meter filled corner0000.png" x="47" y="104" width="39" height="71"/>
<SubTexture name="meter filled piece0000.png" x="89" y="104" width="2" height="71"/>
<SubTexture name="meter slider0000.png" x="0" y="0" width="102" height="102"/>
</TextureAtlas>
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<TextureAtlas imagePath="slider.png" width="463" height="151">
<SubTexture name="bar empty0000.png" x="119" y="130" width="70" height="12" rotated="true" frameX="-0" frameY="-0" frameWidth="12" frameHeight="70"/>
<SubTexture name="bar filled0000.png" x="193" y="127" width="70" height="12" rotated="true" frameX="-0" frameY="-0" frameWidth="12" frameHeight="70"/>
<SubTexture name="corner empty0000.png" x="267" y="114" width="70" height="28" rotated="true" frameX="-0" frameY="-0" frameWidth="28" frameHeight="70"/>
<SubTexture name="corner filled0000.png" x="341" y="108" width="70" height="28" rotated="true" frameX="-0" frameY="-0" frameWidth="28" frameHeight="70"/>
<SubTexture name="segment empty0000.png" x="372" y="97" width="70" height="6" rotated="true" frameX="-0" frameY="-0" frameWidth="6" frameHeight="70"/>
<SubTexture name="segment filled0000.png" x="193" y="143" width="70" height="6" rotated="true" frameX="-0" frameY="-0" frameWidth="6" frameHeight="70"/>
<SubTexture name="slider deselect0000.png" x="372" y="2" width="89" height="91" frameX="-0" frameY="-12" frameWidth="89" frameHeight="109"/>
<SubTexture name="slider deselect0001.png" x="372" y="2" width="89" height="91" frameX="-0" frameY="-12" frameWidth="89" frameHeight="109"/>
<SubTexture name="slider deselect0002.png" x="250" y="2" width="56" height="108" frameX="-17" frameY="-1" frameWidth="89" frameHeight="109"/>
<SubTexture name="slider deselect0003.png" x="250" y="2" width="56" height="108" frameX="-17" frameY="-1" frameWidth="89" frameHeight="109"/>
<SubTexture name="slider segment0000.png" x="61" y="2" width="54" height="137" frameX="-0" frameY="-1" frameWidth="54" frameHeight="138"/>
<SubTexture name="slider segment0001.png" x="61" y="2" width="54" height="137" frameX="-0" frameY="-1" frameWidth="54" frameHeight="138"/>
<SubTexture name="slider selected0000.png" x="119" y="2" width="56" height="124" frameX="-0" frameY="-0" frameWidth="56" frameHeight="124"/>
<SubTexture name="slider selecting0000.png" x="2" y="2" width="55" height="140" frameX="-7" frameY="-0" frameWidth="67" frameHeight="140"/>
<SubTexture name="slider selecting0001.png" x="2" y="2" width="55" height="140" frameX="-7" frameY="-0" frameWidth="67" frameHeight="140"/>
<SubTexture name="slider selecting0002.png" x="179" y="2" width="67" height="121" frameX="-0" frameY="-10" frameWidth="67" frameHeight="140"/>
<SubTexture name="slider selecting0003.png" x="179" y="2" width="67" height="121" frameX="-0" frameY="-10" frameWidth="67" frameHeight="140"/>
<SubTexture name="slider unselected0000.png" x="310" y="2" width="58" height="102" frameX="-0" frameY="-0" frameWidth="58" frameHeight="102"/>
</TextureAtlas>
+121
View File
@@ -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<Control>;
public function new(controls:Array<Control>, ?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<FlxAction>;
public function new(actions:Array<FlxAction>, ?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<FlxKey>;
public function new(keys:Array<FlxKey>, ?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<FlxGamepadInputID>;
public var gamepad:FlxGamepad;
public function new(inputs:Array<FlxGamepadInputID>, ?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;
}
}
+2
View File
@@ -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);
+2 -2
View File
@@ -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';
+2 -1
View File
@@ -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<String, Bool> = "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);
}
}
+2 -1
View File
@@ -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() {
+32 -117
View File
@@ -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<OptionsScreen, Class<OptionsScreen>, (name:String, desc:String) -> OptionsScreen>;
var ?substate:OneOfThree<MusicBeatSubstate, Class<MusicBeatSubstate>, (name:String, desc:String) -> MusicBeatSubstate>;
var ?suffix:String;
}
class OptionsMenu extends TreeMenu {
public static var mainOptions:Array<OptionCategory> = [
{ // 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<OptionsScreen>
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<OptionsScreen>
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<OptionType> {
var options:Array<OptionType> = [];
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<Dynamic> = [];
var optionDisplayOptions:Array<String> = [];
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;
}
}
-89
View File
@@ -1,89 +0,0 @@
package funkin.options;
class OptionsTree extends FlxTypedGroup<OptionsScreen> {
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) {
}
}
+231 -110
View File
@@ -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:FlxTypedSignal<TreeMenuScreen->Void> = new FlxTypedSignal();
public var onMenuChanged:FlxTypedSignal<TreeMenuScreen->Void> = 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<TreeMenuScreen>;
public var length(default, null):Int = 0;
public var lastMenu:Null<TreeMenuScreen>;
public var destroyMenus:Bool = true;
public static var lastState:Class<FlxState> = 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<OptionsScreen, Class<OptionsScreen>, (name:String, desc:String) -> OptionsScreen>;
var ?substate:OneOfThree<MusicBeatSubstate, Class<MusicBeatSubstate>, (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<OptionType>;
var args:Array<Dynamic>;
}
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();
}
}
+130
View File
@@ -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<TurboBasics>;
var curOption:ITreeOption;
var _firstFrame:Bool = true;
public function new(name:String, desc:String, ?prefix:String, ?options:Array<OptionType>) {
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();
}
}
@@ -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;
}
}
@@ -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"),
@@ -1,5 +1,7 @@
#if !macro
package funkin.options.categories;
import funkin.options.TreeMenu;
import funkin.options.TreeMenuScreen;
import funkin.options.type.*;
#end
+27 -58
View File
@@ -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<Dynamic>;
public var displayOptions:Array<String>;
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<Dynamic>, displayOptions:Array<String>, optionName:String, ?selectCallback:String->Void = null, ?parent:Dynamic) {
super(desc);
public function new(text:String, desc:String, options:Array<Dynamic>, displayOptions:Array<String>, ?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;
}
}
+20 -12
View File
@@ -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() {}
}
@@ -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;
}
}