package funkin.ui.options; import flixel.FlxCamera; import flixel.FlxObject; import flixel.FlxSprite; import flixel.text.FlxText; import flixel.util.FlxColor; import flixel.FlxG; import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; import flixel.math.FlxPoint; import funkin.ui.AtlasText.AtlasFont; import funkin.ui.Page; import funkin.graphics.FunkinCamera; import funkin.graphics.FunkinSprite; import funkin.ui.TextMenuList.TextMenuItem; import funkin.ui.options.items.CheckboxPreferenceItem; import funkin.ui.options.items.NumberPreferenceItem; import funkin.ui.options.items.EnumPreferenceItem; #if mobile import funkin.mobile.ui.FunkinBackButton; import funkin.mobile.input.ControlsHandler; import funkin.mobile.ui.FunkinHitbox.FunkinHitboxControlSchemes; import funkin.util.TouchUtil; import funkin.util.SwipeUtil; #end import funkin.util.HapticUtil; import lime.ui.WindowVSyncMode; class PreferencesMenu extends Page { var items:TextMenuList; var preferenceItems:FlxTypedSpriteGroup; var preferenceDesc:Array = []; var itemDesc:FlxText; var itemDescBox:FunkinSprite; var menuCamera:FlxCamera; var hudCamera:FlxCamera; var camFollow:FlxObject; public function new() { super(); menuCamera = new FunkinCamera('prefMenu'); FlxG.cameras.add(menuCamera, false); menuCamera.bgColor = 0x0; hudCamera = new FlxCamera(); FlxG.cameras.add(hudCamera, false); hudCamera.bgColor = 0x0; camera = menuCamera; add(items = new TextMenuList()); add(preferenceItems = new FlxTypedSpriteGroup()); add(itemDescBox = new FunkinSprite()); itemDescBox.cameras = [hudCamera]; add(itemDesc = new FlxText(0, 0, 1180, null, 32)); itemDesc.cameras = [hudCamera]; createPrefItems(); createPrefDescription(); camFollow = new FlxObject(FlxG.width / 2, 0, 140, 70); menuCamera.follow(camFollow, null, 0.085); var margin = 160; menuCamera.deadzone.set(0, margin, menuCamera.width, menuCamera.height - margin * 2); menuCamera.minScrollY = 0; items.onChange.add(function(selected) { itemDesc.text = preferenceDesc[items.selectedIndex]; }); #if FEATURE_TOUCH_CONTROLS var backButton:FunkinBackButton = new FunkinBackButton(FlxG.width - 230, FlxG.height - 200, exit, 1.0); add(backButton); #end } /** * Create the description for preferences. */ function createPrefDescription():Void { itemDescBox.makeSolidColor(1, 1, FlxColor.BLACK); itemDescBox.alpha = 0.6; itemDesc.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.CENTER, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK); itemDesc.borderSize = 3; // Update the text. itemDesc.text = preferenceDesc[items.selectedIndex]; itemDesc.screenCenter(); itemDesc.y += 270; // Create the box around the text. itemDescBox.setPosition(itemDesc.x - 10, itemDesc.y - 10); itemDescBox.setGraphicSize(Std.int(itemDesc.width + 20), Std.int(itemDesc.height + 25)); itemDescBox.updateHitbox(); } /** * Create the menu items for each of the preferences. */ function createPrefItems():Void { #if FEATURE_NAUGHTYNESS createPrefItemCheckbox('Naughtyness', 'If enabled, raunchy content (such as swearing, etc.) will be displayed.', function(value:Bool):Void { Preferences.naughtyness = value; }, Preferences.naughtyness); #end createPrefItemCheckbox('Downscroll', 'If enabled, this will make the notes move downwards.', function(value:Bool):Void { Preferences.downscroll = value; }, Preferences.downscroll, #if mobile ControlsHandler.hasExternalInputDevice || Preferences.controlsScheme != FunkinHitboxControlSchemes.Arrows #end); createPrefItemPercentage('Strumline Background', 'Give the strumline a semi-transparent background', function(value:Int):Void { Preferences.strumlineBackgroundOpacity = value; }, Preferences.strumlineBackgroundOpacity); #if FEATURE_HAPTICS createPrefItemEnum('Haptics', 'If enabled, game will use haptic feedback effects.', [ "All" => HapticsMode.ALL, "Notes Only" => HapticsMode.NOTES_ONLY, "None" => HapticsMode.NONE, ], function(key:String, value:HapticsMode):Void { Preferences.hapticsMode = value; }, switch (Preferences.hapticsMode) { case HapticsMode.NOTES_ONLY: "Notes Only"; case HapticsMode.NONE: "None"; default: "All"; }); createPrefItemNumber('Haptics Intensity', 'Multiplier of intensity for all the haptic feedback effects.', function(value:Float) { Preferences.hapticsIntensityMultiplier = value; }, null, Preferences.hapticsIntensityMultiplier, 0.1, 5, 0.1, 1); #end createPrefItemCheckbox('Flashing Lights', 'If disabled, it will dampen flashing effects. Useful for people with photosensitive epilepsy.', function(value:Bool):Void { Preferences.flashingLights = value; }, Preferences.flashingLights); createPrefItemCheckbox('Camera Zooms', 'If disabled, camera stops bouncing to the song.', function(value:Bool):Void { Preferences.zoomCamera = value; }, Preferences.zoomCamera); #if !mobile createPrefItemCheckbox('Debug Display', 'If enabled, FPS and other debug stats will be displayed.', function(value:Bool):Void { Preferences.debugDisplay = value; }, Preferences.debugDisplay); createPrefItemCheckbox('Pause on Unfocus', 'If enabled, game automatically pauses when it loses focus.', function(value:Bool):Void { Preferences.autoPause = value; }, Preferences.autoPause); createPrefItemCheckbox('Launch in Fullscreen', 'Automatically launch the game in fullscreen on startup.', function(value:Bool):Void { Preferences.autoFullscreen = value; }, Preferences.autoFullscreen); #end #if web createPrefItemCheckbox('Unlocked Framerate', 'If enabled, the framerate will be unlocked.', function(value:Bool):Void { Preferences.unlockedFramerate = value; }, Preferences.unlockedFramerate); #else // disabled on macos due to "error: Late swap tearing currently unsupported" // disable on mobile since it barely has any effect #if !(mac || mobile) createPrefItemEnum('VSync', 'If enabled, game will attempt to match framerate with your monitor.', [ "Off" => WindowVSyncMode.OFF, "On" => WindowVSyncMode.ON, "Adaptive" => WindowVSyncMode.ADAPTIVE, ], function(key:String, value:WindowVSyncMode):Void { trace("Setting vsync mode to " + key); Preferences.vsyncMode = value; }, switch (Preferences.vsyncMode) { case WindowVSyncMode.OFF: "Off"; case WindowVSyncMode.ON: "On"; case WindowVSyncMode.ADAPTIVE: "Adaptive"; }); #end #if !mobile createPrefItemNumber('FPS', 'The maximum framerate that the game targets.', function(value:Float) { Preferences.framerate = Std.int(value); }, null, Preferences.framerate, 30, 500, 5, 0); #end #end #if FEATURE_SCREENSHOTS createPrefItemCheckbox('Hide Mouse', 'If enabled, the mouse will be hidden when taking a screenshot.', function(value:Bool):Void { Preferences.shouldHideMouse = value; }, Preferences.shouldHideMouse); createPrefItemCheckbox('Fancy Preview', 'If enabled, a preview will be shown after taking a screenshot.', function(value:Bool):Void { Preferences.fancyPreview = value; }, Preferences.fancyPreview); createPrefItemCheckbox('Preview on save', 'If enabled, the preview will be shown only after a screenshot is saved.', function(value:Bool):Void { Preferences.previewOnSave = value; }, Preferences.previewOnSave); #end } override function update(elapsed:Float):Void { super.update(elapsed); // Positions the camera to the selected item. if (items != null) camFollow.y = items.selectedItem.y; // Indent the selected item. items.forEach(function(daItem:TextMenuItem) { var thyOffset:Int = 0; // Initializing thy text width (if thou text present) var thyTextWidth:Int = 0; switch (Type.typeof(daItem)) { case TClass(CheckboxPreferenceItem): thyTextWidth = 0; thyOffset = 0; case TClass(EnumPreferenceItem): thyTextWidth = cast(daItem, EnumPreferenceItem).lefthandText.getWidth(); thyOffset = 0 + thyTextWidth - 75; case TClass(NumberPreferenceItem): thyTextWidth = cast(daItem, NumberPreferenceItem).lefthandText.getWidth(); thyOffset = 0 + thyTextWidth - 75; default: // Huh? } if (items.selectedItem == daItem) { thyOffset += 150; } else { thyOffset += 120; } daItem.x = thyOffset + funkin.ui.FullScreenScaleMode.gameNotchSize.x; }); } // - Preference item creation methods - // Should be moved into a separate PreferenceItems class but you can't access PreferencesMenu.items and PreferencesMenu.preferenceItems from outside. /** * Creates a pref item that works with booleans * @param onChange Gets called every time the player changes the value; use this to apply the value * @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable) */ function createPrefItemCheckbox(prefName:String, prefDesc:String, onChange:Bool->Void, defaultValue:Bool, available:Bool = true):Void { var checkbox:CheckboxPreferenceItem = new CheckboxPreferenceItem(funkin.ui.FullScreenScaleMode.gameNotchSize.x, 120 * (items.length - 1 + 1), defaultValue, available); items.createItem(0, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() { var value = !checkbox.currentValue; onChange(value); checkbox.currentValue = value; }, false, available); preferenceItems.add(checkbox); preferenceDesc.push(prefDesc); } /** * Creates a pref item that works with general numbers * @param onChange Gets called every time the player changes the value; use this to apply the value * @param valueFormatter Will get called every time the game needs to display the float value; use this to change how the displayed value looks * @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable) * @param min Minimum value (example: 0) * @param max Maximum value (example: 10) * @param step The value to increment/decrement by (default = 0.1) * @param precision Rounds decimals up to a `precision` amount of digits (ex: 4 -> 0.1234, 2 -> 0.12) */ function createPrefItemNumber(prefName:String, prefDesc:String, onChange:Float->Void, ?valueFormatter:Float->String, defaultValue:Float, min:Float, max:Float, step:Float = 0.1, precision:Int):Void { var item = new NumberPreferenceItem(funkin.ui.FullScreenScaleMode.gameNotchSize.x, (120 * items.length) + 30, prefName, defaultValue, min, max, step, precision, onChange, valueFormatter); items.addItem(prefName, item); preferenceItems.add(item.lefthandText); preferenceDesc.push(prefDesc); } /** * Creates a pref item that works with number percentages * @param onChange Gets called every time the player changes the value; use this to apply the value * @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable) * @param min Minimum value (default = 0) * @param max Maximum value (default = 100) */ function createPrefItemPercentage(prefName:String, prefDesc:String, onChange:Int->Void, defaultValue:Int, min:Int = 0, max:Int = 100):Void { var newCallback = function(value:Float) { onChange(Std.int(value)); }; var formatter = function(value:Float) { return '${value}%'; }; var item = new NumberPreferenceItem(funkin.ui.FullScreenScaleMode.gameNotchSize.x, (120 * items.length) + 30, prefName, defaultValue, min, max, 10, 0, newCallback, formatter); items.addItem(prefName, item); preferenceItems.add(item.lefthandText); preferenceDesc.push(prefDesc); } /** * Creates a pref item that works with enums * @param values Maps enum values to display strings _(ex: `NoteHitSoundType.PingPong => "Ping pong"`)_ * @param onChange Gets called every time the player changes the value; use this to apply the value * @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable) */ function createPrefItemEnum(prefName:String, prefDesc:String, values:Map, onChange:String->T->Void, defaultKey:String):Void { var item = new EnumPreferenceItem(funkin.ui.FullScreenScaleMode.gameNotchSize.x, (120 * items.length) + 30, prefName, values, defaultKey, onChange); items.addItem(prefName, item); preferenceItems.add(item.lefthandText); preferenceDesc.push(prefDesc); } override function exit():Void { camFollow.setPosition(640, 30); menuCamera.snapToTarget(); super.exit(); } }