Frontend menu + settings + saves foundation

Netcode frontend pattern: UITK main menu / pause / settings (MenuUi + controllers), on-demand world lifecycle (WorldLauncher/SessionRunner), GameBootstrap menu branch; Graphics/Audio settings (SettingsService/GameVolume); single-slot save foundation (SaveData/SaveService, born-correct load at director spawn, autosave on Siege->Calm + quit); RuntimePanelSettings + theme; BuildTool menu; 10 EditMode tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 15:05:36 -07:00
parent f3f65bccbf
commit f31ffe910b
56 changed files with 1744 additions and 8 deletions
+118
View File
@@ -0,0 +1,118 @@
using System;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem.UI;
using UnityEngine.UIElements;
namespace ProjectM.Client
{
/// <summary>
/// Shared UI Toolkit helpers for the code-built menu / settings / pause screens — a small palette + styled
/// element factories (so every screen reads in one visual language) plus runtime plumbing: loading the
/// shared <c>PanelSettings</c> (from Resources) and ensuring an <c>EventSystem</c> with an
/// <c>InputSystemUIInputModule</c> exists. Runtime UITK receives pointer/click events THROUGH the
/// EventSystem, and this project uses the Input System (1.19.0), so the module must be the Input-System one,
/// not the legacy StandaloneInputModule — without it the buttons are silently dead.
/// </summary>
public static class MenuUi
{
public static readonly Color Bg = new(0.04f, 0.05f, 0.07f, 0.96f);
public static readonly Color PanelBg = new(0.09f, 0.11f, 0.15f, 0.98f);
public static readonly Color Accent = new(0.30f, 0.85f, 1f, 1f); // Aether cyan
public static readonly Color TextCol = new(0.86f, 0.92f, 0.97f, 1f);
public static readonly Color SubCol = new(0.60f, 0.68f, 0.76f, 1f);
public static PanelSettings LoadPanelSettings()
=> Resources.Load<PanelSettings>("RuntimePanelSettings");
public static void EnsureEventSystem()
{
if (UnityEngine.Object.FindFirstObjectByType<EventSystem>() != null) return;
var go = new GameObject("EventSystem");
go.AddComponent<EventSystem>();
go.AddComponent<InputSystemUIInputModule>();
}
public static VisualElement FullScreenRoot(bool dim)
{
var root = new VisualElement();
root.style.position = Position.Absolute;
root.style.left = 0; root.style.right = 0; root.style.top = 0; root.style.bottom = 0;
root.style.alignItems = Align.Center;
root.style.justifyContent = Justify.Center;
if (dim) root.style.backgroundColor = Bg;
return root;
}
public static VisualElement Card(string titleText)
{
var card = new VisualElement();
card.style.backgroundColor = PanelBg;
card.style.paddingLeft = 28; card.style.paddingRight = 28;
card.style.paddingTop = 22; card.style.paddingBottom = 22;
card.style.minWidth = 380;
card.style.alignItems = Align.Stretch;
Round(card, 10);
Border(card, new Color(Accent.r, Accent.g, Accent.b, 0.25f), 1);
if (!string.IsNullOrEmpty(titleText)) card.Add(Title(titleText));
return card;
}
public static Label Title(string text)
{
var l = new Label(text);
l.style.unityFontStyleAndWeight = FontStyle.Bold;
l.style.fontSize = 26;
l.style.color = Accent;
l.style.unityTextAlign = TextAnchor.MiddleCenter;
l.style.marginBottom = 16;
return l;
}
public static Label Caption(string text)
{
var l = new Label(text);
l.style.fontSize = 13;
l.style.color = SubCol;
l.style.unityTextAlign = TextAnchor.MiddleCenter;
l.style.marginTop = 8; l.style.marginBottom = 6;
return l;
}
public static Button Button(string text, Action onClick)
{
var b = new Button(onClick) { text = text };
b.style.height = 40;
b.style.fontSize = 16;
b.style.marginTop = 5; b.style.marginBottom = 5;
b.style.color = TextCol;
b.style.backgroundColor = new Color(0.16f, 0.20f, 0.27f, 1f);
b.style.unityFontStyleAndWeight = FontStyle.Bold;
Round(b, 6);
Border(b, new Color(0f, 0f, 0f, 0f), 0);
return b;
}
public static Button SmallButton(string text, Action onClick)
{
var b = Button(text, onClick);
b.style.width = 36; b.style.height = 30;
b.style.marginLeft = 4; b.style.marginRight = 4;
return b;
}
public static void Round(VisualElement e, float r)
{
e.style.borderTopLeftRadius = r; e.style.borderTopRightRadius = r;
e.style.borderBottomLeftRadius = r; e.style.borderBottomRightRadius = r;
}
public static void Border(VisualElement e, Color c, float w)
{
e.style.borderLeftWidth = w; e.style.borderRightWidth = w;
e.style.borderTopWidth = w; e.style.borderBottomWidth = w;
e.style.borderLeftColor = c; e.style.borderRightColor = c;
e.style.borderTopColor = c; e.style.borderBottomColor = c;
}
}
}