310 lines
13 KiB
C#
310 lines
13 KiB
C#
using static Game.I18n;
|
||
using System.Runtime.InteropServices;
|
||
using System.Text;
|
||
|
||
namespace Game.UI {
|
||
public class TextDisplay {
|
||
private const string RGB_FG_SEQ_START = "\e[38;2;";
|
||
private const string RGB_BG_SEQ_START = "\e[48;2;";
|
||
private const string ESC_SEQ_START = "\e[";
|
||
private const string SETCURSOR_END = "H";
|
||
private const string GRAPHICS_SEQ_END = "m";
|
||
private const string HIDECURSOR_SEQ = "\e[?25l";
|
||
private const string SHOWCURSOR_SEQ = "\e[?25h";
|
||
|
||
private readonly char[] SIMPLE_BOX_CHARS = { '─', '│', '┌', '┐', '└', '┘' };
|
||
private readonly char[] DOUBLE_BOX_CHARS = { '═', '║', '╔', '╗', '╚', '╝' };
|
||
private readonly char[] FALLBACK_BOX_CHARS = { '-', '|', '+', '+', '+', '+' };
|
||
|
||
private int currentFgColor = 0xffffff, currentBgColor = 0;
|
||
private int cursorRow = 0, cursorCol = 0;
|
||
private int windowWidth = 0, windowHeight = 0;
|
||
|
||
public bool InitWindow() {
|
||
if (Console.IsOutputRedirected) {
|
||
Console.WriteLine(GetMessage(Message.OUTPUT_IS_NOT_TERMINAL));
|
||
return false;
|
||
}
|
||
Console.InputEncoding = Encoding.UTF8;
|
||
Console.OutputEncoding = Encoding.UTF8;
|
||
|
||
IntPtr consoleInHandle = Win32Imports.GetStdHandle(Win32Imports.STD_INPUT_HANDLE);
|
||
IntPtr consoleOutHandle = Win32Imports.GetStdHandle(Win32Imports.STD_OUTPUT_HANDLE);
|
||
|
||
uint inConsoleMode, outConsoleMode;
|
||
if (!Win32Imports.GetConsoleMode(consoleInHandle, out inConsoleMode)) {
|
||
Console.WriteLine(GetMessage(Message.ERROR_GETCONSOLEMODE));
|
||
return false;
|
||
}
|
||
if (!Win32Imports.GetConsoleMode(consoleOutHandle, out outConsoleMode)) {
|
||
Console.WriteLine(GetMessage(Message.ERROR_GETCONSOLEMODE));
|
||
return false;
|
||
}
|
||
inConsoleMode |= Win32Imports.ENABLE_VIRTUAL_TERMINAL_INPUT;
|
||
//inConsoleMode &= ~Win32Imports.ENABLE_LINE_INPUT;
|
||
outConsoleMode |= Win32Imports.ENABLE_VIRTUAL_TERMINAL_PROCESSING;
|
||
if (!Win32Imports.SetConsoleMode(consoleInHandle, inConsoleMode)) {
|
||
Console.WriteLine(GetMessage(Message.ERROR_SETCONSOLEMODE));
|
||
return false;
|
||
}
|
||
if (!Win32Imports.SetConsoleMode(consoleOutHandle, outConsoleMode)) {
|
||
Console.WriteLine(GetMessage(Message.ERROR_SETCONSOLEMODE));
|
||
return false;
|
||
}
|
||
SetColors(currentFgColor, currentBgColor);
|
||
HideCursor();
|
||
SetCursor(cursorRow, cursorCol);
|
||
windowWidth = Console.BufferWidth;
|
||
windowHeight = Console.BufferHeight;
|
||
return true;
|
||
}
|
||
|
||
public void ResetWindow() {
|
||
SetColors(0xffffff, 0);
|
||
SetCursor(1, 1);
|
||
ShowCursor();
|
||
}
|
||
|
||
public void PrintSimpleWelcome() {
|
||
WriteColored("Welcome to КТО ХОЧЕТ СТАТЬ СЕКУНДОМЕРОМ", 0x5e61ed);
|
||
}
|
||
|
||
public void ShowSplash() {
|
||
ClearWithColor(0x5e61ed);
|
||
|
||
DrawTitle(windowHeight / 2 + 1 - 3, 0x5e61ed);
|
||
DrawCopyright(0x5e61ed);
|
||
}
|
||
|
||
public void ShowFatalError(string text) {
|
||
ClearWithColor(0xfc5d2d);
|
||
DrawTextBox(HAlignment.Center, VAlignment.Center, BoxType.Double, windowHeight / 2 + 1, windowWidth / 2 + 1, text, 0xffffff, 0xffffff, 0xfc5d2d, windowWidth / 2);
|
||
}
|
||
|
||
public void ShowMainGameScreenBase() {
|
||
ClearWithColor(0x5e61ed);
|
||
DrawTitle(2, 0x5e61ed);
|
||
DrawCopyright(0x5e61ed);
|
||
}
|
||
|
||
public void DrawRules() {
|
||
DrawTextBox(HAlignment.Center, VAlignment.Center, BoxType.Simple, windowHeight / 2 + 1, windowWidth / 2 + 1, GetMessage(Message.RULES), 0x00fcdb, 0x00fcdb, 0x000000, windowWidth * 3 / 4);
|
||
}
|
||
|
||
void DrawTitle(int y, int bgColor) {
|
||
string text1 = GetMessage(Message.TITLE_LINE1)!;
|
||
string text2 = GetMessage(Message.TITLE_LINE2)!;
|
||
SetColors(0xffffff, 0);
|
||
|
||
int text1Start = (windowWidth - text1.Length) / 2 + 1;
|
||
SetCursor(y, text1Start - 6);
|
||
for (int i = 0; i < 6; ++i) {
|
||
WriteColored(" ", -1, ColorBlend(0, bgColor, (float)i / 6));
|
||
}
|
||
WriteColored(text1, 0xffffff, 0);
|
||
for (int i = 6; i >= 0; --i) {
|
||
WriteColored(" ", -1, ColorBlend(0, bgColor, (float)i / 6));
|
||
}
|
||
|
||
int text2Start = (windowWidth - text2.Length) / 2 + 1;
|
||
SetCursor(y + 2, text2Start - 6);
|
||
for (int i = 0; i < 6; ++i) {
|
||
WriteColored(" ", -1, ColorBlend(0, bgColor, (float)i / 6));
|
||
}
|
||
WriteColored(text2, 0xffffff, 0);
|
||
for (int i = 6; i >= 0; --i) {
|
||
WriteColored(" ", -1, ColorBlend(0, bgColor, (float)i / 6));
|
||
}
|
||
}
|
||
|
||
void DrawCopyright(int bgColor) {
|
||
string bottomText = "(c) subvia, 2025";
|
||
SetCursor(windowHeight, (windowWidth - bottomText.Length) / 2 + 1);
|
||
WriteColored(bottomText, 0x4d4dc0, bgColor);
|
||
}
|
||
|
||
void DrawTextBox(HAlignment hAlign, VAlignment vAlign, BoxType boxType, int y, int x, string text, int borderColor, int textColor, int bgColor = -1, int preferredWidth = -1) {
|
||
int maxWidth = windowWidth;
|
||
switch (hAlign) {
|
||
case HAlignment.Left:
|
||
maxWidth = windowWidth - x + 1;
|
||
break;
|
||
case HAlignment.Center:
|
||
maxWidth = Math.Min(x - 1, windowWidth - x + 1) * 2;
|
||
break;
|
||
case HAlignment.Right:
|
||
maxWidth = x - 1;
|
||
break;
|
||
}
|
||
int maxHeight = windowHeight;
|
||
switch (vAlign) {
|
||
case VAlignment.Top:
|
||
maxHeight = windowHeight - y + 1;
|
||
break;
|
||
case VAlignment.Center:
|
||
maxHeight = Math.Min(y - 1, windowHeight - y + 1) * 2;
|
||
break;
|
||
case VAlignment.Bottom:
|
||
maxHeight = y - 1;
|
||
break;
|
||
}
|
||
string[] lines = WordWrap(text, (preferredWidth == -1 ? maxWidth : preferredWidth) - 4, out bool overflow);
|
||
bool reflowed = false;
|
||
if (preferredWidth != -1 && lines.Length > maxHeight - 4) {
|
||
reflowed = true;
|
||
lines = WordWrap(text, maxWidth - 4, out overflow);
|
||
}
|
||
int actualWidth = Math.Min(preferredWidth == -1 ? maxWidth : (reflowed ? maxWidth : preferredWidth), lines.Max(s => s.Length) + 4);
|
||
int actualHeight = Math.Min(maxHeight, lines.Length + 4);
|
||
|
||
int left = hAlign == HAlignment.Left ? x : (hAlign == HAlignment.Right ? x - actualWidth + 1 : x - (actualWidth - 1) / 2);
|
||
int top = vAlign == VAlignment.Top ? y : (vAlign == VAlignment.Bottom ? y - actualHeight + 1 : y - (actualHeight - 1) / 2);
|
||
DrawBox(boxType, top, left, actualHeight, actualWidth, borderColor, bgColor);
|
||
SetColors(textColor, -1);
|
||
for (int i = 0; i < actualHeight - 4; ++i) {
|
||
SetCursor(top + 2 + i, left + 2);
|
||
WriteColored(lines[i], textColor, bgColor);
|
||
}
|
||
}
|
||
|
||
public void DrawBox(BoxType boxType, int y, int x, int h, int w, int borderColor, int bgColor = -1) {
|
||
char[] boxChars = boxType == BoxType.Simple ? SIMPLE_BOX_CHARS : (boxType == BoxType.Double ? DOUBLE_BOX_CHARS : FALLBACK_BOX_CHARS);
|
||
SetColors(borderColor, bgColor);
|
||
SetCursor(y, x);
|
||
StringBuilder output = new StringBuilder();
|
||
output.Append(boxChars[2]);
|
||
output.Append(boxChars[0], w - 2);
|
||
output.Append(boxChars[3]);
|
||
WriteColored(output.ToString(), borderColor, bgColor);
|
||
for (int i = 0; i < h - 2; ++i) {
|
||
output.Clear();
|
||
SetCursor(y + 1 + i, x);
|
||
output.Append(boxChars[1]);
|
||
output.Append(' ', w - 2);
|
||
output.Append(boxChars[1]);
|
||
WriteColored(output.ToString(), borderColor, bgColor);
|
||
}
|
||
SetCursor(y + h - 1, x);
|
||
output.Clear();
|
||
output.Append(boxChars[4]);
|
||
output.Append(boxChars[0], w - 2);
|
||
output.Append(boxChars[5]);
|
||
WriteColored(output.ToString(), borderColor, bgColor);
|
||
}
|
||
|
||
public void WriteColored(string s, int color, int bgColor = -1) {
|
||
if (color == -1) color = currentFgColor;
|
||
(int r, int g, int b) = ((color >> 16) & 255, (color >> 8) & 255, color & 255);
|
||
currentFgColor = color;
|
||
if (bgColor == -1) {
|
||
Console.Write(RGB_FG_SEQ_START + r + ';' + g + ';' + b + GRAPHICS_SEQ_END + s);
|
||
} else {
|
||
(int br, int bg, int bb) = ((bgColor >> 16) & 255, (bgColor >> 8) & 255, bgColor & 255);
|
||
Console.Write(RGB_FG_SEQ_START + r + ';' + g + ';' + b + GRAPHICS_SEQ_END + RGB_BG_SEQ_START + br + ';' + bg + ';' + bb + GRAPHICS_SEQ_END + s);
|
||
currentBgColor = bgColor;
|
||
}
|
||
}
|
||
|
||
public void SetColors(int fg, int bg) {
|
||
StringBuilder command = new StringBuilder();
|
||
if (fg != -1) {
|
||
(int r, int g, int b) = ((fg >> 16) & 255, (fg >> 8) & 255, fg & 255);
|
||
command.Append(RGB_FG_SEQ_START + r + ';' + g + ';' + b + GRAPHICS_SEQ_END);
|
||
currentFgColor = fg;
|
||
}
|
||
if (bg != -1) {
|
||
(int r, int g, int b) = ((bg >> 16) & 255, (bg >> 8) & 255, bg & 255);
|
||
command.Append(RGB_BG_SEQ_START + r + ';' + g + ';' + b + GRAPHICS_SEQ_END);
|
||
currentBgColor = bg;
|
||
}
|
||
Console.Write(command.ToString());
|
||
}
|
||
|
||
public void ClearWithColor(int color) {
|
||
(int r, int g, int b) = ((color >> 16) & 255, (color >> 8) & 255, color & 255);
|
||
SetColors(-1, color);
|
||
Console.Write("\e[2J");
|
||
}
|
||
|
||
public void SetCursor(int row, int column) {
|
||
if (cursorRow == row && cursorCol == column) return;
|
||
Console.Write(ESC_SEQ_START + row + ';' + column + SETCURSOR_END);
|
||
cursorRow = row;
|
||
cursorCol = column;
|
||
}
|
||
|
||
public void HideCursor() {
|
||
Console.Write(HIDECURSOR_SEQ);
|
||
}
|
||
|
||
public void ShowCursor() {
|
||
Console.Write(SHOWCURSOR_SEQ);
|
||
}
|
||
|
||
public int ColorBlend(int fg, int bg, float alpha) {
|
||
//return (int)(fg * alpha + bg * (1f - alpha));
|
||
return ((int)(((fg >> 16) & 255) * alpha + ((bg >> 16) & 255) * (1 - alpha)) << 16) + ((int)(((fg >> 8) & 255) * alpha + ((bg >> 8) & 255) * (1 - alpha)) << 8) + (int)((fg & 255) * alpha + (bg & 255) * (1 - alpha));
|
||
}
|
||
|
||
public string[] WordWrap(string text, int width, out bool overflow) {
|
||
overflow = false;
|
||
List<string> lines = new List<string>();
|
||
StringBuilder line = new StringBuilder(text.Length / width * 4 / 3);
|
||
int remaining = width;
|
||
foreach (string word in text.Split(new char[] {' ', '\n'}, StringSplitOptions.RemoveEmptyEntries)) {
|
||
if (word.Length <= remaining) {
|
||
if (line.Length == 0 || line[^1] != ' ') {
|
||
line.Append(' ');
|
||
remaining -= 1;
|
||
}
|
||
line.Append(word);
|
||
remaining -= word.Length;
|
||
} else {
|
||
lines.Add(line.ToString());
|
||
line.Clear();
|
||
line.Append(word);
|
||
remaining = width - word.Length;
|
||
if (remaining < 0) {
|
||
overflow = true;
|
||
line.Remove(width + 1, line.Length - (width + 1));
|
||
lines.Add(line.ToString());
|
||
line.Clear();
|
||
}
|
||
}
|
||
}
|
||
if (line.Length > 0)
|
||
lines.Add(line.ToString());
|
||
return lines.ToArray();
|
||
}
|
||
|
||
public enum HAlignment {
|
||
Left, Center, Right
|
||
}
|
||
|
||
public enum VAlignment {
|
||
Top, Center, Bottom
|
||
}
|
||
|
||
public enum BoxType {
|
||
Simple,
|
||
Double,
|
||
Fallback,
|
||
}
|
||
}
|
||
|
||
static class Win32Imports
|
||
{
|
||
public const int STD_INPUT_HANDLE = -10;
|
||
public const int STD_OUTPUT_HANDLE = -11;
|
||
public const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4;
|
||
public const uint ENABLE_LINE_INPUT = 2;
|
||
public const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 512;
|
||
|
||
[DllImport("kernel32.dll", SetLastError = true)]
|
||
public static extern IntPtr GetStdHandle(int stdHandle);
|
||
[DllImport("kernel32.dll")]
|
||
public static extern bool SetConsoleMode(IntPtr consoleHandle, uint mode);
|
||
[DllImport("kernel32.dll")]
|
||
public static extern bool GetConsoleMode(IntPtr consoleHandle, out uint mode);
|
||
}
|
||
} |