champions-of-luneraria/Assets/Luneraria/UI/Hud/LuneSettingsPanel.cs
Martin Stickelbroeck 4883d95a2f U5.4 Figuren: Surface-Materialien, Randlicht, SSAO
Die Figuren (Einheiten, Monster, Völker, Helden) bekommen den Materialweg
der Welt; dazu Umgebungsverdeckung für die ganze Szene.

- Surface-Materialien für alle 146 Figurenmaterialien (142 neu unter
  Materials/Units/, die LN_Boss_* gehörten schon zu den Gebäuden). Farbe,
  Rauigkeit und Emission bleiben die Blender-Werte; Metalle werden
  metallisch (vorher war keines der 414 Figurenmaterialien metallisch),
  ein Randlicht hebt die Silhouetten im Draufblick vom Boden ab.
- Oberflächenstruktur je Materialfamilie im eigenen Raum der Figur, nur
  in den HUD-Porträts. Im Feld (30-60 px hohe Figuren) war sie nicht zu
  sehen und kostete rund 10 % Bildrate.
- Ringborkes 4k-Satz liegt am Schnittholz (RB_Cutwood), wohin er gehört;
  U5.3 hatte ihn an die Rinde gebunden. Verwaistes RB_Bark.mat entfernt.
- SSAO über LuneProjectSetup: nach den opaken Objekten, halbe Auflösung,
  Radius 0,35 m, Ausblendung ab 260 m. Standardmäßig an (Entscheidung des
  Projektinhabers), in den Optionen abschaltbar, -luneSsao 0 für eine
  Sitzung. Die Variante mit Tiefen-Normalen-Vorpass kostete 19-28 %.
- Shader: Steinreliefkarte nur noch für Gelände, Stein, Pflaster, Erde.
- Werkzeug: Übersichtsbilder mit LUNE_DRESS=1 eingekleidet wie im Spiel;
  das Materialset wird nach der leeren Vorschauszene geladen (sie entlädt
  unreferenzierte Assets).
- Tests: EditMode 29/29, PlayMode 14/14, Verify 718/0, Player-Selbsttest
  30/30 (4 neue zur SSAO-Option), Fingerabdrücke unverändert
  (DCE95C96556F3A1A, 7B8E43AEFC0D60C5).
- Leistung (A/B mit U5.3 im selben Lauf): Mid nah 177,7 -> 148,5 fps
  (-16 %), Karte 127,8 -> 111,5 fps (-13 %); ohne SSAO -3,5 / -4,5 %.
- Doku: U5-VISUAL (U5.4), PORT-STATUS, PORT-QA; Bilder screenshots/u5.4/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:07:28 +02:00

