package funkin.ui.options; import funkin.util.InputUtil; import flixel.FlxCamera; import flixel.FlxObject; import flixel.FlxSprite; import funkin.graphics.FunkinCamera; import flixel.group.FlxGroup; import flixel.input.actions.FlxActionInput; import flixel.input.gamepad.FlxGamepadInputID; import flixel.input.keyboard.FlxKey; import funkin.graphics.FunkinSprite; import funkin.input.Controls; import funkin.ui.AtlasText; import funkin.ui.MenuList; import funkin.ui.TextMenuList; class ControlsMenu extends funkin.ui.options.OptionsState.Page { public static inline final COLUMNS = 2; static var controlList = Control.createAll(); /* * Defines groups of controls that cannot share inputs, like left and right. Say, if ACCEPT is Z, Back is X, * if the player sets Back to Z it also set ACCEPT to X. This prevents the player from setting the controls in * a way the prevents them from changing more controls or exiting the menu. */ static var controlGroups:Array> = [ [NOTE_UP, NOTE_DOWN, NOTE_LEFT, NOTE_RIGHT], [UI_UP, UI_DOWN, UI_LEFT, UI_RIGHT, ACCEPT, BACK], [CUTSCENE_ADVANCE], [FREEPLAY_FAVORITE, FREEPLAY_LEFT, FREEPLAY_RIGHT], [WINDOW_FULLSCREEN, WINDOW_SCREENSHOT], [VOLUME_UP, VOLUME_DOWN, VOLUME_MUTE], [DEBUG_MENU, DEBUG_CHART] ]; var itemGroups:Array> = [for (i in 0...controlGroups.length) []]; var controlGrid:MenuTypedList; var deviceList:TextMenuList; var menuCamera:FlxCamera; var prompt:Prompt; var camFollow:FlxObject; var labels:FlxTypedGroup; var currentDevice:Device = Keys; var deviceListSelected:Bool = false; public function new() { super(); menuCamera = new FunkinCamera('controlsMenu'); FlxG.cameras.add(menuCamera, false); menuCamera.bgColor = 0x0; camera = menuCamera; labels = new FlxTypedGroup(); var headers:FlxTypedGroup = new FlxTypedGroup(); controlGrid = new MenuTypedList(Columns(COLUMNS), Vertical); add(labels); add(headers); add(controlGrid); if (FlxG.gamepads.numActiveGamepads > 0) { var devicesBg:FunkinSprite = new FunkinSprite(); devicesBg.makeSolidColor(FlxG.width, 100, 0xFFFAFD6D); add(devicesBg); deviceList = new TextMenuList(Horizontal, None); add(deviceList); deviceListSelected = true; var item:TextMenuItem; item = deviceList.createItem('Keyboard', AtlasFont.BOLD, selectDevice.bind(Keys)); item.x = FlxG.width / 2 - item.width - 30; item.y = (devicesBg.height - item.height) / 2; item = deviceList.createItem('Gamepad', AtlasFont.BOLD, selectDevice.bind(Gamepad(FlxG.gamepads.firstActive.id))); item.x = FlxG.width / 2 + 30; item.y = (devicesBg.height - item.height) / 2; } // FlxG.debugger.drawDebug = true; var y = deviceList == null ? 30 : 120; var spacer = 70; var currentHeader:String = null; // list order is determined by enum order for (i in 0...controlList.length) { var control = controlList[i]; var name = control.getName(); if (currentHeader != "UI_" && name.indexOf("UI_") == 0) { currentHeader = "UI_"; headers.add(new AtlasText(0, y, "UI", AtlasFont.BOLD)).screenCenter(X); y += spacer; } else if (currentHeader != "NOTE_" && name.indexOf("NOTE_") == 0) { currentHeader = "NOTE_"; headers.add(new AtlasText(0, y, "NOTES", AtlasFont.BOLD)).screenCenter(X); y += spacer; } else if (currentHeader != "CUTSCENE_" && name.indexOf("CUTSCENE_") == 0) { currentHeader = "CUTSCENE_"; headers.add(new AtlasText(0, y, "CUTSCENE", AtlasFont.BOLD)).screenCenter(X); y += spacer; } else if (currentHeader != "FREEPLAY_" && name.indexOf("FREEPLAY_") == 0) { currentHeader = "FREEPLAY_"; headers.add(new AtlasText(0, y, "FREEPLAY", AtlasFont.BOLD)).screenCenter(X); y += spacer; } else if (currentHeader != "WINDOW_" && name.indexOf("WINDOW_") == 0) { currentHeader = "WINDOW_"; headers.add(new AtlasText(0, y, "WINDOW", AtlasFont.BOLD)).screenCenter(X); y += spacer; } else if (currentHeader != "VOLUME_" && name.indexOf("VOLUME_") == 0) { currentHeader = "VOLUME_"; headers.add(new AtlasText(0, y, "VOLUME", AtlasFont.BOLD)).screenCenter(X); y += spacer; } else if (currentHeader != "DEBUG_" && name.indexOf("DEBUG_") == 0) { currentHeader = "DEBUG_"; headers.add(new AtlasText(0, y, "DEBUG", AtlasFont.BOLD)).screenCenter(X); y += spacer; } if (currentHeader != null && name.indexOf(currentHeader) == 0) name = name.substr(currentHeader.length); var label = labels.add(new AtlasText(100, y, name, AtlasFont.BOLD)); label.alpha = 0.6; for (i in 0...COLUMNS) createItem(label.x + 550 + i * 400, y, control, i); y += spacer; } camFollow = new FlxObject(FlxG.width / 2, 0, 70, 70); if (deviceList != null) { camFollow.y = deviceList.selectedItem.y; controlGrid.selectedItem.idle(); controlGrid.enabled = false; } else camFollow.y = controlGrid.selectedItem.y; menuCamera.follow(camFollow, null, 0.06); var margin = 100; menuCamera.deadzone.set(0, margin, menuCamera.width, menuCamera.height - margin * 2); menuCamera.minScrollY = 0; controlGrid.onChange.add(function(selected) { camFollow.y = selected.y; labels.forEach((label) -> label.alpha = 0.6); labels.members[Std.int(controlGrid.selectedIndex / COLUMNS)].alpha = 1.0; }); prompt = new Prompt("\nPress any key to rebind\n\n\nBackspace to unbind\n Escape to cancel", None); prompt.create(); prompt.createBgFromMargin(100, 0xFFfafd6d); prompt.back.scrollFactor.set(0, 0); prompt.exists = false; add(prompt); } function createItem(x = 0.0, y = 0.0, control:Control, index:Int) { var item = new InputItem(x, y, currentDevice, control, index, onSelect); for (i in 0...controlGroups.length) { if (controlGroups[i].contains(control)) itemGroups[i].push(item); } return controlGrid.addItem(item.name, item); } function onSelect():Void { switch (currentDevice) { case Keys: { keyUsedToEnterPrompt = FlxG.keys.firstJustPressed(); } case Gamepad(id): { buttonUsedToEnterPrompt = FlxG.gamepads.getByID(id).firstJustPressedID(); } } controlGrid.enabled = false; canExit = false; prompt.exists = true; } function goToDeviceList() { controlGrid.selectedItem.idle(); labels.members[Std.int(controlGrid.selectedIndex / COLUMNS)].alpha = 0.6; controlGrid.enabled = false; deviceList.enabled = true; canExit = true; camFollow.y = deviceList.selectedItem.y; deviceListSelected = true; } function selectDevice(device:Device) { currentDevice = device; for (item in controlGrid.members) item.updateDevice(currentDevice); var inputName = device == Keys ? "key" : "button"; var cancel = device == Keys ? "Escape" : "Back"; // todo: alignment if (device == Keys) prompt.setText('\nPress any key to rebind\n\n\n\n $cancel to cancel'); else prompt.setText('\nPress any button\n to rebind\n\n\n $cancel to cancel'); controlGrid.selectedItem.select(); labels.members[Std.int(controlGrid.selectedIndex / COLUMNS)].alpha = 1.0; controlGrid.enabled = true; deviceList.enabled = false; deviceListSelected = false; canExit = false; } var keyUsedToEnterPrompt:Null = null; var buttonUsedToEnterPrompt:Null = null; override function update(elapsed:Float):Void { super.update(elapsed); var controls = PlayerSettings.player1.controls; if (controlGrid.enabled && deviceList != null && deviceListSelected == false && controls.BACK) goToDeviceList(); if (prompt.exists) { switch (currentDevice) { case Keys: { // Um? // Checking pressed causes problems when you change the BACK key, // but checking released causes problems when the prompt is instant. // keyUsedToEnterPrompt is my weird workaround. var key = FlxG.keys.firstJustReleased(); if (key != NONE && key != keyUsedToEnterPrompt) { if (key == ESCAPE) { closePrompt(); } else if (key == BACKSPACE) { onInputSelect(NONE); closePrompt(); } else { onInputSelect(key); closePrompt(); } } } case Gamepad(id): { var button = FlxG.gamepads.getByID(id).firstJustReleasedID(); if (button != NONE && button != buttonUsedToEnterPrompt) { if (button != BACK) onInputSelect(button); closePrompt(); } var key = FlxG.keys.firstJustReleased(); if (key != NONE && key != keyUsedToEnterPrompt) { if (key == ESCAPE) { closePrompt(); } else if (key == BACKSPACE) { onInputSelect(NONE); closePrompt(); } } } } } switch (currentDevice) { case Keys: { var keyJustReleased:Int = FlxG.keys.firstJustReleased(); if (keyJustReleased != NONE && keyJustReleased == keyUsedToEnterPrompt) { keyUsedToEnterPrompt = null; } buttonUsedToEnterPrompt = null; } case Gamepad(id): { var buttonJustReleased:Int = FlxG.gamepads.getByID(id).firstJustReleasedID(); if (buttonJustReleased != NONE && buttonJustReleased == buttonUsedToEnterPrompt) { buttonUsedToEnterPrompt = null; } keyUsedToEnterPrompt = null; } } } function onInputSelect(input:Int):Void { var item = controlGrid.selectedItem; // check if that key is already set for this if (input != FlxKey.NONE) { var column0 = Math.floor(controlGrid.selectedIndex / 2) * 2; for (i in 0...COLUMNS) { if (controlGrid.members[column0 + i].input == input) return; } } // Check if items in the same group already have the new input for (group in itemGroups) { if (input != FlxKey.NONE && group.contains(item)) { for (otherItem in group) { if (otherItem != item && otherItem.input == input) { // replace that input with this items old input. PlayerSettings.player1.controls.replaceBinding(otherItem.control, currentDevice, item.input, otherItem.input); // Don't use resetItem() since items share names/labels otherItem.input = item.input; otherItem.label.text = item.label.text; } } } } PlayerSettings.player1.controls.replaceBinding(item.control, currentDevice, input, item.input); // Don't use resetItem() since items share names/labels item.input = input; item.label.text = item.getLabel(input); // Shift left on the grid if the item on the right is bound and the item on the left is unbound. if (controlGrid.selectedIndex % 2 == 1) { trace('Modified item on right side of grid'); var leftItem = controlGrid.members[controlGrid.selectedIndex - 1]; if (leftItem != null && input != FlxKey.NONE && leftItem.input == FlxKey.NONE) { trace('Left item is unbound and right item is not!'); // Swap them. var temp = leftItem.input; leftItem.input = item.input; item.input = temp; leftItem.label.text = leftItem.getLabel(leftItem.input); item.label.text = item.getLabel(item.input); } } else { trace('Modified item on left side of grid'); var rightItem = controlGrid.members[controlGrid.selectedIndex + 1]; if (rightItem != null && input == FlxKey.NONE && rightItem.input != FlxKey.NONE) { trace('Left item is unbound and right item is not!'); // Swap them. var temp = item.input; item.input = rightItem.input; rightItem.input = temp; item.label.text = item.getLabel(item.input); rightItem.label.text = rightItem.getLabel(rightItem.input); } } PlayerSettings.player1.saveControls(); } function closePrompt() { prompt.exists = false; controlGrid.enabled = true; if (deviceList == null) canExit = true; } override function destroy() { super.destroy(); itemGroups = null; if (FlxG.cameras.list.contains(menuCamera)) FlxG.cameras.remove(menuCamera); } override function set_enabled(value:Bool) { if (value == false) { controlGrid.enabled = false; if (deviceList != null) deviceList.enabled = false; } else { controlGrid.enabled = !deviceListSelected; if (deviceList != null) deviceList.enabled = deviceListSelected; } return super.set_enabled(value); } } class InputItem extends TextMenuItem { public var device(default, null):Device = Keys; public var control:Control; public var input:Int = -1; public var index:Int = -1; public function new(x = 0.0, y = 0.0, device, control, index, ?callback) { this.device = device; this.control = control; this.index = index; this.input = getInput(); super(x, y, getLabel(input), DEFAULT, callback); this.fireInstantly = true; } public function updateDevice(device:Device) { if (this.device != device) { this.device = device; input = getInput(); label.text = getLabel(input); } } function getInput() { var list = PlayerSettings.player1.controls.getInputsFor(control, device); if (list.length > index) { if (list[index] != FlxKey.ESCAPE || list[index] != FlxGamepadInputID.BACK) return list[index]; if (list.length > ControlsMenu.COLUMNS) // Escape isn't mappable, show a third option, instead. return list[ControlsMenu.COLUMNS]; } return -1; } public function getLabel(input:Int) { return input == FlxKey.NONE ? "---" : InputUtil.format(input, device); } }