diff --git a/Assets/_Project/Scripts/Presentation/UI/PauseMenuHud.cs b/Assets/_Project/Scripts/Presentation/UI/PauseMenuHud.cs
index 4a40537..8cf8a47 100644
--- a/Assets/_Project/Scripts/Presentation/UI/PauseMenuHud.cs
+++ b/Assets/_Project/Scripts/Presentation/UI/PauseMenuHud.cs
@@ -40,8 +40,9 @@ namespace Nova.Presentation.UI
///
/// READ-ONLY toward the simulation: the only state this component
/// touches is the kernel clock (pause/resume); it never submits a
- /// command. Settings are deliberately NOT a second mask here — they live
- /// in the main menu, and the panel says so instead of drifting a copy.
+ /// command. The settings rows render the shared GameSettingsStore in
+ /// place (owner request after the T-03 round): same gateway, same
+ /// persistence as the main-menu mask — not a copy of state.
///
///
[DisallowMultipleComponent]
@@ -61,6 +62,7 @@ public sealed class PauseMenuHud : MonoBehaviour
private bool _menuOpen;
private bool _pausedByMenu;
+ private bool _settingsOpen;
private GUIStyle _headlineStyle;
private GUIStyle _bodyStyle;
@@ -86,6 +88,7 @@ private void OnDisable()
// so a re-enabled component (next match start) starts clean.
_menuOpen = false;
_pausedByMenu = false;
+ _settingsOpen = false;
ModalSurfaceLink.Reset();
}
@@ -184,7 +187,7 @@ private void OnGUI()
Matrix4x4 previousMatrix = GUI.matrix;
GUI.matrix = Matrix4x4.Scale(new Vector3(scale, scale, 1f));
- Rect rect = CenteredRect(250f);
+ Rect rect = CenteredRect(_settingsOpen ? 470f : 250f);
GUI.Box(rect, GUIContent.none, HudChrome.OpaquePanelStyle);
GUILayout.BeginArea(rect);
GUILayout.Space(HudChrome.OpaquePanelStyle.padding.top);
@@ -199,6 +202,15 @@ private void OnGUI()
{
CloseMenu(playClick: true);
}
+ if (GUILayout.Button(_settingsOpen ? "Einstellungen ausblenden" : "Einstellungen …", _buttonStyle, GUILayout.Height(28f)))
+ {
+ AudioServiceLocator.Play2D(SoundEventId.UI_Click);
+ _settingsOpen = !_settingsOpen;
+ }
+ if (_settingsOpen)
+ {
+ DrawSettingsRows();
+ }
if (GUILayout.Button("Zum Hauptmenü", _buttonStyle, GUILayout.Height(30f)))
{
AudioServiceLocator.Play2D(SoundEventId.UI_Click);
@@ -214,7 +226,6 @@ private void OnGUI()
if (_menu != null) _menu.Quit();
}
GUILayout.Space(6f);
- GUILayout.Label("Einstellungen (Ton, Bild, Auflösung) gibt es im Hauptmenü.", _bodyStyle);
GUILayout.EndArea();
GUI.matrix = previousMatrix;
@@ -228,6 +239,95 @@ private Rect CenteredRect(float height)
return new Rect(x, y, _panelWidth, height);
}
+ ///
+ /// The in-menu settings rows (owner request, T-03 feedback round):
+ /// the same functions the main menu's settings mask owns — music and
+ /// SFX on/off + volume, fullscreen, resolution — writing through the
+ /// same GameSettingsStore gateway, so a change applies at once and
+ /// persists. A second mask would drift; these rows are the pause
+ /// menu's own rendering of the SAME store, not a copy of state.
+ ///
+ private void DrawSettingsRows()
+ {
+ GameSettings settings = GameSettingsStore.Current;
+
+ bool musicEnabled = GUILayout.Toggle(settings.musicEnabled, " Musik an", _bodyStyle);
+ float musicVolume = DrawVolumeSlider("Musik", settings.musicVolume);
+ bool sfxEnabled = GUILayout.Toggle(settings.sfxEnabled, " SFX an", _bodyStyle);
+ float sfxVolume = DrawVolumeSlider("SFX", settings.sfxVolume);
+ bool fullScreen = GUILayout.Toggle(settings.fullScreen, " Vollbild", _bodyStyle);
+ bool cycleResolution = GUILayout.Button(ResolutionLabel(settings), _buttonStyle, GUILayout.Height(24f));
+
+ if (musicEnabled != settings.musicEnabled
+ || sfxEnabled != settings.sfxEnabled
+ || fullScreen != settings.fullScreen
+ || !Mathf.Approximately(musicVolume, settings.musicVolume)
+ || !Mathf.Approximately(sfxVolume, settings.sfxVolume))
+ {
+ settings.musicEnabled = musicEnabled;
+ settings.musicVolume = musicVolume;
+ settings.sfxEnabled = sfxEnabled;
+ settings.sfxVolume = sfxVolume;
+ settings.fullScreen = fullScreen;
+ GameSettingsStore.ApplyAndSave();
+ }
+
+ if (cycleResolution)
+ {
+ CycleResolution(settings);
+ }
+ }
+
+ /// One labelled volume slider row ("Musik — 40 %").
+ private float DrawVolumeSlider(string label, float value)
+ {
+ GUILayout.BeginHorizontal();
+ GUILayout.Label($"{label} — {Mathf.RoundToInt(value * 100f)} %", _bodyStyle, GUILayout.Width(110f));
+ float next = GUILayout.HorizontalSlider(value, 0f, 1f);
+ GUILayout.EndHorizontal();
+ return next;
+ }
+
+ private static string ResolutionLabel(GameSettings settings)
+ {
+ return settings.resolutionWidth > 0
+ ? $"Auflösung: {settings.resolutionWidth}×{settings.resolutionHeight}"
+ : $"Auflösung: aktuell ({Screen.width}×{Screen.height})";
+ }
+
+ /// Cycles the stored resolution through the display's supported modes (next on each click, wrapping).
+ private static void CycleResolution(GameSettings settings)
+ {
+ Resolution[] modes = Screen.resolutions;
+ if (modes.Length == 0) return;
+
+ int index = -1;
+ for (int i = 0; i < modes.Length; i++)
+ {
+ if (modes[i].width == settings.resolutionWidth && modes[i].height == settings.resolutionHeight)
+ {
+ index = i;
+ break;
+ }
+ }
+ // No stored match yet (0×0 = native): start from the CURRENT mode.
+ if (index < 0)
+ {
+ for (int i = 0; i < modes.Length; i++)
+ {
+ if (modes[i].width == Screen.width && modes[i].height == Screen.height)
+ {
+ index = i;
+ break;
+ }
+ }
+ }
+ Resolution next = modes[(index + 1) % modes.Length];
+ settings.resolutionWidth = next.width;
+ settings.resolutionHeight = next.height;
+ GameSettingsStore.ApplyAndSave();
+ }
+
private void EnsureStyles()
{
if (_headlineStyle == null)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3d3d56d..e9561d7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -92,6 +92,12 @@ die Versionierung folgt (in der aktuellen Doku-Phase) dem Dokumentationsstand de
eigene Bystander-Zeile ohne Befehlsbeitrag) und trägt bei einer
beschädigten Einzelauswahl deren HP im Titel; rein sim-lesend, ohne
Eingriff in die Simulation
+- **Einstellungen jetzt auch im Pausemenü.** Auf Inhaber-Wunsch nach der
+ T-03-Proberunde: das Pausemenü enthält dieselben Funktionen wie die
+ Hauptmenü-Maske — Musik und SFX jeweils mit An/Aus und Lautstärke, Vollbild
+ und Auflösungswechsel. Beide Oberflächen schreiben über dasselbe Gateway
+ (`GameSettingsStore.ApplyAndSave`): wirkt sofort, persistiert in dieselbe
+ `settings.json`, kein Zustands-Duplikat
- **Restbestand der Vorkommen anklickbar und sichtbar (Paket 21.2, #86).**
Ein Linksklick auf ein Aetherium-Vorkommen (auch ein erschöpftes) zeigt in der
Befehlskarte Restbestand und Anfangsreserve („Aetherium-Vorkommen — 6.420 /