346 lines
14 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;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.UIElements;
namespace Luneraria.UI
{
/// <summary>
/// Options and controls (port of scripts/luneraria/ui/lune_settings.gd): volume, mute, full
/// screen, reduced motion and the HUD size; the key list is the one place where every shortcut
/// is shown. A running combat pauses offline while this window is open (the game controller
/// does that on Opened/Closed).
/// </summary>
public sealed class LuneSettingsPanel : VisualElement
{
public static readonly float[] Scales = { .9f, 1f, 1.15f, 1.3f };
public static readonly (string group, (string key, string action)[] pairs)[] Keys =
{
("Planung", new[] { ("1–5", "Truppe kaufen"), ("Leertaste", "Bereit"), ("Klick auf Einheit", "Details, Aufwerten, Einsatz"), ("Esc", "Auswahl oder Fenster schließen") }),
("Kampf", new[] { ("Leertaste / P", "Pause (offline)"), ("Tempo-Knopf", "1× · 2× · 4×") }),
("Kamera", new[] { ("F2 · F3 · F4", "Top · Mid · Bottom"), ("G", "Bossgrotte"), ("B", "eigener Ancient"), ("H", "eigener Held"), ("Tab", "nächste Lane"),
("O", "Übersicht"), ("R", "zurücksetzen"), ("Q / E", "drehen"), ("WASD · Pfeile · mittlere Maustaste", "verschieben"), ("Mausrad", "Zoom"), ("Minimap", "klicken oder ziehen") }),
("Allgemein", new[] { ("F1", "Optionen"), ("M", "Ton an/aus"), ("F11", "Vollbild") }),
};
public event Action Opened;
public event Action Closed;
readonly LuneSlider _volume;
readonly Dictionary<string, LuneSwitch> _checks = new Dictionary<string, LuneSwitch>();
public readonly List<LuneButton> ScaleButtons = new List<LuneButton>();
public LuneSettingsPanel()
{
style.position = Position.Absolute;
style.left = 0f;
style.top = 0f;
style.right = 0f;
style.bottom = 0f;
style.backgroundColor = new Color(.02f, .03f, .04f, .9f);
style.justifyContent = Justify.Center;
style.alignItems = Align.Center;
RegisterCallback<WheelEvent>(evt => evt.StopPropagation());
var panel = new LunePanel(LunePanels.Menu);
panel.style.width = 900f;
Add(panel);
VisualElement column = LuneUi.Box(true, 10f, panel);
Label heading = LuneUi.Label("Optionen & Steuerung", "HeadingLabel", column);
heading.style.alignSelf = Align.Center;
LuneUi.Label("Ein laufender Kampf pausiert offline, solange dieses Fenster geöffnet ist.", "MutedLabel", column).style.alignSelf = Align.Center;
VisualElement top = LuneUi.Box(false, 30f, column);
top.style.alignItems = Align.FlexStart;
VisualElement left = LuneUi.Box(true, 8f, top);
left.style.flexGrow = 1f;
left.style.flexBasis = 0f;
LuneUi.Label("LAUTSTÄRKE", "SectionLabel", left);
_volume = new LuneSlider();
left.Add(_volume);
_volume.Changed += value =>
{
LunePreferences.Volume = value;
LunePreferences.Commit();
Apply();
};
foreach ((string key, string text) entry in new[] { ("muted", "Ton stumm [M]"), ("fullscreen", "Vollbild [F11]"), ("reduced_motion", "Bewegung reduzieren"), ("ambient_occlusion", "Umgebungsverdeckung (SSAO)") })
{
var check = new LuneSwitch(entry.text);
left.Add(check);
string key = entry.key;
check.Changed += value =>
{
if (key == "muted")
LunePreferences.Muted = value;
else if (key == "fullscreen")
LunePreferences.Fullscreen = value;
else if (key == "ambient_occlusion")
LunePreferences.AmbientOcclusion = value;
else
LunePreferences.ReducedMotion = value;
LunePreferences.Commit();
Apply();
};
_checks[key] = check;
}
VisualElement right = LuneUi.Box(true, 8f, top);
right.style.flexGrow = 1f;
right.style.flexBasis = 0f;
LuneUi.Label("UI-GRÖSSE IM SPIEL", "SectionLabel", right);
VisualElement scales = LuneUi.Box(false, 6f, right);
foreach (float value in Scales)
{
var button = new LuneButton(Mathf.RoundToInt(value * 100f) + " %", LuneButtons.HudToggle, scales);
button.style.flexGrow = 1f;
float captured = value;
button.Clicked += () =>
{
LunePreferences.UiScale = captured;
LunePreferences.Commit();
Sync();
};
ScaleButtons.Add(button);
}
LuneUi.Wrap(LuneUi.Label("Größer für hochauflösende oder weit entfernte Bildschirme. Das HUD begrenzt die Größe selbst, sodass nichts aus dem Fenster ragt.", "MutedLabel", right));
var line = new VisualElement { pickingMode = PickingMode.Ignore };
line.style.height = 1f;
line.style.marginTop = 7f;
line.style.marginBottom = 7f;
line.style.backgroundColor = LuneUiTheme.WithAlpha(LuneUiTheme.Bronze, .35f);
column.Add(line);
LuneUi.Label("STEUERUNG", "SectionLabel", column);
VisualElement keys = LuneUi.Box(false, 26f, column);
keys.style.alignItems = Align.FlexStart;
for (int index = 0; index < 2; index++)
{
VisualElement list = LuneUi.Box(true, 2f, keys);
list.style.flexGrow = 1f;
list.style.flexBasis = 0f;
var groups = index == 0 ? new[] { Keys[0], Keys[1], Keys[3] } : new[] { Keys[2] };
foreach (var group in groups)
{
LuneUi.Label(LuneUi.Upper(group.group), "EyebrowLabel", list, 14f).style.marginTop = 6f;
foreach (var pair in group.pairs)
{
VisualElement row = LuneUi.Box(false, 14f, list);
Label key = LuneUi.Label(pair.key, "CardFactLabel", row, 15f);
key.style.minWidth = 160f;
LuneUi.SetColor(key, LuneUiTheme.BronzeLight);
LuneUi.Wrap(LuneUi.Label(pair.action, "MutedLabel", row, 15f)).style.flexGrow = 1f;
}
}
}
var back = new LuneButton("ZURÜCK [Esc]", LuneButtons.Primary, column);
back.style.alignSelf = Align.Center;
back.Clicked += Close;
LuneUi.Show(this, false);
}
public bool Open { get { return style.display != DisplayStyle.None; } }
public void Show()
{
Sync();
LuneUi.Show(this, true);
BringToFront();
Opened?.Invoke();
}
public void Close()
{
if (!Open)
return;
LuneUi.Show(this, false);
Closed?.Invoke();
}
/// <summary>Reads the preferences into the controls (Godot sync()).</summary>
public void Sync()
{
_volume.SetValueWithoutNotify(LunePreferences.Volume);
_checks["muted"].SetValueWithoutNotify(LunePreferences.Muted);
_checks["fullscreen"].SetValueWithoutNotify(LunePreferences.Fullscreen);
_checks["reduced_motion"].SetValueWithoutNotify(LunePreferences.ReducedMotion);
_checks["ambient_occlusion"].SetValueWithoutNotify(LunePreferences.AmbientOcclusion);
int best = 1;
for (int index = 0; index < Scales.Length; index++)
{
if (Mathf.Abs(Scales[index] - LunePreferences.UiScale) < Mathf.Abs(Scales[best] - LunePreferences.UiScale))
best = index;
}
for (int index = 0; index < ScaleButtons.Count; index++)
ScaleButtons[index].On = index == best;
}
/// <summary>Volume, mute, full screen and ambient occlusion take effect at once.</summary>
public static void Apply()
{
AudioListener.volume = LunePreferences.Muted ? 0f : LunePreferences.Volume;
ApplyAmbientOcclusion(LunePreferences.AmbientOcclusion);
if (Application.isBatchMode)
return;
if (Screen.fullScreen != LunePreferences.Fullscreen)
Screen.fullScreen = LunePreferences.Fullscreen;
}
/// <summary>
/// Switches the SSAO feature of the URP renderer (U5.4). Only in the player: in the editor
/// the renderer is a project asset, and switching it there would change the file.
/// </summary>
static void ApplyAmbientOcclusion(bool on)
{
if (Application.isEditor)
return;
ScriptableRendererFeature feature = AmbientOcclusionFeature();
if (feature != null && feature.isActive != on)
feature.SetActive(on);
}
/// <summary>The SSAO feature of the active URP renderer, or null (player self-test).</summary>
public static ScriptableRendererFeature AmbientOcclusionFeature()
{
if (!(GraphicsSettings.currentRenderPipeline is UniversalRenderPipelineAsset pipeline))
return null;
foreach (ScriptableRendererData data in pipeline.rendererDataList)
{
if (data == null)
continue;
foreach (ScriptableRendererFeature feature in data.rendererFeatures)
{
if (feature is ScreenSpaceAmbientOcclusion)
return feature;
}
}
return null;
}
}
/// <summary>Horizontal slider in the HUD look (track, bronze fill, handle); value 0–1.</summary>
public sealed class LuneSlider : VisualElement
{
public event Action<float> Changed;
public float Value { get; private set; }
bool _drag;
public LuneSlider()
{
style.height = 28f;
pickingMode = PickingMode.Position;
generateVisualContent += Draw;
RegisterCallback<PointerDownEvent>(evt =>
{
if (evt.button != 0)
return;
_drag = true;
this.CapturePointer(evt.pointerId);
Set(evt.localPosition.x);
});
RegisterCallback<PointerMoveEvent>(evt =>
{
if (_drag)
Set(evt.localPosition.x);
});
RegisterCallback<PointerUpEvent>(evt =>
{
_drag = false;
this.ReleasePointer(evt.pointerId);
});
}
void Set(float x)
{
float value = Mathf.Clamp01((x - 9f) / Mathf.Max(1f, layout.width - 18f));
if (Mathf.Approximately(value, Value))
return;
Value = value;
MarkDirtyRepaint();
Changed?.Invoke(value);
}
public void SetValueWithoutNotify(float value)
{
Value = Mathf.Clamp01(value);
MarkDirtyRepaint();
}
void Draw(MeshGenerationContext context)
{
Painter2D p = context.painter2D;
float width = layout.width;
float y = layout.height * .5f;
LunePlates.Line(p, new Vector2(9f, y), new Vector2(width - 9f, y), new Color(0f, 0f, 0f, .55f), 6f);
float x = 9f + (width - 18f) * Value;
LunePlates.Line(p, new Vector2(9f, y), new Vector2(x, y), LuneUiTheme.Bronze, 6f);
p.fillColor = LuneUiTheme.BronzeLight;
p.BeginPath();
p.Arc(new Vector2(x, y), 8f, Angle.Degrees(0f), Angle.Degrees(360f));
p.ClosePath();
p.Fill();
}
}
/// <summary>On/off switch with a label (Godot CheckButton with the toggle icons of the menu theme).</summary>
public sealed class LuneSwitch : VisualElement
{
public event Action<bool> Changed;
public bool Value { get; private set; }
readonly VisualElement _knob;
public LuneSwitch(string text)
{
style.flexDirection = FlexDirection.Row;
style.alignItems = Align.Center;
style.height = 34f;
pickingMode = PickingMode.Position;
Label label = LuneUi.Label(text, "CardFactLabel", this);
label.style.flexGrow = 1f;
_knob = new VisualElement { pickingMode = PickingMode.Ignore };
_knob.style.width = 44f;
_knob.style.height = 22f;
_knob.generateVisualContent += Draw;
Add(_knob);
RegisterCallback<PointerDownEvent>(evt =>
{
if (evt.button == 0)
Click();
});
}
/// <summary>Text of the switch (self-test lookup).</summary>
public string Text { get { return this.Q<Label>()?.text ?? ""; } }
/// <summary>Flips the switch as a click does.</summary>
public void Click()
{
Value = !Value;
_knob.MarkDirtyRepaint();
Changed?.Invoke(Value);
}
public void SetValueWithoutNotify(bool value)
{
Value = value;
_knob.MarkDirtyRepaint();
}
void Draw(MeshGenerationContext context)
{
Painter2D p = context.painter2D;
float w = _knob.layout.width;
float h = _knob.layout.height;
float r = h * .5f;
p.fillColor = Value ? LuneUiTheme.WithAlpha(LuneUiTheme.Bronze, .85f) : new Color(0f, 0f, 0f, .5f);
p.BeginPath();
p.Arc(new Vector2(r, r), r, Angle.Degrees(90f), Angle.Degrees(270f));
p.LineTo(new Vector2(w - r, 0f));
p.Arc(new Vector2(w - r, r), r, Angle.Degrees(270f), Angle.Degrees(450f));
p.ClosePath();
p.Fill();
p.fillColor = Value ? LuneUiTheme.Parchment : LuneUiTheme.Muted;
p.BeginPath();
p.Arc(new Vector2(Value ? w - r : r, r), r - 3f, Angle.Degrees(0f), Angle.Degrees(360f));
p.ClosePath();
p.Fill();
}
}
}