Funkin/source/funkin/ui/options/ControlsMenu.hx
2024-06-09 02:22:03 -04:00

485 lines
14 KiB
Haxe

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<Array<Control>> = [
[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<Array<InputItem>> = [for (i in 0...controlGroups.length) []];
var controlGrid:MenuTypedList<InputItem>;
var deviceList:TextMenuList;
var menuCamera:FlxCamera;
var prompt:Prompt;
var camFollow:FlxObject;
var labels:FlxTypedGroup<AtlasText>;
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<AtlasText>();
var headers:FlxTypedGroup<AtlasText> = new FlxTypedGroup<AtlasText>();
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<Int> = null;
var buttonUsedToEnterPrompt:Null<Int> = 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);
}
}