champions-of-luneraria/Assets/Luneraria/UI/Hud/LuneArmyPanels.cs
Martin Stickelbroeck 2b86c0d3b5 U3B/U3C Zwischenstand: HUD-Bereiche, Eingabe und Befehle
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>
2026-10-02 01:45:02 +02:00

329 lines
18 KiB
C#
Raw Permalink 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 System.Linq;
using Luneraria.Core;
using UnityEngine;
using UnityEngine.UIElements;
namespace Luneraria.UI
{
/// <summary>
/// Attack champions (Godot LuneChampionPanel): target lane, the three champion cards (copper,
/// hatched, "1 WELLE") and the own bookings ("Einsatz in Welle 15 – MID"). Champions are bought
/// in planning n, fight once in wave n+1 and then vanish; they never join the permanent army.
/// </summary>
public sealed class LuneChampionPanel : VisualElement
{
public event Action<Command> CommandRequested;
public string TargetLane = "mid";
public readonly Dictionary<string, LuneUnitCard> Cards = new Dictionary<string, LuneUnitCard>();
public readonly Dictionary<string, LuneButton> LaneButtons = new Dictionary<string, LuneButton>();
readonly VisualElement _bookings;
readonly Label _count;
string _signature = "";
LuneHudModel _model;
public LuneChampionPanel()
{
pickingMode = PickingMode.Ignore;
style.flexDirection = FlexDirection.Row;
style.alignItems = Align.FlexStart;
VisualElement lanes = LuneUi.Box(true, 4f, this);
LuneUi.Label("ZIEL", "HudCaps", lanes);
foreach (string lane in LuneMap.Lanes)
{
var button = new LuneButton(LuneUi.Upper(LuneMap.LaneNames[lane]), LuneButtons.HudToggle, lanes);
button.style.minWidth = 86f;
button.On = lane == TargetLane;
string captured = lane;
button.Clicked += () =>
{
TargetLane = captured;
_signature = "";
foreach (KeyValuePair<string, LuneButton> pair in LaneButtons)
pair.Value.On = pair.Key == captured;
if (_model != null)
Apply(_model);
};
LaneButtons[lane] = button;
LuneTooltip.Attach(button, () => new LuneTip { Title = "Ziel-Lane " + LuneMap.LaneNames[captured], Text = "Gebuchte Champions greifen in der nächsten Welle auf dieser Lane an." });
}
VisualElement row = LuneUi.Box(false, 6f, this);
row.style.marginLeft = 10f;
row.style.alignItems = Align.FlexStart;
foreach (KeyValuePair<string, ChampionSpec> pair in LuneContent.Champions)
{
string championId = pair.Key;
ChampionSpec spec = pair.Value;
var card = new LuneUnitCard();
row.Add(card);
string role = championId == "ch_schutz" ? "schutz" : championId == "ch_druck" ? "druck" : championId == "ch_belagerung" ? "siege" : "melee";
card.Setup(championId, spec.Model, spec.Name, role, spec.Cost, "", true, DataCatalog.GetSend(spec.Base));
card.Clicked += () => CommandRequested?.Invoke(Command.ChampionBook(championId, TargetLane));
Cards[championId] = card;
LuneTooltip.Attach(card, () => Tip(championId));
}
VisualElement right = LuneUi.Box(true, 4f, this);
right.style.marginLeft = 10f;
right.style.flexGrow = 1f;
right.style.flexShrink = 1f;
right.style.minWidth = 200f;
VisualElement head = LuneUi.Box(false, 0f, right);
LuneUi.Label("EINSÄTZE", "HudCaps", head).style.flexGrow = 1f;
_count = LuneUi.Label("", "HudMuted", head);
_bookings = LuneUi.Box(true, 3f, right);
}
public void Apply(LuneHudModel model)
{
_model = model;
List<LuneHudModel.BookingView> bookings = model.OwnBookings();
int fresh = model.Planning ? model.Me.Draft.NewBookings.Count : 0;
int gold = model.OwnGold;
string signature = string.Join(",", bookings) + "|" + gold + "|" + model.Planning + "|" + model.Locked + "|" + TargetLane + "|" + model.State.RoundNumber;
if (signature == _signature)
return;
_signature = signature;
bool canAct = model.Planning && !model.Locked;
foreach (KeyValuePair<string, LuneUnitCard> pair in Cards)
{
int cost = LuneContent.Champions[pair.Key].Cost;
string why = "";
if (!model.Planning)
why = "Buchungen nur in der Planung.";
else if (model.Locked)
why = "Bereit gemeldet – die Planung ist verbindlich.";
else if (fresh >= LuneContent.MaxBookings)
why = "Höchstens " + LuneContent.MaxBookings + " Champions pro Runde.";
else if (gold < cost)
why = "Es fehlen " + (cost - gold) + " Gold.";
pair.Value.SetAvailable(why.Length == 0, why, !canAct, gold < cost);
}
_count.text = fresh + "/" + LuneContent.MaxBookings + " diese Runde";
_bookings.Clear();
if (bookings.Count == 0)
{
LuneUi.Wrap(LuneUi.Label("Keine Champions gebucht. Kauf jetzt – Einsatz in Welle " + (model.State.RoundNumber + (model.Planning ? 1 : 2)) + ".", "HudMuted", _bookings));
return;
}
foreach (LuneHudModel.BookingView booking in bookings)
{
VisualElement line = LuneUi.Box(false, 6f, _bookings);
LuneUi.Glyph("hourglass", LuneUiTheme.Champion, 14f, line);
bool running = booking.DeployRound == model.State.RoundNumber;
VisualElement texts = LuneUi.Box(true, -2f, line);
texts.style.flexGrow = 1f;
LuneUi.Label(LuneContent.Champions[booking.ChampionId].Name, "HudText", texts);
LuneUi.Label("Einsatz in Welle " + booking.DeployRound + " – " + LuneUi.Upper(LuneMap.LaneNames[booking.Lane]) + (running ? " (jetzt)" : ""), "HudMuted", texts);
if (booking.Fresh)
{
var cancel = new LuneButton("×", LuneButtons.HudIconButton, line);
cancel.Disabled = !canAct;
string uid = booking.Uid;
cancel.Clicked += () => CommandRequested?.Invoke(Command.ChampionCancel(uid));
LuneTooltip.Attach(cancel, () => new LuneTip { Title = "Buchung zurücknehmen", Text = "Volle Erstattung, solange die Planung läuft." });
}
}
}
LuneTip Tip(string championId)
{
ChampionSpec spec = LuneContent.Champions[championId];
int round = _model != null ? _model.State.RoundNumber + 1 : 2;
UnitDef definition = LuneContent.ChampionDefinition(championId, round, _model != null ? _model.State.Balance : LuneBalance.Default());
var tip = new LuneTip
{
Title = spec.Name + " · Angriffschampion", Glyph = "champion", GlyphColor = LuneUiTheme.Champion,
Subtitle = spec.RoleText + " · " + spec.Cost + " Gold · Einsatz in Welle " + round + " – " + LuneUi.Upper(LuneMap.LaneNames[TargetLane]),
Text = spec.Text,
Note = "Einmaliger Einsatz: kämpft eine Welle lang und verschwindet danach. Die Ankündigung ist für alle sichtbar; sein Tod bringt dem Gegner " + definition.Bounty + " Gold.",
Warn = Cards[championId].Reason,
};
tip.Stats.AddRange(LuneStats.Rows(definition));
return tip;
}
}
/// <summary>
/// The selected normal unit (Godot LuneUnitDetail): portrait, name, stage, role, HP, damage,
/// tempo, range, armour, ability, upgrade with cost and stat change, return value, and the lane
/// order with TOP, MID, BOTTOM and GROTTE (only allowed options active). Normal units carry no
/// items. In combat or after Ready it only informs.
/// </summary>
public sealed class LuneUnitDetail : LunePanel
{
public const string GrottoWarning = "Diese Einheit fehlt deiner Lane in der nächsten Kampfphase.";
public event Action<Command> CommandRequested;
public event Action Closed;
public string Uid = "";
readonly LunePortraitElement _portrait;
readonly Label _name;
readonly Label _sub;
readonly LuneHpBar _live;
readonly VisualElement _stats;
readonly Label _ability;
readonly Label _types;
readonly Label _upgradeTitle;
readonly Label _upgradeDelta;
public readonly LuneButton UpgradeButton;
public readonly LuneButton SellButton;
readonly Label _sellNote;
public readonly Dictionary<string, LuneButton> LaneButtons = new Dictionary<string, LuneButton>();
readonly Label _warning;
readonly Label _locked;
string _signature = "";
public LuneUnitDetail() : base(LunePanels.Hud)
{
VisualElement column = LuneUi.Box(true, 5f, this);
VisualElement head = LuneUi.Box(false, 10f, column);
head.style.alignItems = Align.FlexStart;
_portrait = new LunePortraitElement();
_portrait.style.width = 76f;
_portrait.style.height = 76f;
head.Add(_portrait);
VisualElement titles = LuneUi.Box(true, 2f, head);
titles.style.flexGrow = 1f;
titles.style.flexShrink = 1f;
titles.style.minWidth = 0f;
_name = LuneUi.Wrap(LuneUi.Label("", "HudName", titles));
_sub = LuneUi.Wrap(LuneUi.Label("", "HudMuted", titles));
_live = new LuneHpBar(0f, 6f);
titles.Add(_live);
var close = new LuneButton(null, LuneButtons.HudIconButton, head);
close.style.width = 32f;
close.style.height = 30f;
LuneUi.Glyph("close", LuneUiTheme.Muted, 12f, close);
close.Clicked += () => Closed?.Invoke();
LuneTooltip.Attach(close, () => new LuneTip { Title = "Auswahl aufheben", Text = "Esc" });
_stats = LuneUi.Box(true, 2f, column);
LuneTooltip.Attach(_stats, StatsTip);
_types = LuneUi.Wrap(LuneUi.Label("", "HudMuted", column));
_ability = LuneUi.Wrap(LuneUi.Label("", "HudText", column));
VisualElement upgradeBox = LuneUi.Box(true, 2f, column);
VisualElement upgradeRow = LuneUi.Box(false, 8f, upgradeBox);
_upgradeTitle = LuneUi.Wrap(LuneUi.Label("", "HudText", upgradeRow));
_upgradeTitle.style.flexGrow = 1f;
UpgradeButton = new LuneButton("", LuneButtons.HudPrimary, upgradeRow);
UpgradeButton.Clicked += () => CommandRequested?.Invoke(Command.Upgrade(Uid));
_upgradeDelta = LuneUi.Wrap(LuneUi.Label("", "HudMuted", upgradeBox));
VisualElement laneRow = LuneUi.Box(false, 4f, column);
foreach (string lane in LuneMap.Lanes.Concat(new[] { LuneMatch.Grotto }))
{
string title = lane == LuneMatch.Grotto ? "GROTTE" : LuneUi.Upper(LuneMap.LaneNames[lane]);
var button = new LuneButton(title, LuneButtons.HudToggle, laneRow);
button.style.flexGrow = 1f;
string captured = lane;
button.Clicked += () => CommandRequested?.Invoke(Command.Assign(Uid, captured));
LaneButtons[lane] = button;
LuneTooltip.Attach(button, () => new LuneTip { Title = "Einsatz: " + title,
Text = captured == LuneMatch.Grotto ? GrottoWarning : "Kämpft in der nächsten Welle auf der " + LuneMap.LaneNames[captured] + "-Lane." });
}
_warning = LuneUi.Wrap(LuneUi.Label(GrottoWarning, "HudText", column));
LuneUi.SetColor(_warning, LuneUiTheme.Warn);
VisualElement sellRow = LuneUi.Box(false, 8f, column);
SellButton = new LuneButton("", LuneButtons.HudButton, sellRow);
SellButton.Clicked += () => CommandRequested?.Invoke(Command.Sell(Uid));
_sellNote = LuneUi.Wrap(LuneUi.Label("", "HudMuted", sellRow));
_sellNote.style.flexGrow = 1f;
_locked = LuneUi.Wrap(LuneUi.Label("", "HudMuted", column));
}
/// <summary>entry: own roster entry; live: the unit in combat or null.</summary>
public void Apply(RosterEntry entry, LuneHudModel model, LuneUnit live)
{
Uid = entry.Uid;
LuneHudModel.TroopInfo data = model.Troop(entry.TypeId);
bool planning = model.Planning;
bool locked = model.Locked;
int gold = model.OwnGold;
string signature = entry.Uid + ":" + entry.TypeId + ":" + entry.Lane + ":" + entry.Invested + ":" + entry.Fresh + "|" + planning + "|" + locked + "|" + gold + "|"
+ model.State.GrottoAvailable() + "|" + (live != null) + "|" + model.State.RoundNumber;
if (live != null)
{
_live.Fraction = live.Alive ? (float)(live.Hp / Math.Max(1.0, live.MaxHp)) : 0f;
_live.Fill = LuneHpBar.HealthColor(_live.Fraction, LuneUiTheme.TeamColor(model.Team));
}
LuneUi.Show(_live, live != null);
if (signature == _signature)
return;
_signature = signature;
UnitDef def = data.Def;
_portrait.ShowModel(entry.TypeId);
_name.text = def.DisplayName;
string laneText = entry.Lane == LuneMatch.Grotto ? "Grotte" : LuneMap.LaneNames[entry.Lane];
_sub.text = LuneStats.RoleName(data.Role) + " · Stufe " + (data.Stage == 2 ? "II" : "I") + " · " + laneText + (live != null && !live.Alive ? " · gefallen" : "");
_stats.Clear();
List<string[]> rows = LuneStats.ShortRows(def);
for (int index = 0; index < 4; index += 2)
{
VisualElement line = LuneUi.Box(false, 6f, _stats);
for (int cell = index; cell < index + 2; cell++)
{
LuneUi.Glyph(rows[cell][0], LuneUiTheme.Muted, 14f, line);
Label value = LuneUi.Clip(LuneUi.Label(rows[cell][1], "HudText", line));
value.style.flexGrow = 1f;
value.style.flexBasis = 0f;
}
}
_types.text = "Rüstung " + LuneStats.ArmorName(def.ArmorType) + " · Angriff " + LuneStats.DamageName(def.AttackType) + ", stark gegen " + LuneStats.StrongAgainst(def.AttackType);
_ability.text = def.AbilityDescription ?? "";
bool canAct = planning && !locked;
string upgrade = def.UpgradeId ?? "";
if (upgrade.Length == 0)
{
_upgradeTitle.text = "Höchste Stufe erreicht.";
LuneUi.Show(UpgradeButton, false);
_upgradeDelta.text = "";
}
else
{
UnitDef next = DataCatalog.GetUnit(upgrade);
_upgradeTitle.text = "Aufwerten → " + next.DisplayName;
LuneUi.Show(UpgradeButton, true);
UpgradeButton.Text = "↑ " + next.Cost;
UpgradeButton.Disabled = !canAct || gold < next.Cost;
string delta = "Leben " + LuneUiTheme.Number(def.MaxHp) + " → " + LuneUiTheme.Number(next.MaxHp) + " · Schaden " + LuneUiTheme.Decimal(def.Damage) + " → " + LuneUiTheme.Decimal(next.Damage);
if (Math.Abs(def.AttackRange - next.AttackRange) > 1e-9)
delta += " · Reichweite " + LuneUiTheme.Decimal(def.AttackRange) + " → " + LuneUiTheme.Decimal(next.AttackRange) + " m";
if (canAct && gold < next.Cost)
delta += " · es fehlen " + (next.Cost - gold) + " Gold";
_upgradeDelta.text = delta;
}
LuneUi.Show(_upgradeDelta, _upgradeDelta.text.Length > 0);
int refund = entry.Fresh ? entry.Invested : (int)Math.Floor(entry.Invested * model.State.Balance.SellRefund);
SellButton.Text = "Zurückgeben +" + refund;
SellButton.Disabled = !canAct;
LuneUi.Show(SellButton, planning);
_sellNote.text = entry.Fresh ? "voller Preis" : LuneUiTheme.Round(model.State.Balance.SellRefund * 100.0) + " % von " + entry.Invested;
LuneUi.Show(_sellNote, planning);
bool grottoOpen = model.State.GrottoAvailable();
foreach (KeyValuePair<string, LuneButton> pair in LaneButtons)
{
pair.Value.On = entry.Lane == pair.Key;
pair.Value.Disabled = !canAct || (pair.Key == LuneMatch.Grotto && !grottoOpen);
}
LuneUi.Show(_warning, entry.Lane == LuneMatch.Grotto);
if (!planning)
_locked.text = "Im Kampf gibt es keine Befehle; Aufwertung, Rückgabe und Einsatz wieder in der Planung.";
else if (locked)
_locked.text = "Bereit gemeldet: Die Planung ist verbindlich.";
else if (!grottoOpen)
_locked.text = "Grotte gesperrt bis Welle " + model.State.Boss.NextAvailableRound + ".";
else
_locked.text = "";
LuneUi.Show(_locked, _locked.text.Length > 0);
}
LuneTip StatsTip()
{
string type = _portrait.ModelId;
if (string.IsNullOrEmpty(type))
return null;
UnitDef data = DataCatalog.GetUnit(type);
var tip = new LuneTip { Title = data.DisplayName, Note = "Ein Angriff pro Sekunde mal Tempo; Schaden je Treffer vor Rüstung. Stark: 130 %, schwach: 75 % (Rüstungsmatrix)." };
tip.Stats.AddRange(LuneStats.Rows(data, true));
return tip;
}
}
}