Das G3-HUD steht im Spiel; die Abnahme (U3D) folgt. - U3B: alle Bereiche und Komponenten aus Godot übertragen (LuneHud, Lanes, Bossgrotte, nächste Welle, Kaufkarten, Armee, Held mit Items und Talenten, Angriffschampions, Team, Ereignisse, Minimap, Tooltip, Optionen, Partieende). Porträts aus den echten Modellen, einmal gerendert. - Offene G3-Punkte umgesetzt: „1 WELLE“ als Band am Porträtrand, Schadens- und Rüstungssymbole auf den Karten, Armeewert, Heldenporträts auf der Minimap. Befehlsplatz für die Ultimates der geplanten Helden reserviert (verborgen). - U3C: LuneGame verdrahtet Befehle (gleiche Schnittstelle wie die Bots), Bereit mit 1-s-Sperre nach dem Kampf, Tempo und Pause, Tasten wie in G3, Kamera-Ziele, Auswahl im Feld, Planungsvorschau und Aufstellungsflächen. Ersetzt die U2-Debug-Anzeige. Bot-Partien und Benchmark unverändert. - Szenen- und Bibliotheksbauer idempotent: Animator-Controller und Post-Processing-Profil werden an Ort und Stelle aktualisiert. Die Szene bekommt bei jedem Bau neue Objekt-IDs, der Inhalt ist gleich. - Prüfung (Port von verify_g3, Teile A–H): 699 Prüfungen ohne Fehler. EditMode 23/23, PlayMode 12/12, Fingerabdrücke der Bot-Partien unverändert (DCE95C96556F3A1A, 7B8E43AEFC0D60C5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
301 lines
13 KiB
C#
301 lines
13 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
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") })
|
||
{
|
||
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
|
||
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);
|
||
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 and full screen take effect at once.</summary>
|
||
public static void Apply()
|
||
{
|
||
AudioListener.volume = LunePreferences.Muted ? 0f : LunePreferences.Volume;
|
||
if (Application.isBatchMode)
|
||
return;
|
||
if (Screen.fullScreen != LunePreferences.Fullscreen)
|
||
Screen.fullScreen = LunePreferences.Fullscreen;
|
||
}
|
||
}
|
||
|
||
/// <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)
|
||
return;
|
||
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();
|
||
}
|
||
}
|
||
}
|