Files
MSCO/LobbyUI.cs
T

615 lines
30 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using UnityEngine;
namespace MSCO
{
/// <summary>
/// Multiplayer-Launcher im Hauptmenü (dunkles Design mit Seitenleiste, Statusleiste
/// und Minimieren). Seiten: P2P (Spieler hostet selbst) und Server Browser (Platzhalter,
/// dort kommt später auch Direct Connect zu dedizierten Servern hin).
/// </summary>
public class LobbyUI
{
enum Page { P2P, ServerBrowser }
const float Width = 760f;
const float Height = 460f;
const float SidebarWidth = 190f;
const float StatusHeight = 36f;
static readonly Color Background = Hex(0x17, 0x17, 0x1b);
static readonly Color Sidebar = Hex(0x0f, 0x0f, 0x12);
static readonly Color Field = Hex(0x24, 0x24, 0x2a);
static readonly Color FieldFocus = Hex(0x2e, 0x2e, 0x36);
static readonly Color Accent = Hex(0xf2, 0x6b, 0x1d);
static readonly Color AccentHover = Hex(0xff, 0x82, 0x3a);
static readonly Color Secondary = Hex(0x2c, 0x2c, 0x33);
static readonly Color SecondaryHover = Hex(0x3a, 0x3a, 0x43);
static readonly Color Muted = Hex(0x8e, 0x8e, 0x99);
static readonly Color Online = Hex(0x3d, 0xd6, 0x8c);
static readonly Color Pending = Hex(0xf2, 0xc2, 0x1d);
Page page = Page.P2P;
bool minimized;
Vector2 playerScroll;
Vector2 serverScroll;
bool browserLoaded;
string selectedServer; // "adresse:port" des geöffneten Server-Profils
string searchInput = "";
bool showSteam = true, showNonSteam = true, hideFull;
string nameInput;
string directInput;
const string PrefDirect = "MSCO_LastServer";
bool stylesReady;
Texture2D texBackground, texSidebar, texAccent, texOnline, texPending, texMuted, texRow;
List<ServerListEntry> shownAll, shownServers; // Stand des letzten Layout-Durchgangs
string layoutError;
GUIStyle windowBox, sidebarBox, rowBox, accentBox, pillStyle, pillMutedStyle, chipOnStyle, chipOffStyle;
GUIStyle logoStyle, taglineStyle, navStyle, navActiveStyle, titleStyle, textStyle, captionStyle,
fieldStyle, primaryStyle, secondaryStyle, statusStyle, tabStyle, headerCellStyle,
rowNameStyle, rowTagStyle, badgeStyle, toggleStyle, placeholderStyle;
/// <summary>Fläche, die das Fenster gerade belegt (GUI-Koordinaten, oben = 0).</summary>
public Rect ScreenRect { get; private set; }
/// <summary>Fenster offen (nicht minimiert) - dann ist das Spielmenü dahinter gesperrt.</summary>
public bool IsOpen { get { return !minimized; } }
/// <summary>Ist die Maus gerade über dem Fenster bzw. der eingeklappten Leiste?</summary>
public bool ContainsMouse()
{
Vector3 m = Input.mousePosition;
return ScreenRect.Contains(new Vector2(m.x, Screen.height - m.y));
}
public void Draw(MultiplayerManager mp)
{
EnsureStyles();
bool pendingJoin = mp.PendingJoinHost != null && !mp.IsInSession;
bool versionWarning = mp.PendingVersionWarning != null && !mp.IsInSession;
if (pendingJoin || versionWarning) minimized = false; // JOIN-Link / Warnung: Fenster zeigen
if (minimized) { DrawMinimized(mp); return; }
var window = new Rect((Screen.width - Width) / 2f, (Screen.height - Height) / 2f, Width, Height);
ScreenRect = window;
GUI.Box(window, GUIContent.none, windowBox);
DrawSidebar(new Rect(window.x, window.y, SidebarWidth, Height - StatusHeight));
var content = new Rect(window.x + SidebarWidth + 32, window.y + 28,
Width - SidebarWidth - 64, Height - StatusHeight - 52);
GUILayout.BeginArea(content);
if (versionWarning) DrawVersionWarning(mp);
else if (pendingJoin) DrawJoinConfirm(mp);
else if (mp.IsInSession) DrawSession(mp);
else if (page == Page.P2P) DrawP2P(mp);
else DrawServerBrowser(mp);
GUILayout.EndArea();
DrawStatusBar(mp, new Rect(window.x, window.yMax - StatusHeight, Width, StatusHeight));
}
// ---------------------------------------------------------------- Bereiche
void DrawSidebar(Rect r)
{
GUI.Box(r, GUIContent.none, sidebarBox);
// Logo + Schriftzug
Texture2D logo = Branding.LogoTexture;
float textX = r.x + 22;
if (logo != null)
{
GUI.DrawTexture(new Rect(r.x + 20, r.y + 20, 44, 44), logo);
textX = r.x + 72;
}
GUI.Label(new Rect(textX, r.y + 24, r.xMax - textX, 40), "MSCO", logoStyle);
GUI.Label(new Rect(r.x + 22, r.y + 70, r.width - 30, 20), "MY SUMMER CAR ONLINE", taglineStyle);
float y = r.y + 110;
if (NavButton(new Rect(r.x, y, r.width, 42), "P2P", page == Page.P2P)) page = Page.P2P;
y += 44;
if (NavButton(new Rect(r.x, y, r.width, 42), "Server Browser", page == Page.ServerBrowser)) page = Page.ServerBrowser;
GUI.Label(new Rect(r.x + 24, r.yMax - 30, r.width - 48, 20), "v" + MSCO.ModVersion, taglineStyle);
}
bool NavButton(Rect r, string text, bool active)
{
if (active) GUI.Box(new Rect(r.x + 4, r.y + 8, 4, r.height - 16), GUIContent.none, accentBox); // Akzentbalken links
return GUI.Button(r, text, active ? navActiveStyle : navStyle);
}
void DrawP2P(MultiplayerManager mp)
{
GUILayout.Label("P2P", titleStyle);
GUILayout.Label("Host a game from your own save and invite up to " + (MultiplayerManager.MaxPlayers - 1) +
" friends through Steam.", textStyle);
GUILayout.Space(18);
if (mp.SteamAvailable)
{
if (GUILayout.Button("HOST GAME", primaryStyle, GUILayout.Height(46), GUILayout.Width(250)))
mp.HostSteam();
GUILayout.Space(14);
GUILayout.Label("To join a friend: accept their invite in Steam, or right-click them in your " +
"Steam friends list and choose \"Join Game\".", textStyle);
}
else
{
GUILayout.Label("Steam is not running - start the game through Steam to host or join.", textStyle);
}
}
void DrawServerBrowser(MultiplayerManager mp)
{
ServerBrowser browser = mp.Browser;
// Beim ersten Öffnen laden, danach automatisch alle 15 s (Spielerzahl, Ping, neue Server)
if (!browserLoaded || (!browser.Loading && Time.realtimeSinceStartup - browser.LastRefresh > ServerBrowser.AutoRefreshSeconds))
{
browserLoaded = true;
browser.Refresh(mp);
}
// Server, die auf die direkte Abfrage nicht antworten (z.B. gerade geschlossen), gar nicht zeigen
// Unity-GUI läuft in mehreren Durchgängen (Layout, Klick, Zeichnen). Die Liste nur im Layout-Durchgang
// übernehmen - ändert sie sich dazwischen (Aktualisierung im Hintergrund), bricht das Layout sonst ab
if (shownAll == null || Event.current.type == EventType.Layout)
shownAll = browser.Servers.FindAll(s => !s.Offline);
List<ServerListEntry> all = shownAll;
// Server-Profil (Klick auf einen Server)
if (selectedServer != null)
{
ServerListEntry selected = all.Find(s => s.Address + ":" + s.Port == selectedServer);
DrawServerProfile(mp, selected);
return;
}
GUILayout.BeginHorizontal();
// Titel darf nicht vom REFRESH-Button zusammengedrückt werden (sonst Zeilenumbruch)
GUILayout.Label("Server Browser", titleStyle, GUILayout.ExpandWidth(true));
if (GUILayout.Button(browser.Loading ? "LOADING..." : "REFRESH", tabStyle, GUILayout.Height(28), GUILayout.Width(110)))
browser.Refresh(mp);
GUILayout.EndHorizontal();
// Suche + Filter
GUILayout.BeginHorizontal();
searchInput = GUILayout.TextField(searchInput, 40, fieldStyle, GUILayout.Height(26), GUILayout.Width(170));
if (string.IsNullOrEmpty(searchInput))
{
Rect field = GUILayoutUtility.GetLastRect();
GUI.Label(new Rect(field.x + 10, field.y, field.width, field.height), "Search...", placeholderStyle);
}
GUILayout.Space(12);
showSteam = Chip(showSteam, "STEAM");
GUILayout.Space(6);
showNonSteam = Chip(showNonSteam, "NON-STEAM");
GUILayout.Space(6);
hideFull = Chip(hideFull, "HIDE FULL");
GUILayout.EndHorizontal();
GUILayout.Space(6);
string search = (searchInput ?? "").Trim().ToLower();
if (shownServers == null || Event.current.type == EventType.Layout)
shownServers = all.FindAll(s =>
(s.RequireSteam ? showSteam : showNonSteam) &&
(!hideFull || s.Players < s.MaxPlayers) &&
(search.Length == 0 || (s.Name ?? "").ToLower().Contains(search) || (s.Owner ?? "").ToLower().Contains(search)));
List<ServerListEntry> servers = shownServers;
string shownError = Event.current.type == EventType.Layout ? (layoutError = browser.Error) : layoutError;
// Tabellenkopf
Rect header = GUILayoutUtility.GetRect(10, 26, GUILayout.ExpandWidth(true));
GUI.Box(header, GUIContent.none, rowBox);
GUI.Label(new Rect(header.x + 12, header.y, header.width * 0.48f, header.height), "SERVER NAME", headerCellStyle);
GUI.Label(new Rect(header.x + header.width * 0.5f, header.y, 70, header.height), "PLAYERS", headerCellStyle);
GUI.Label(new Rect(header.x + header.width * 0.62f, header.y, 70, header.height), "TYPE", headerCellStyle);
GUI.Label(new Rect(header.x + header.width * 0.76f, header.y, 50, header.height), "PING", headerCellStyle);
serverScroll = GUILayout.BeginScrollView(serverScroll, GUILayout.Height(140));
if (shownError != null) GUILayout.Label(shownError, textStyle);
if (servers.Count == 0)
GUILayout.Label(browser.Loading ? "Loading servers..." : all.Count > 0 ? "No servers match your filters." : "No servers online right now.", textStyle);
foreach (ServerListEntry s in servers)
{
Rect row = GUILayoutUtility.GetRect(10, 34, GUILayout.ExpandWidth(true));
GUI.Box(new Rect(row.x, row.y + 2, row.width, row.height - 4), GUIContent.none, rowBox);
// Klick auf die Zeile öffnet das Server-Profil (dort wird verbunden)
if (GUI.Button(new Rect(row.x, row.y, row.width, row.height), GUIContent.none, GUIStyle.none))
selectedServer = s.Address + ":" + s.Port;
GUI.DrawTexture(new Rect(row.x + 10, row.y + 13, 8, 8), s.Ping < 0 ? texPending : texOnline);
float nameX = row.x + 26;
if (s.IsLocal)
{
// Server auf diesem PC (localhost)
nameX += Pill(nameX, row, "LOCAL", pillStyle);
}
if (!string.IsNullOrEmpty(s.Version) && s.Version != MSCO.ModVersion)
{
nameX += Pill(nameX, row, "v" + s.Version, pillStyle); // abweichende Version
}
GUI.Label(new Rect(nameX, row.y, row.x + row.width * 0.49f - nameX, row.height), s.Name, rowNameStyle);
GUI.Label(new Rect(row.x + row.width * 0.5f, row.y, 70, row.height), s.Players + "/" + s.MaxPlayers, rowNameStyle);
Pill(row.x + row.width * 0.62f, row, s.RequireSteam ? "STEAM" : "NO STEAM", s.RequireSteam ? pillStyle : pillMutedStyle);
GUI.Label(new Rect(row.x + row.width * 0.76f, row.y, 50, row.height), s.Ping < 0 ? "..." : s.Ping + " ms", rowNameStyle);
GUI.Label(new Rect(row.xMax - 30, row.y, 20, row.height), "›", headerCellStyle); // Hinweis: Profil öffnen
}
GUILayout.EndScrollView();
GUILayout.FlexibleSpace();
GUILayout.BeginHorizontal();
if (!mp.SteamAvailable)
{
// Ohne Steam: eigener Name (nur für Non-Steam-Server)
GUILayout.BeginVertical(GUILayout.Width(150));
GUILayout.Label("YOUR NAME", captionStyle);
DrawNameField(mp, 150);
GUILayout.EndVertical();
GUILayout.Space(10);
}
GUILayout.BeginVertical();
GUILayout.Label("DIRECT CONNECT", captionStyle);
GUILayout.BeginHorizontal();
directInput = GUILayout.TextField(directInput, fieldStyle, GUILayout.Height(34));
GUILayout.Space(10);
if (GUILayout.Button("CONNECT", primaryStyle, GUILayout.Height(34), GUILayout.Width(110)))
{
string host;
int port;
if (ServerBrowser.TryParseAddress(directInput, out host, out port))
{
PlayerPrefs.SetString(PrefDirect, directInput.Trim());
mp.JoinServer(host, port);
}
}
GUILayout.EndHorizontal();
GUILayout.EndVertical();
GUILayout.EndHorizontal();
}
static bool CanJoin(MultiplayerManager mp, ServerListEntry s)
{
return s.Players < s.MaxPlayers && (!s.RequireSteam || mp.SteamAvailable);
}
void DrawNameField(MultiplayerManager mp, float width)
{
if (nameInput == null) nameInput = mp.PlayerName;
string edited = GUILayout.TextField(nameInput, 24, fieldStyle, GUILayout.Height(34), GUILayout.Width(width));
if (edited != nameInput)
{
nameInput = edited;
if (edited.Trim().Length > 0) mp.PlayerName = edited.Trim();
}
}
/// <summary>Server-Profil: wem der Server gehört, Version, Spieler, Laufzeit ...</summary>
void DrawServerProfile(MultiplayerManager mp, ServerListEntry s)
{
if (GUILayout.Button("◂ BACK", tabStyle, GUILayout.Height(26), GUILayout.Width(90)))
{
selectedServer = null;
return;
}
GUILayout.Space(6);
if (s == null)
{
GUILayout.Label("Server offline", titleStyle);
GUILayout.Label("This server does not respond anymore.", textStyle);
return;
}
GUILayout.Label(s.Name, titleStyle);
GUILayout.BeginHorizontal();
GUILayout.Label(s.RequireSteam ? "STEAM" : "NON-STEAM", badgeStyle, GUILayout.ExpandWidth(false));
if (s.IsLocal) { GUILayout.Space(10); GUILayout.Label("LOCAL", badgeStyle, GUILayout.ExpandWidth(false)); }
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
if (!string.IsNullOrEmpty(s.Description)) GUILayout.Label(s.Description, textStyle);
GUILayout.Space(10);
bool versionOk = s.Version == MSCO.ModVersion;
GUILayout.BeginHorizontal();
GUILayout.BeginVertical(GUILayout.Width(250));
InfoRow("HOSTED BY", string.IsNullOrEmpty(s.Owner) ? "unknown" : s.Owner, rowNameStyle);
InfoRow("VERSION", "v" + s.Version + (versionOk ? "" : " (you: v" + MSCO.ModVersion + ")"), versionOk ? rowNameStyle : badgeStyle);
InfoRow("STEAM", s.RequireSteam ? "Required" : "Not required", rowNameStyle);
InfoRow("ONLINE FOR", s.UptimeSeconds >= 0 ? FormatUptime(s.UptimeSeconds) : "-", rowNameStyle);
GUILayout.EndVertical();
GUILayout.BeginVertical();
InfoRow("PLAYERS", s.Players + " / " + s.MaxPlayers, rowNameStyle);
InfoRow("PING", s.Ping < 0 ? "-" : s.Ping + " ms", rowNameStyle);
InfoRow("ADDRESS", s.Address + ":" + s.Port, rowNameStyle);
GUILayout.EndVertical();
GUILayout.EndHorizontal();
// Spielerliste
if (s.PlayerNames != null && s.PlayerNames.Count > 0)
{
GUILayout.Space(4);
GUILayout.Label("ONLINE", captionStyle);
GUILayout.Label(string.Join(", ", s.PlayerNames.ToArray()), textStyle);
}
GUILayout.FlexibleSpace();
GUILayout.BeginHorizontal();
GUI.enabled = CanJoin(mp, s);
if (GUILayout.Button(s.Players >= s.MaxPlayers ? "SERVER FULL" : "JOIN SERVER", primaryStyle, GUILayout.Height(42), GUILayout.Width(190)))
mp.JoinServer(s.Address, s.Port, s.Name, s.MaxPlayers);
GUI.enabled = true;
GUILayout.Space(12);
if (s.RequireSteam && !mp.SteamAvailable)
GUILayout.Label("This server requires Steam.", textStyle);
else if (!mp.SteamAvailable)
{
GUILayout.Label("NAME", captionStyle, GUILayout.Width(42));
DrawNameField(mp, 150);
}
GUILayout.EndHorizontal();
}
/// <summary>Kleines abgerundetes Abzeichen, so breit wie sein Text. Gibt die belegte Breite zurück.</summary>
static float Pill(float x, Rect row, string text, GUIStyle style)
{
Vector2 size = style.CalcSize(new GUIContent(text));
GUI.Label(new Rect(x, row.y + (row.height - 18) / 2f, size.x, 18), text, style);
return size.x + 8;
}
/// <summary>Filter-Chip (an = orange umrandet), ersetzt die Unity-Häkchen.</summary>
bool Chip(bool on, string text)
{
if (GUILayout.Button(text, on ? chipOnStyle : chipOffStyle, GUILayout.Height(26), GUILayout.ExpandWidth(false))) on = !on;
return on;
}
void InfoRow(string caption, string value, GUIStyle valueStyle)
{
GUILayout.Label(caption, captionStyle);
GUILayout.Label(value, valueStyle);
GUILayout.Space(2);
}
static string FormatUptime(int seconds)
{
int days = seconds / 86400, hours = seconds / 3600 % 24, minutes = seconds / 60 % 60;
if (days > 0) return days + "d " + hours + "h";
if (hours > 0) return hours + "h " + minutes + "m";
return Mathf.Max(1, minutes) + " min";
}
/// <summary>Server hat eine andere Version: warnen, Verbinden nur auf Wunsch.</summary>
void DrawVersionWarning(MultiplayerManager mp)
{
MultiplayerManager.VersionWarning w = mp.PendingVersionWarning;
GUILayout.Label("Version mismatch", titleStyle);
GUILayout.Label("This server runs a different MSCO version than you. You can still join, but things may not " +
"sync correctly (players, items, cars). For the best experience use the same version.", textStyle);
GUILayout.Space(16);
GUILayout.Label("SERVER", captionStyle);
GUILayout.Label(w.Name + " (" + w.Host + ":" + w.Port + ")", rowNameStyle);
GUILayout.Space(8);
GUILayout.Label("VERSIONS", captionStyle);
GUILayout.Label("Server v" + w.ServerVersion + " - You v" + MSCO.ModVersion, rowTagStyle);
GUILayout.Space(22);
GUILayout.BeginHorizontal();
if (GUILayout.Button("JOIN ANYWAY", primaryStyle, GUILayout.Height(42), GUILayout.Width(170)))
mp.ConfirmVersionWarning();
GUILayout.Space(10);
if (GUILayout.Button("CANCEL", secondaryStyle, GUILayout.Height(42), GUILayout.Width(170)))
mp.CancelVersionWarning();
GUILayout.EndHorizontal();
}
/// <summary>Nachfrage nach einem JOIN-Link der Website (keine Webseite soll einen ungefragt verbinden).</summary>
void DrawJoinConfirm(MultiplayerManager mp)
{
GUILayout.Label("Join server?", titleStyle);
GUILayout.Label("You opened a join link from the server list.", textStyle);
GUILayout.Space(18);
GUILayout.Label("SERVER ADDRESS", captionStyle);
GUILayout.Label(mp.PendingJoinHost + ":" + mp.PendingJoinPort, rowNameStyle);
GUILayout.Space(24);
if (!mp.SteamAvailable)
GUILayout.Label("Steam is required to join servers - start the game through Steam.", textStyle);
GUILayout.BeginHorizontal();
GUI.enabled = mp.SteamAvailable;
if (GUILayout.Button("JOIN", primaryStyle, GUILayout.Height(42), GUILayout.Width(170)))
mp.ConfirmPendingJoin();
GUI.enabled = true;
GUILayout.Space(10);
if (GUILayout.Button("CANCEL", secondaryStyle, GUILayout.Height(42), GUILayout.Width(170)))
mp.CancelPendingJoin();
GUILayout.EndHorizontal();
}
void DrawSession(MultiplayerManager mp)
{
GUILayout.Label(mp.IsHost ? "Hosting" : "Connected", titleStyle);
GUILayout.Label(mp.IsHost
? "Your game is open. Start when everyone has joined."
: "Waiting for the host to start the game.", textStyle);
GUILayout.Space(16);
GUILayout.Label("PLAYERS " + mp.Players.Count + "/" + MultiplayerManager.MaxPlayers, captionStyle);
playerScroll = GUILayout.BeginScrollView(playerScroll, GUILayout.Height(170));
foreach (PlayerInfo p in mp.Players)
{
Rect row = GUILayoutUtility.GetRect(10, 32, GUILayout.ExpandWidth(true));
GUI.Box(new Rect(row.x, row.y, row.width, row.height - 4), GUIContent.none, rowBox);
GUI.DrawTexture(new Rect(row.x + 12, row.y + 11, 8, 8), texOnline);
GUI.Label(new Rect(row.x + 30, row.y, row.width - 150, row.height - 2), p.Name, rowNameStyle);
string tag = p.Id == NetSession.HostId ? "HOST" : "";
if (p.IsLocal) tag = tag.Length > 0 ? tag + " · YOU" : "YOU";
GUI.Label(new Rect(row.xMax - 120, row.y, 110, row.height - 2), tag, rowTagStyle);
}
GUILayout.EndScrollView();
GUILayout.Space(14);
GUILayout.BeginHorizontal();
if (mp.IsHost)
{
if (GUILayout.Button("START GAME", primaryStyle, GUILayout.Height(42), GUILayout.Width(150)))
mp.StartHostGame();
GUILayout.Space(10);
if (mp.IsSteamSession && mp.Players.Count < MultiplayerManager.MaxPlayers &&
GUILayout.Button("INVITE FRIENDS", secondaryStyle, GUILayout.Height(42), GUILayout.Width(150)))
mp.InviteFriends();
GUILayout.Space(10);
}
if (GUILayout.Button("DISCONNECT", secondaryStyle, GUILayout.Height(42), GUILayout.Width(150)))
mp.Disconnect();
GUILayout.EndHorizontal();
}
void DrawStatusBar(MultiplayerManager mp, Rect r)
{
GUI.Box(r, GUIContent.none, sidebarBox);
Texture2D dot = mp.IsPeerReady ? texOnline : mp.IsInSession ? texPending : texMuted;
GUI.DrawTexture(new Rect(r.x + 18, r.y + (r.height - 8) / 2f, 8, 8), dot);
GUI.Label(new Rect(r.x + 36, r.y, r.width - 180, r.height), mp.Status, statusStyle);
if (GUI.Button(new Rect(r.xMax - 118, r.y + 6, 106, r.height - 12), "MINIMIZE", tabStyle))
minimized = true;
}
void DrawMinimized(MultiplayerManager mp)
{
var r = new Rect((Screen.width - 220) / 2f, 0, 220, 30);
ScreenRect = r;
GUI.Box(r, GUIContent.none, sidebarBox);
GUI.Box(new Rect(r.x + 10, r.yMax - 3, r.width - 20, 3), GUIContent.none, accentBox);
Texture2D dot = mp.IsPeerReady ? texOnline : mp.IsInSession ? texPending : texMuted;
Texture2D logo = Branding.LogoTexture;
if (logo != null) GUI.DrawTexture(new Rect(r.x + 8, r.y + 4, 22, 22), logo);
GUI.DrawTexture(new Rect(r.x + 36, r.y + 11, 8, 8), dot);
if (GUI.Button(new Rect(r.x + 48, r.y, r.width - 48, r.height), "MSCO MULTIPLAYER ▾", tabStyle))
minimized = false;
}
// ---------------------------------------------------------------- Hilfen
void EnsureStyles()
{
if (stylesReady && texBackground != null) return;
// PlayerPrefs erst hier lesen (nicht im Konstruktor erlaubt)
directInput = PlayerPrefs.GetString(PrefDirect, "");
texBackground = Solid(Background);
texSidebar = Solid(Sidebar);
texAccent = Solid(Accent);
texOnline = UiTheme.Rounded(Online, 4); // runde Status-Punkte
texPending = UiTheme.Rounded(Pending, 4);
texMuted = UiTheme.Rounded(Muted, 4);
texRow = Solid(Field);
windowBox = UiTheme.Panel9(Background, 10);
sidebarBox = UiTheme.Panel9(Sidebar, 10);
rowBox = UiTheme.Panel9(Field, UiTheme.Radius);
accentBox = UiTheme.Panel9(Accent, 1);
pillStyle = UiTheme.Panel9(new Color(Accent.r, Accent.g, Accent.b, 0.18f), 4);
pillStyle.normal.textColor = Accent;
pillStyle.fontSize = 10;
pillStyle.fontStyle = FontStyle.Bold;
pillStyle.alignment = TextAnchor.MiddleCenter;
pillStyle.padding = new RectOffset(7, 7, 0, 0);
pillStyle.wordWrap = false;
pillMutedStyle = new GUIStyle(pillStyle);
pillMutedStyle.normal.background = UiTheme.Rounded(new Color(1f, 1f, 1f, 0.08f), 4);
pillMutedStyle.normal.textColor = Muted;
chipOnStyle = Button(new Color(Accent.r, Accent.g, Accent.b, 0.22f), new Color(Accent.r, Accent.g, Accent.b, 0.32f), AccentHover, 11, FontStyle.Bold);
chipOffStyle = Button(Field, FieldFocus, Muted, 11, FontStyle.Bold);
chipOnStyle.padding = chipOffStyle.padding = new RectOffset(12, 12, 0, 0);
logoStyle = Label(28, Color.white, FontStyle.Bold);
taglineStyle = Label(10, Muted, FontStyle.Bold);
titleStyle = Label(24, Color.white, FontStyle.Bold);
textStyle = Label(13, Muted, FontStyle.Normal);
textStyle.wordWrap = true;
captionStyle = Label(11, Muted, FontStyle.Bold);
captionStyle.margin.bottom = 6;
statusStyle = Label(12, Color.white, FontStyle.Normal);
statusStyle.alignment = TextAnchor.MiddleLeft;
headerCellStyle = Label(11, Muted, FontStyle.Bold);
headerCellStyle.alignment = TextAnchor.MiddleLeft;
rowNameStyle = Label(14, Color.white, FontStyle.Normal);
rowNameStyle.alignment = TextAnchor.MiddleLeft;
rowTagStyle = Label(11, Accent, FontStyle.Bold);
rowTagStyle.alignment = TextAnchor.MiddleRight;
badgeStyle = new GUIStyle(rowTagStyle);
badgeStyle.alignment = TextAnchor.MiddleLeft;
badgeStyle.padding = new RectOffset(6, 6, 2, 2);
placeholderStyle = Label(13, Muted, FontStyle.Italic);
placeholderStyle.alignment = TextAnchor.MiddleLeft;
toggleStyle = new GUIStyle(GUI.skin.toggle);
toggleStyle.normal.textColor = toggleStyle.onNormal.textColor = toggleStyle.hover.textColor = toggleStyle.onHover.textColor = Color.white;
toggleStyle.fontSize = 11;
toggleStyle.fontStyle = FontStyle.Bold;
navStyle = Button(Sidebar, Secondary, Muted, 14, FontStyle.Normal);
navStyle.alignment = TextAnchor.MiddleLeft;
navStyle.padding = new RectOffset(26, 10, 0, 0);
navActiveStyle = Button(Secondary, Secondary, Color.white, 14, FontStyle.Bold);
navActiveStyle.alignment = TextAnchor.MiddleLeft;
navActiveStyle.padding = new RectOffset(26, 10, 0, 0);
primaryStyle = Button(Accent, AccentHover, Color.white, 14, FontStyle.Bold);
secondaryStyle = Button(Secondary, SecondaryHover, Color.white, 14, FontStyle.Bold);
tabStyle = Button(Sidebar, Secondary, Muted, 11, FontStyle.Bold);
fieldStyle = UiTheme.Field(14);
stylesReady = true;
}
static GUIStyle Label(int size, Color color, FontStyle font)
{
var s = new GUIStyle(GUI.skin.label);
s.fontSize = size;
s.fontStyle = font;
s.normal.textColor = color;
return s;
}
static GUIStyle Button(Color normal, Color hover, Color text, int size, FontStyle font)
{
var s = new GUIStyle(GUI.skin.button);
// abgerundete Ecken (9-Slice), siehe UiTheme
s.normal.background = UiTheme.Rounded(normal);
s.hover.background = UiTheme.Rounded(hover);
s.active.background = UiTheme.Rounded(hover);
s.focused.background = UiTheme.Rounded(normal);
s.normal.textColor = s.hover.textColor = s.active.textColor = s.focused.textColor = text;
s.border = new RectOffset(UiTheme.Radius + 2, UiTheme.Radius + 2, UiTheme.Radius + 2, UiTheme.Radius + 2);
s.fontSize = size;
s.fontStyle = font;
return s;
}
static Texture2D Solid(Color color)
{
var tex = new Texture2D(1, 1);
tex.SetPixel(0, 0, color);
tex.Apply();
tex.hideFlags = HideFlags.HideAndDontSave;
return tex;
}
static Color Hex(int r, int g, int b)
{
return new Color(r / 255f, g / 255f, b / 255f, 1f);
}
}
}