From 024fc6eb9dadff3b351ee4a1a5f90dbe54658123 Mon Sep 17 00:00:00 2001 From: Dennis Westermann Date: Wed, 19 Aug 2026 18:11:27 +0200 Subject: [PATCH] feat(ui): settings rows in the pause menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner request after the T-03 play round: changing the volume meant leaving the match. The pause menu now carries the same settings the main menu owns — music and SFX each with on/off + volume slider, fullscreen toggle and a resolution cycle — all writing through the shared GameSettingsStore gateway (ApplyAndSave): applied immediately, persisted to the same settings.json, no duplicated state. The earlier 'settings live in the main menu' hint is superseded by the owner decision. Verified: Nova.Presentation.UI builds clean; Unity EditMode 615/615. Play-side check (slider audibility, panel height with the section open) is on the manual list — OnGUI rows have no automated click path. --- .../Scripts/Presentation/UI/PauseMenuHud.cs | 108 +++++++++++++++++- CHANGELOG.md | 6 + 2 files changed, 110 insertions(+), 4 deletions(-) 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 /