using System.Collections.Generic; using System; using System.Text; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.EventSystems; using UnityEngine.UI; using NeonTea.Quakeball.Players; using TMPro; namespace NeonTea.Quakeball.Interface { public class Terminal : MonoBehaviour { public RectTransform TerminalPanel; public TMP_InputField InputField; public TMP_Text TextField; public LocalPlayer Player; [Tooltip("Used to optimize the amount of text rendered. 0 for no optimization.")] public float LinesVisibleAtOnce; public Scrollbar TerminalScrollbar; private InputAction ToggleTerminalAction; private InputAction SubmitTerminal; private float DesiredTerminalPos; private bool IsOpen = false; private Dictionary> Commands = new Dictionary>(); private Dictionary Helps = new Dictionary(); private List Messages = new List(); private List PreviousRuns = new List(); private int CurrentScroll = -1; private bool JustScrolled = false; public static string INFO_COLOR = "#FE8"; public static string ERROR_COLOR = "#F33"; public static Terminal Singleton => GameObject.FindGameObjectWithTag("Terminal").GetComponent(); private void Start() { ToggleTerminalAction = new InputAction("Toggle Terminal", binding: "/backquote"); ToggleTerminalAction.Enable(); ToggleTerminalAction.performed += ToggleTerminal; SubmitTerminal = new InputAction("Terminal Submit", binding: "/enter"); SubmitTerminal.Enable(); SubmitTerminal.performed += Submit; SubmitTerminal = new InputAction("Terminal ScrollUp", binding: "/uparrow"); SubmitTerminal.Enable(); SubmitTerminal.performed += _ => { Scroll(1); }; SubmitTerminal = new InputAction("Terminal ScrollDown", binding: "/downarrow"); SubmitTerminal.Enable(); SubmitTerminal.performed += _ => { Scroll(-1); }; DesiredTerminalPos = (TerminalPanel.rect.height / 2) * (IsOpen ? -1 : 1); InputField.restoreOriginalTextOnEscape = false; InputField.onValueChanged.AddListener(_ => { if (JustScrolled) { JustScrolled = false; return; } CurrentScroll = -1; }); RegisterCommand("help", args => { if (args.Length == 0) { foreach (string command in Commands.Keys) { string help = Helps.ContainsKey(command) ? Helps[command] : "No help info"; Println($"{command}: {help}"); } } foreach (string command in args) { if (Helps.ContainsKey(command)) { Println($"help {command}: {Helps[command]}"); } else { Println($"Help for command {command} not found."); } } return true; }, "help [command [...]] - Displays help information for the given commands"); Println($"Welcome to Quakeball!"); } public void ToggleTerminal(InputAction.CallbackContext context) { IsOpen = !IsOpen; DesiredTerminalPos = (TerminalPanel.rect.height / 2) * (IsOpen ? -1 : 1); if (IsOpen) { InputField.text = ""; } InputField.readOnly = true; if (Player != null) { Player.DisableInput = IsOpen; } } private void Update() { Vector3 pos = TerminalPanel.anchoredPosition; pos.y = Mathf.Lerp(pos.y, DesiredTerminalPos, Time.deltaTime * 10); TerminalPanel.anchoredPosition = pos; if (IsOpened() && EventSystem.current.currentSelectedGameObject != InputField) { InputField.Select(); InputField.ActivateInputField(); InputField.readOnly = false; } if (IsOpen) { if (LinesVisibleAtOnce == 0) { TextField.text = String.Join("\n", Messages); } else { float VisibleLinesCenterIndex = (1 - TerminalScrollbar.value) * Messages.Count; int StartIndex = Mathf.Max(0, (int)(VisibleLinesCenterIndex - LinesVisibleAtOnce / 2)); int EndIndex = Mathf.Min(Messages.Count - 1, (int)(VisibleLinesCenterIndex + LinesVisibleAtOnce / 2)); StringBuilder OptimizedText = new StringBuilder(); for (int i = 0; i < Messages.Count; i++) { if (i < StartIndex || i > EndIndex) { OptimizedText.AppendLine(); } else { OptimizedText.AppendLine(Messages[i]); } } TextField.text = OptimizedText.ToString(); } } } public bool RegisterCommand(string name, Func command) { if (Commands.ContainsKey(name)) { return false; } Commands.Add(name, command); return true; } public bool RegisterCommand(string name, Func command, string help) { if (RegisterCommand(name, command)) { Helps.Add(name, help); return true; } return false; } public void Run(string command) { string[] parts = command.Split(new char[] { ' ' }); string name = parts[0]; if (Commands.ContainsKey(name)) { Func func = Commands[name]; string[] args = new string[parts.Length - 1]; Array.Copy(parts, 1, args, 0, parts.Length - 1); int message = Println($"> {command}"); if (!func(args)) { EditPrintln(message, $"> {command}"); } } else { Println($"> {command}"); Println("No such command exists!"); } PreviousRuns.Insert(0, command); } public int Println(string message) { Messages.Add(message); return Messages.Count - 1; } public void EditPrintln(int idx, string message) { if (0 > idx || idx >= Messages.Count) { return; } Messages[idx] = message; } private void Scroll(int amount) { if (IsOpened() && PreviousRuns.Count > 0) { JustScrolled = true; CurrentScroll = Math.Min(Math.Max(0, CurrentScroll + amount), PreviousRuns.Count - 1); InputField.text = PreviousRuns[CurrentScroll]; } } private void Submit(InputAction.CallbackContext context) { if (IsOpened()) { Run(InputField.text); InputField.text = ""; } } private bool IsOpened() { return IsOpen && (TerminalPanel.anchoredPosition.y + TerminalPanel.rect.height / 2) < 10; } } }