using System.Collections.Generic;
using UnityEngine;
namespace MSCO
{
///
/// 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).
///
public class LobbyUI
{
enum Page { P2P, ServerBrowser, Favorites }
// Vollbild, automatisch skaliert: gebaut für RefHeight x RefWidth, vergrößert/verkleinert passend zur Auflösung
const float RefHeight = 760f;
const float RefWidth = 1180f;
const float MinScale = 0.8f, MaxScale = 1.75f;
const float SidebarWidth = 240f;
const float StatusHeight = 40f;
const float MaxContentWidth = 1100f;
float vw, vh, uiScale = 1f; // virtuelle Bildschirmgröße (Einheiten vor der Skalierung)
float contentHeight = 400f; // Höhe des Inhaltsbereichs - Listen wachsen damit
/// Höhe für eine Liste: was nach "reserved" für Überschriften/Knöpfe übrig bleibt.
float ListHeight(float reserved, float min) { return Mathf.Max(min, contentHeight - reserved); }
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);
static readonly Color Gold = Hex(0xff, 0xb4, 0x5a); // hervorgehobene (featured) Server
Page page = Page.P2P;
Vector2 profileScroll;
bool quitConfirm;
Vector2 playerScroll;
Vector2 serverScroll;
bool browserLoaded;
string selectedServer; // "adresse:port" des geöffneten Server-Profils
string searchInput = "";
bool showSteam = true, showNonSteam = true, hideFull;
readonly Dictionary nicknameEdits = new Dictionary(); // Favoriten-Tab: Adresse -> bearbeiteter Kurzname
string favoriteAddInput = "";
Vector2 favoritesScroll;
Texture2D texHeartOn, texHeartOff;
string nameInput;
string directInput;
const string PrefDirect = "MSCO_LastServer";
bool stylesReady;
Texture2D texBackground, texSidebar, texAccent, texOnline, texPending, texMuted, texRow;
Texture2D texDivider, texGold, texBarBack, texBarFill, texBarFull;
GUIStyle rowHoverBox, pingGoodStyle, pingMidStyle, pingBadStyle;
List shownAll, shownServers; // Stand des letzten Layout-Durchgangs
string layoutError;
GUIStyle dangerStyle;
GUIStyle windowBox, sidebarBox, rowBox, accentBox, pillStyle, pillMutedStyle, chipOnStyle, chipOffStyle;
GUIStyle featuredRowBox, pillFeaturedStyle;
GUIStyle logoStyle, taglineStyle, navStyle, navActiveStyle, titleStyle, textStyle, captionStyle,
fieldStyle, primaryStyle, secondaryStyle, statusStyle, tabStyle, headerCellStyle,
rowNameStyle, rowTagStyle, badgeStyle, toggleStyle, placeholderStyle;
/// Fläche, die das Fenster gerade belegt (GUI-Koordinaten, oben = 0).
public Rect ScreenRect { get; private set; }
/// Der Launcher füllt im Hauptmenü immer den ganzen Bildschirm - das Spielmenü dahinter ist gesperrt.
public bool IsOpen { get { return true; } }
/// Ist die Maus gerade über dem Fenster bzw. der eingeklappten Leiste?
public bool ContainsMouse()
{
Vector3 m = Input.mousePosition;
return ScreenRect.Contains(new Vector2(m.x, Screen.height - m.y));
}
public void Draw(MultiplayerManager mp)
{
EnsureStyles();
uiScale = Mathf.Clamp(Mathf.Min(Screen.height / RefHeight, Screen.width / RefWidth), MinScale, MaxScale);
vw = Screen.width / uiScale;
vh = Screen.height / uiScale;
ScreenRect = new Rect(0, 0, Screen.width, Screen.height);
Matrix4x4 previous = GUI.matrix;
GUI.matrix = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(uiScale, uiScale, 1f));
try { DrawScaled(mp); }
finally { GUI.matrix = previous; }
}
void DrawScaled(MultiplayerManager mp)
{
// Undurchsichtiger Hintergrund über das ganze Spielmenü (Logo, Farbauswahl, Knöpfe ... sind dahinter verborgen)
if (texScreen == null) texScreen = Solid(Hex(0x0d, 0x0d, 0x10));
GUI.DrawTexture(new Rect(0, 0, vw, vh), texScreen);
bool pendingJoin = mp.PendingJoinHost != null && !mp.IsInSession;
bool versionWarning = mp.PendingVersionWarning != null && !mp.IsInSession;
bool disconnected = mp.DisconnectNotice != null && !mp.IsInSession;
// Getrennt / Neu-Verbinden: eigenes Fenster in der Bildmitte, alles andere gesperrt
if (disconnected) { DrawPrompt(mp, false); return; }
if (mp.IsReconnecting) { DrawPrompt(mp, true); return; }
var window = new Rect(0, 0, vw, vh);
GUI.Box(window, GUIContent.none, windowBox);
DrawSidebar(new Rect(0, 0, SidebarWidth, vh - StatusHeight));
// Inhalt mittig im freien Bereich, nicht beliebig breit (lesbar bleiben)
float free = vw - SidebarWidth;
float width = Mathf.Min(MaxContentWidth, free - 96f);
var content = new Rect(SidebarWidth + (free - width) / 2f, 40f, width, vh - StatusHeight - 72f);
contentHeight = content.height;
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 if (page == Page.Favorites) DrawFavorites(mp);
else DrawServerBrowser(mp);
GUILayout.EndArea();
DrawStatusBar(mp, new Rect(0, vh - StatusHeight, vw, 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);
GUI.DrawTexture(new Rect(r.x + 20, r.y + 98, r.width - 40, 1), texDivider);
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;
y += 44;
int favCount = Favorites.All.Count;
if (NavButton(new Rect(r.x, y, r.width, 42), "Favorites" + (favCount > 0 ? " (" + favCount + ")" : ""), page == Page.Favorites)) page = Page.Favorites;
// Eigenes Hauptmenü: Spiel beenden (die Knöpfe des Spiel-Menüs sind ausgeblendet)
if (quitConfirm)
{
if (GUI.Button(new Rect(r.x + 16, r.yMax - 118, r.width - 32, 36), "QUIT GAME?", dangerStyle)) Application.Quit();
if (GUI.Button(new Rect(r.x + 16, r.yMax - 76, r.width - 32, 30), "CANCEL", tabStyle)) quitConfirm = false;
}
else if (GUI.Button(new Rect(r.x + 16, r.yMax - 76, r.width - 32, 36), "QUIT", tabStyle)) quitConfirm = true;
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 and invite up to " + (MultiplayerManager.MaxPlayers - 1) +
" friends through Steam - continue your own save or start a new game.", 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 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) ||
(s.Tags != null && s.Tags.Exists(t => (t ?? "").Contains(search)))));
List 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(ListHeight(230f, 140f)));
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));
// Hervorgehobene Server (Keymaster-Admin) stehen oben - der Master liefert sie zuerst
var rowRect = new Rect(row.x, row.y + 2, row.width, row.height - 4);
bool hover = rowRect.Contains(Event.current.mousePosition);
GUI.Box(rowRect, GUIContent.none, s.Featured ? featuredRowBox : hover ? rowHoverBox : rowBox);
if (s.Featured) GUI.DrawTexture(new Rect(row.x, row.y + 8, 3, row.height - 16), texGold);
// Herz: als Favorit merken / entfernen (vor dem Zeilen-Knopf, damit es den Klick bekommt)
Heart(new Rect(row.xMax - 58, row.y + 7, 20, 20), s);
// 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;
Favorites.Favorite fav = Favorites.FindByAddress(s.Address, s.Port);
if (s.Featured) nameX += Pill(nameX, row, "FEATURED", pillFeaturedStyle);
if (fav != null) nameX += Pill(nameX, row, fav.Nickname, pillMutedStyle);
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);
// Spielerzahl mit Füllbalken (wie auf der Website)
float px = row.x + row.width * 0.5f;
GUI.Label(new Rect(px, row.y - 3, 70, row.height), s.Players + "/" + s.MaxPlayers, rowNameStyle);
float fill = s.MaxPlayers > 0 ? Mathf.Clamp01((float)s.Players / s.MaxPlayers) : 0f;
GUI.DrawTexture(new Rect(px, row.y + 24, 48, 3), texBarBack);
GUI.DrawTexture(new Rect(px, row.y + 24, Mathf.Max(2f, 48 * fill), 3), fill >= 1f ? texBarFull : texBarFill);
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, 60, row.height), s.Ping < 0 ? "..." : s.Ping + " ms", PingStyle(s.Ping));
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();
}
}
// ---------------------------------------------------------------- Favoriten
/// Herz: gefüllt = Favorit. Klick merkt den Server (Kurzname aus dem Namen) bzw. entfernt ihn.
void Heart(Rect r, ServerListEntry s)
{
EnsureHearts();
Favorites.Favorite fav = Favorites.FindByAddress(s.Address, s.Port);
bool hover = r.Contains(Event.current.mousePosition);
GUI.DrawTexture(r, fav != null || hover ? texHeartOn : texHeartOff);
if (GUI.Button(r, GUIContent.none, GUIStyle.none))
{
if (fav != null) Favorites.Remove(fav);
else AddFavorite(s.Name, s.Address, s.Port);
}
}
static void AddFavorite(string serverName, string host, int port)
{
// Kurzname aus dem Servernamen; ist er schon vergeben, eine Zahl anhängen
string baseNick = Favorites.SuggestNickname(serverName), nick = baseNick;
for (int i = 2; Favorites.Find(nick) != null && i < 100; i++) nick = baseNick + i;
string error = Favorites.Set(nick, host, port, serverName);
if (error != null) MscoLog.Warning(error);
}
/// Tab "Favorites": Kurznamen setzen, beitreten, entfernen, per Adresse hinzufügen.
void DrawFavorites(MultiplayerManager mp)
{
EnsureHearts();
GUILayout.Label("Favorites", titleStyle);
GUILayout.Label("Give each server a nickname - switch in the game with /switch in the chat or F7 console.", textStyle);
GUILayout.Space(8);
List favs = Favorites.All;
favoritesScroll = GUILayout.BeginScrollView(favoritesScroll, GUILayout.Height(ListHeight(190f, 160f)));
if (favs.Count == 0)
GUILayout.Label("No favorites yet. Click the heart next to a server in the Server Browser, or add one by address below.", textStyle);
Favorites.Favorite remove = null;
foreach (Favorites.Favorite f in favs)
{
ServerListEntry live = mp.Browser.Servers.Find(x => x.Port == f.Port && string.Equals(x.Address, f.Host, System.StringComparison.OrdinalIgnoreCase));
GUILayout.BeginHorizontal(rowBox, GUILayout.Height(44));
GUILayout.Space(6);
// Herz (entfernen)
Rect heart = GUILayoutUtility.GetRect(22, 22, GUILayout.Width(22), GUILayout.Height(40));
Rect heartIcon = new Rect(heart.x, heart.y + 10, 20, 20);
GUI.DrawTexture(heartIcon, texHeartOn);
if (GUI.Button(heartIcon, GUIContent.none, GUIStyle.none)) remove = f;
GUILayout.Space(8);
// Kurzname bearbeiten
string edit;
if (!nicknameEdits.TryGetValue(f.Address, out edit)) edit = f.Nickname;
GUILayout.BeginVertical(GUILayout.Width(150));
GUILayout.Space(7);
edit = GUILayout.TextField(edit, 20, fieldStyle, GUILayout.Height(28), GUILayout.Width(150));
GUILayout.EndVertical();
nicknameEdits[f.Address] = edit;
GUILayout.Space(6);
bool changed = Favorites.CleanNickname(edit) != f.Nickname;
GUILayout.BeginVertical(GUILayout.Width(64));
GUILayout.Space(7);
GUI.enabled = changed;
if (GUILayout.Button("SAVE", tabStyle, GUILayout.Height(28), GUILayout.Width(64)))
{
string error = Favorites.Set(edit, f.Host, f.Port, f.ServerName);
if (error != null) MscoLog.Warning(error);
nicknameEdits.Remove(f.Address);
}
GUI.enabled = true;
GUILayout.EndVertical();
GUILayout.Space(10);
// Server + Status
GUILayout.BeginVertical();
GUILayout.Space(4);
GUILayout.Label(string.IsNullOrEmpty(f.ServerName) ? f.Address : f.ServerName, rowNameStyle);
GUILayout.Label(live == null ? f.Address + " · not listed / offline"
: f.Address + " · " + live.Players + "/" + live.MaxPlayers + " · " + (live.Ping < 0 ? "..." : live.Ping + " ms"), textStyle);
GUILayout.EndVertical();
GUILayout.FlexibleSpace();
GUILayout.BeginVertical(GUILayout.Width(90));
GUILayout.Space(7);
if (GUILayout.Button("JOIN", primaryStyle, GUILayout.Height(30), GUILayout.Width(90))) mp.SwitchServer(f.Nickname);
GUILayout.EndVertical();
GUILayout.Space(6);
GUILayout.EndHorizontal();
GUILayout.Space(4);
}
if (remove != null) { Favorites.Remove(remove); nicknameEdits.Remove(remove.Address); }
GUILayout.EndScrollView();
GUILayout.FlexibleSpace();
GUILayout.Label("ADD BY ADDRESS", captionStyle);
GUILayout.BeginHorizontal();
favoriteAddInput = GUILayout.TextField(favoriteAddInput ?? "", 60, fieldStyle, GUILayout.Height(34));
if (string.IsNullOrEmpty(favoriteAddInput))
{
Rect field = GUILayoutUtility.GetLastRect();
GUI.Label(new Rect(field.x + 10, field.y, field.width, field.height), "host:port", placeholderStyle);
}
GUILayout.Space(10);
if (GUILayout.Button("ADD", secondaryStyle, GUILayout.Height(34), GUILayout.Width(110)))
{
string host;
int port;
if (ServerBrowser.TryParseAddress(favoriteAddInput, out host, out port))
{
ServerListEntry live = mp.Browser.Servers.Find(x => x.Port == port && x.Address == host);
AddFavorite(live != null ? live.Name : host, host, port);
favoriteAddInput = "";
}
else MscoLog.Warning("Enter an address like 1.2.3.4:7777");
}
GUILayout.EndHorizontal();
}
void EnsureHearts()
{
if (texHeartOn != null && texHeartOff != null) return;
texHeartOn = HeartTexture(new Color(0.93f, 0.28f, 0.33f), true);
texHeartOff = HeartTexture(new Color(1f, 1f, 1f, 0.35f), false);
}
/// Herz als Textur (gefüllt oder nur Umriss), weich geglättet.
static Texture2D HeartTexture(Color color, bool filled)
{
const int size = 48;
var tex = new Texture2D(size, size, TextureFormat.ARGB32, false);
for (int py = 0; py < size; py++)
for (int px = 0; px < size; px++)
{
// Herz-Kurve (x² + y² - 1)³ - x² y³ <= 0, mehrfach abgetastet für glatte Kanten
int inside = 0, samples = 0;
for (int sy = 0; sy < 3; sy++)
for (int sx = 0; sx < 3; sx++)
{
float x = ((px + (sx + 0.5f) / 3f) / size - 0.5f) * 2.6f;
float y = ((py + (sy + 0.5f) / 3f) / size - 0.55f) * 2.6f;
float a = x * x + y * y - 1f;
bool inHeart = a * a * a - x * x * y * y * y <= 0f;
if (!filled && inHeart)
{
// Umriss: innen ein kleineres Herz aussparen
float x2 = x * 1.28f, y2 = (y + 0.08f) * 1.28f;
float b = x2 * x2 + y2 * y2 - 1f;
if (b * b * b - x2 * x2 * y2 * y2 * y2 <= 0f) inHeart = false;
}
if (inHeart) inside++;
samples++;
}
tex.SetPixel(px, py, new Color(color.r, color.g, color.b, color.a * inside / samples));
}
tex.filterMode = FilterMode.Bilinear;
tex.wrapMode = TextureWrapMode.Clamp;
tex.Apply();
tex.hideFlags = HideFlags.HideAndDontSave;
return tex;
}
/// Server-Profil: wem der Server gehört, Version, Spieler, Laufzeit ...
void DrawServerProfile(MultiplayerManager mp, ServerListEntry s)
{
if (GUILayout.Button("◂ BACK", tabStyle, GUILayout.Height(26), GUILayout.Width(90)))
{
selectedServer = null;
return;
}
GUILayout.Space(8);
if (s == null)
{
GUILayout.Label("Server offline", titleStyle);
GUILayout.Label("This server does not respond anymore.", textStyle);
return;
}
// Kopf: Name + Abzeichen links, JOIN rechts - immer sichtbar
GUILayout.BeginHorizontal();
GUILayout.BeginVertical();
GUILayout.Label(s.Name, titleStyle);
GUILayout.BeginHorizontal();
Rect badgeRow = GUILayoutUtility.GetRect(10, 20, GUILayout.ExpandWidth(true));
float bx = badgeRow.x;
if (s.Featured) bx += Pill(bx, badgeRow, "FEATURED", pillFeaturedStyle);
bx += Pill(bx, badgeRow, s.RequireSteam ? "STEAM" : "NON-STEAM", s.RequireSteam ? pillStyle : pillMutedStyle);
if (s.IsLocal) bx += Pill(bx, badgeRow, "LOCAL", pillStyle);
if (s.Version != MSCO.ModVersion) bx += Pill(bx, badgeRow, "v" + s.Version, pillStyle);
if (s.Tags != null)
foreach (string tag in s.Tags)
if (!string.IsNullOrEmpty(tag) && tag != "featured") bx += Pill(bx, badgeRow, tag.ToUpper(), pillMutedStyle);
GUILayout.EndHorizontal();
GUILayout.EndVertical();
GUILayout.Space(12);
Rect heartSlot = GUILayoutUtility.GetRect(44, 44, GUILayout.Width(44), GUILayout.Height(44));
GUI.Box(heartSlot, GUIContent.none, rowBox);
Heart(new Rect(heartSlot.x + 11, heartSlot.y + 11, 22, 22), s);
GUILayout.Space(8);
GUI.enabled = CanJoin(mp, s);
if (GUILayout.Button(s.Players >= s.MaxPlayers ? "SERVER FULL" : "JOIN SERVER", primaryStyle, GUILayout.Height(44), GUILayout.Width(180)))
mp.JoinServer(s.Address, s.Port, s.Name, s.MaxPlayers);
GUI.enabled = true;
GUILayout.EndHorizontal();
if (!string.IsNullOrEmpty(s.Description)) GUILayout.Label(s.Description, textStyle);
GUILayout.Space(10);
// Ohne Steam: Name wählen (sauber in einem eigenen Kasten)
if (s.RequireSteam && !mp.SteamAvailable)
{
GUILayout.BeginHorizontal(rowBox, GUILayout.Height(40));
GUILayout.Label("This server requires Steam - start the game through Steam to join.", textStyle);
GUILayout.EndHorizontal();
GUILayout.Space(10);
}
else if (!mp.SteamAvailable)
{
GUILayout.BeginHorizontal(rowBox, GUILayout.Height(44));
GUILayout.Space(6);
GUILayout.BeginVertical(GUILayout.Width(110));
GUILayout.Space(4);
GUILayout.Label("PLAYING AS", captionStyle);
GUILayout.Label("no Steam", textStyle);
GUILayout.EndVertical();
GUILayout.BeginVertical();
GUILayout.Space(5);
DrawNameField(mp, 220);
GUILayout.EndVertical();
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
GUILayout.Space(10);
}
// Infos links, Spielerliste rechts
bool versionOk = s.Version == MSCO.ModVersion;
GUILayout.BeginHorizontal();
GUILayout.BeginVertical(GUILayout.Width(250));
InfoLine("HOSTED BY", string.IsNullOrEmpty(s.Owner) ? "unknown" : s.Owner, rowNameStyle);
InfoLine("VERSION", "v" + s.Version + (versionOk ? "" : " (you: v" + MSCO.ModVersion + ")"), versionOk ? rowNameStyle : badgeStyle);
InfoLine("STEAM", s.RequireSteam ? "Required" : "Not required", rowNameStyle);
InfoLine("ONLINE FOR", s.UptimeSeconds >= 0 ? FormatUptime(s.UptimeSeconds) : "-", rowNameStyle);
InfoLine("PING", s.Ping < 0 ? "-" : s.Ping + " ms", PingStyle(s.Ping));
InfoLine("ADDRESS", s.Address + ":" + s.Port, rowNameStyle);
GUILayout.EndVertical();
GUILayout.Space(16);
GUILayout.BeginVertical(rowBox, GUILayout.ExpandWidth(true), GUILayout.MinHeight(150));
GUILayout.Space(6);
GUILayout.Label("PLAYERS " + s.Players + " / " + s.MaxPlayers, captionStyle);
profileScroll = GUILayout.BeginScrollView(profileScroll, GUILayout.Height(ListHeight(330f, 130f)));
if (s.PlayerNames == null || s.PlayerNames.Count == 0)
GUILayout.Label("Nobody online right now.", textStyle);
else
foreach (string name in s.PlayerNames)
{
Rect line = GUILayoutUtility.GetRect(10, 24, GUILayout.ExpandWidth(true));
GUI.DrawTexture(new Rect(line.x + 2, line.y + 8, 8, 8), texOnline);
GUI.Label(new Rect(line.x + 18, line.y, line.width - 18, line.height), name, rowNameStyle);
}
GUILayout.EndScrollView();
GUILayout.EndVertical();
GUILayout.EndHorizontal();
GUILayout.FlexibleSpace();
}
/// Eine Info-Zeile: Bezeichnung links (grau), Wert rechts.
void InfoLine(string caption, string value, GUIStyle valueStyle)
{
GUILayout.BeginHorizontal(GUILayout.Height(26));
GUILayout.Label(caption, captionStyle, GUILayout.Width(100));
GUILayout.Label(value, valueStyle);
GUILayout.EndHorizontal();
}
/// Kleines abgerundetes Abzeichen, so breit wie sein Text. Gibt die belegte Breite zurück.
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;
}
/// Filter-Chip (an = orange umrandet), ersetzt die Unity-Häkchen.
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);
}
/// Ping farbig: grün gut, gelb geht so, rot schlecht.
GUIStyle PingStyle(int ping)
{
if (ping < 0) return rowNameStyle;
return ping < 80 ? pingGoodStyle : ping < 160 ? pingMidStyle : pingBadStyle;
}
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";
}
/// Server hat eine andere Version: warnen, Verbinden nur auf Wunsch.
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();
}
/// Nachfrage nach einem JOIN-Link der Website (keine Webseite soll einen ungefragt verbinden).
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();
}
const float PromptWidth = 540f;
const float PromptHeight = 270f;
Texture2D texDim, texDanger, texScreen;
GUIStyle promptTextStyle;
///
/// Fenster in der Bildmitte über abgedunkeltem Hintergrund: nach dem Trennen (Kick, Ban, Shutdown, Timeout ...)
/// mit Grund und OK, bzw. beim automatischen Neu-Verbinden nach reset-world mit Versuch-Anzeige und CANCEL.
///
void DrawPrompt(MultiplayerManager mp, bool reconnecting)
{
if (texDim == null) texDim = Solid(new Color(0f, 0f, 0f, 0.6f));
if (texDanger == null) texDanger = Solid(Hex(0xe5, 0x48, 0x4d));
if (promptTextStyle == null)
{
promptTextStyle = new GUIStyle(rowNameStyle);
promptTextStyle.wordWrap = true;
promptTextStyle.fontSize = 15;
}
GUI.DrawTexture(new Rect(0, 0, vw, vh), texDim);
var r = new Rect((vw - PromptWidth) / 2f, (vh - PromptHeight) / 2f, PromptWidth, PromptHeight);
UiTheme.DrawShadow(r);
GUI.Box(r, GUIContent.none, windowBox);
string title = reconnecting
? ((mp.ReconnectMessage ?? "").StartsWith("Switching") ? "SWITCHING SERVER" : "RECONNECTING")
: mp.DisconnectTitle;
bool bad = !reconnecting && title != "DISCONNECTED";
GUI.DrawTexture(new Rect(r.x, r.y, r.width, 4), bad ? texDanger : texAccent);
float x = r.x + 32, w = r.width - 64, y = r.y + 28;
GUI.Label(new Rect(x, y, w, 34), title, titleStyle);
y += 50;
GUI.Label(new Rect(x, y, w, 20), "REASON", captionStyle);
if (reconnecting)
{
GUI.Label(new Rect(x, y + 22, w, 44), mp.ReconnectMessage ?? "", promptTextStyle);
string attempt = mp.ReconnectAttempt == 0
? "Connecting again in a moment..."
: "Attempt " + mp.ReconnectAttempt + " of " + mp.ReconnectMaxAttempts +
(string.IsNullOrEmpty(mp.Status) ? "" : " - " + mp.Status);
GUI.Label(new Rect(x, y + 74, w, 40), attempt, textStyle);
if (GUI.Button(new Rect(r.xMax - 32 - 170, r.yMax - 28 - 42, 170, 42), "CANCEL", secondaryStyle)) mp.CancelReconnect();
return;
}
GUI.Label(new Rect(x, y + 22, w, 60), mp.DisconnectNotice ?? "", promptTextStyle);
GUI.Label(new Rect(x, y + 88, w, 20), "Your own save game has been restored.", textStyle);
if (GUI.Button(new Rect(r.xMax - 32 - 170, r.yMax - 28 - 42, 170, 42), "OK", primaryStyle)) mp.ClearDisconnectNotice();
}
bool newGameConfirm;
/// Host: eigenen Spielstand weiterspielen oder ein neues Spiel anlegen (mit Sicherheitsabfrage).
void DrawHostStart(MultiplayerManager mp)
{
System.DateTime? saved = SaveSync.LocalSaveTime;
if (newGameConfirm)
{
GUILayout.BeginVertical(rowBox);
GUILayout.Space(6);
GUILayout.Label("START A NEW GAME?", captionStyle);
GUILayout.Label(saved != null
? "Your current save game is kept as a backup in MSCO_Saves - everyone starts in a fresh world."
: "Everyone starts in a fresh world.", textStyle);
GUILayout.Space(6);
GUILayout.BeginHorizontal();
if (GUILayout.Button("YES, NEW GAME", dangerStyle, GUILayout.Height(38), GUILayout.Width(170)))
{
newGameConfirm = false;
mp.StartHostNewGame();
}
GUILayout.Space(8);
if (GUILayout.Button("CANCEL", secondaryStyle, GUILayout.Height(38), GUILayout.Width(110))) newGameConfirm = false;
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
GUILayout.Space(6);
GUILayout.EndVertical();
return;
}
GUILayout.BeginHorizontal();
GUI.enabled = saved != null;
GUILayout.BeginVertical(GUILayout.Width(190));
if (GUILayout.Button("CONTINUE", primaryStyle, GUILayout.Height(42), GUILayout.Width(190)))
mp.StartHostGame();
GUILayout.Label(saved != null ? "saved " + saved.Value.ToString("dd.MM.yyyy HH:mm") : "no save game yet", captionStyle);
GUILayout.EndVertical();
GUI.enabled = true;
GUILayout.Space(10);
GUILayout.BeginVertical(GUILayout.Width(190));
if (GUILayout.Button("NEW GAME", secondaryStyle, GUILayout.Height(42), GUILayout.Width(190)))
{
if (saved != null) newGameConfirm = true;
else mp.StartHostNewGame();
}
GUILayout.Label("fresh world for everyone", captionStyle);
GUILayout.EndVertical();
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
}
void DrawSession(MultiplayerManager mp)
{
GUILayout.Label(mp.IsHost ? "Hosting" : "Connected", titleStyle);
GUILayout.Label(mp.IsHost
? "Your game is open. Continue your save or start a new game - friends can also join later."
: mp.IsDedicated || mp.IsConnectingToServer
? "Connected to the server - the world is loading" + (string.IsNullOrEmpty(mp.Status) ? "." : ": " + mp.Status)
: "Waiting for the host to start the game.", textStyle);
GUILayout.Space(16);
GUILayout.Label("PLAYERS " + mp.Players.Count + "/" + mp.SessionMaxPlayers, captionStyle);
playerScroll = GUILayout.BeginScrollView(playerScroll, GUILayout.Height(ListHeight(mp.IsHost ? 330f : 170f, 120f)));
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);
// Eigener Punkt gelb, solange man noch lädt/synchronisiert; grün, wenn man wirklich im Spiel ist
GUI.DrawTexture(new Rect(row.x + 12, row.y + 11, 8, 8), p.IsLocal && !mp.IsFullyConnected ? texPending : 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);
if (mp.IsHost)
{
DrawHostStart(mp);
GUILayout.Space(10);
}
GUILayout.BeginHorizontal();
if (mp.IsHost)
{
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.IsFullyConnected ? texOnline : mp.IsInSession || mp.IsReconnecting ? 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 - 72, r.height), mp.Status, statusStyle);
}
// ---------------------------------------------------------------- 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);
// Fenster und Seitenleiste mit feinem Rand (Seitenleiste: ihr Rand trennt sie vom Inhalt)
windowBox = UiTheme.Card(Background, 10);
sidebarBox = UiTheme.Card(Sidebar, 10);
rowBox = UiTheme.Panel9(Field, UiTheme.Radius);
rowHoverBox = UiTheme.Card(FieldFocus, UiTheme.Radius);
texDivider = Solid(new Color(1f, 1f, 1f, 0.07f));
texGold = Solid(Gold);
texBarBack = Solid(new Color(1f, 1f, 1f, 0.1f));
texBarFill = Solid(Online);
texBarFull = Solid(UiTheme.Danger);
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;
pillFeaturedStyle = new GUIStyle(pillStyle);
pillFeaturedStyle.normal.background = UiTheme.Rounded(Gold, 4);
pillFeaturedStyle.normal.textColor = Hex(0x1a, 0x12, 0x06);
featuredRowBox = UiTheme.Panel9(Color.Lerp(Field, Gold, 0.16f), UiTheme.Radius);
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;
pingGoodStyle = new GUIStyle(rowNameStyle);
pingGoodStyle.normal.textColor = Online;
pingMidStyle = new GUIStyle(rowNameStyle);
pingMidStyle.normal.textColor = Pending;
pingBadStyle = new GUIStyle(rowNameStyle);
pingBadStyle.normal.textColor = UiTheme.DangerHover;
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.hover.textColor = Color.white; // Hover: Text hell (vorher blieb er grau)
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);
UiTheme.ApplyPrimaryGradient(primaryStyle);
dangerStyle = Button(UiTheme.Danger, UiTheme.DangerHover, Color.white, 13, FontStyle.Bold);
secondaryStyle = Button(Secondary, SecondaryHover, Color.white, 14, FontStyle.Bold);
tabStyle = Button(Sidebar, Secondary, Muted, 11, FontStyle.Bold);
tabStyle.hover.textColor = Color.white;
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);
}
}
}