champions-of-luneraria/Assets/Luneraria/UI/Hud/LuneHeroPanels.cs
Martin Stickelbroeck 66867f3332 U3D Abnahme: Prüfung, Feld um das HUD, Bilder, Leistung
Das G3-HUD ist abgenommen; der Meilenstein U3 ist damit fertig.

- Prüfung (Port von verify_g3, Teile A–H plus I): 718 Prüfungen ohne
  Fehler. Teil I prüft die Feld-Darstellung um das HUD (U3-Zugabe aus
  dem Motivvergleich): Aufstellungsflächen, Grotteneingang, ruhender
  Boss, Namensschilder, Eigentümer-Ring, Ancient-Balken.
- Feld um das HUD: Namensschilder für Helden (mit Stufe), Angriffs-
  champions und den eingesetzten Boss („für Blau“), Eigentümer-Ring,
  Ancient-Balken bei Schaden, ruhende Schlundkrone auf dem Podest,
  eigener Grotteneingang, Aufstellungsflächen als eigene Objekte.
- Schriften korrigiert: Die Referenz deklariert Cantarell 560/700–760
  und Cinzel 700, wendet die Gewichte aber nie an (variation_opentype
  mit dem String-Schlüssel „wght“ wird von Godot ignoriert). G3 zeigt
  Regular 400, der Port ebenso — Cinzel-Regular statt Bold, keine
  Kontur zur Aufdickung. Laufweite aus spacing_glyph umgerechnet;
  dunkler Saum als Schatten statt TextCore-Kontur.
- Werkzeuge: HudViewsTests (14 G3-Motive), HudPlaytest (Spieltest über
  die echte Oberfläche per Befehlsdateien), ui_ms_per_frame im
  LuneBenchmark. Layout folgt den echten Panelhöhen, Kamera startet wie
  Godot _ready auf der eigenen Lane.
- Tests: EditMode 23/23, PlayMode 13/13, Kerntests 402/402, Verify
  718/0. Fingerabdrücke der Bot-Partien unverändert (DCE95C96556F3A1A,
  7B8E43AEFC0D60C5).
- Bilder: 14 Motive bei 1920×1080, Auflösungen 1152×720 bis 2560×1440,
  Prüf-Schlüsselbilder, Spieltest-Screenshots.
- Leistung (macOS-Player mit HUD, 116 Einheiten): 169 fps Übersicht,
  218 fps Mid nah; HUD-Skripte 0,090 bzw. 0,078 ms je Bild.
- Dokumentation: U3-UI (U3D, Schriftbefund), PORT-QA (U3), PORT-STATUS
  (U3 fertig, U4 nächster Schritt), KNOWN-BEHAVIOR-DIFFERENCES.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:55:06 +02:00

609 lines
31 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 System.Linq;
using Luneraria.Core;
using UnityEngine;
using UnityEngine.UIElements;
namespace Luneraria.UI
{
/// <summary>
/// The hero in the bottom bar (Godot LuneHeroCard): portrait, name, level, experience
/// progress, HP, role, the three item slots and the talent state. A click opens the hero window
/// (details, items, talents) or, without a hero, the recruiting window. No MOBA action bar:
/// heroes act alone. The command slot is reserved for the ultimates of the planned heroes
/// (PlannedHeroes, trigger Command); it stays hidden while no such hero is playable.
/// </summary>
public sealed class LuneHeroCard : LuneButton
{
public string HeroId = "";
public event Action CommandRequested;
readonly LunePortraitElement _portrait;
readonly Label _name;
readonly Label _level;
readonly LuneGlyphElement _roleGlyph;
readonly Label _role;
readonly LuneHpBar _xp;
readonly Label _xpText;
readonly Label _hp;
readonly LuneHpBar _hpBar;
readonly LuneGlyphElement[] _slots = new LuneGlyphElement[LuneContent.ItemSlots];
readonly LuneStatusBadge _talent;
readonly VisualElement _empty;
readonly VisualElement _filled;
readonly List<LunePortraitElement> _faces = new List<LunePortraitElement>();
readonly Label _emptyTitle;
readonly Label _emptyPrice;
readonly LuneGlyphElement _emptyCoin;
readonly VisualElement _facesRow;
/// <summary>Reserved: the ultimate of a planned hero (released through the command menu in combat).</summary>
public readonly LuneButton CommandSlot;
LuneHudModel _model;
HeroState _hero;
UnitDef _definition;
string _signature = "";
public LuneHeroCard() : base(null, LuneButtons.HudCard)
{
style.minWidth = 320f;
style.height = 132f;
style.justifyContent = Justify.FlexStart;
style.alignItems = Align.Stretch;
style.paddingLeft = 8f;
style.paddingRight = 8f;
style.paddingTop = 8f;
style.paddingBottom = 8f;
_portrait = new LunePortraitElement();
_portrait.style.width = 88f;
Add(_portrait);
_filled = LuneUi.Box(true, 2f, this);
_filled.style.marginLeft = 10f;
_filled.style.flexGrow = 1f;
_filled.style.flexShrink = 1f;
_filled.style.minWidth = 0f;
_name = LuneUi.Clip(LuneUi.Label("", "HudName", _filled));
VisualElement roleRow = LuneUi.Box(false, 5f, _filled);
LuneUi.Glyph("star", LuneUiTheme.Hero, 13f, roleRow);
_level = LuneUi.Label("", "HudValue", roleRow);
_roleGlyph = LuneUi.Glyph("tank", LuneUiTheme.Muted, 14f, roleRow);
_role = LuneUi.Label("", "HudMuted", roleRow);
VisualElement xpRow = LuneUi.Box(false, 6f, _filled);
_xp = new LuneHpBar(60f, 6f) { Fill = LuneUiTheme.BronzeLight };
_xp.style.flexGrow = 1f;
xpRow.Add(_xp);
_xpText = LuneUi.Label("", "HudMuted", xpRow);
VisualElement hpRow = LuneUi.Box(false, 5f, _filled);
LuneUi.Glyph("heart", LuneUiTheme.Lightened(LuneUiTheme.Danger, .2f), 13f, hpRow);
_hp = LuneUi.Label("", "HudText", hpRow);
_hpBar = new LuneHpBar(50f, 5f);
_hpBar.style.flexGrow = 1f;
hpRow.Add(_hpBar);
VisualElement itemRow = LuneUi.Box(false, 4f, _filled);
for (int index = 0; index < _slots.Length; index++)
{
var box = new VisualElement { pickingMode = PickingMode.Ignore };
box.style.width = 26f;
box.style.height = 26f;
box.style.backgroundColor = new Color(0f, 0f, 0f, .35f);
box.style.borderTopLeftRadius = 3f;
box.style.borderTopRightRadius = 3f;
box.style.borderBottomLeftRadius = 3f;
box.style.borderBottomRightRadius = 3f;
box.style.borderLeftWidth = 1f;
box.style.borderRightWidth = 1f;
box.style.borderTopWidth = 1f;
box.style.borderBottomWidth = 1f;
Color edge = LuneUiTheme.Hex("33404d");
box.style.borderLeftColor = edge;
box.style.borderRightColor = edge;
box.style.borderTopColor = edge;
box.style.borderBottomColor = edge;
box.style.paddingLeft = 4f;
box.style.paddingTop = 4f;
itemRow.Add(box);
_slots[index] = LuneUi.Glyph("", Color.white, 18f, box);
}
_talent = new LuneStatusBadge();
itemRow.Add(_talent);
CommandSlot = new LuneButton("ULTIMATE", LuneButtons.HudPrimary, itemRow);
CommandSlot.Clicked += () => CommandRequested?.Invoke();
LuneUi.Show(CommandSlot, false);
_empty = LuneUi.Box(true, 3f, this);
_empty.style.marginLeft = 10f;
_empty.style.flexGrow = 1f;
_empty.style.justifyContent = Justify.Center;
_emptyTitle = LuneUi.Label("HELD ANWERBEN", "HudName", _empty);
_facesRow = LuneUi.Box(false, 4f, _empty);
foreach (string id in LuneContent.Heroes.Keys)
{
var face = new LunePortraitElement();
face.style.width = 38f;
face.style.height = 38f;
_facesRow.Add(face);
_faces.Add(face);
}
VisualElement price = LuneUi.Box(false, 4f, _empty);
_emptyCoin = LuneUi.Glyph("coin", LuneUiTheme.Gold, 14f, price);
_emptyPrice = LuneUi.Label("", "HudMuted", price);
LuneTooltip.Attach(this, Tip);
}
/// <summary>hero: the own hero (draft in planning) or null; live: the combat unit or null.</summary>
public void Apply(HeroState hero, LuneHudModel model, LuneUnit live)
{
_model = model;
bool hasHero = hero != null;
if (live != null && hasHero)
{
_hp.text = live.Alive ? LuneUiTheme.Number(live.Hp) + " / " + LuneUiTheme.Number(live.MaxHp) : "gefallen";
_hpBar.Fraction = live.Alive ? (float)(live.Hp / Math.Max(1.0, live.MaxHp)) : 0f;
_hpBar.Fill = LuneHpBar.HealthColor(_hpBar.Fraction, LuneUiTheme.Good);
_portrait.Crossed = !live.Alive;
}
string signature = (hasHero ? hero.HeroId + ":" + hero.Xp + ":" + hero.Lane + ":" + string.Join(",", hero.Items) + ":" + string.Join(",", hero.Talents) : "-") + "|"
+ (live != null) + "|" + model.OwnGold + "|" + model.Planning;
if (signature == _signature)
return;
_signature = signature;
LuneUi.Show(_filled, hasHero);
LuneUi.Show(_empty, !hasHero);
_hero = hero;
if (!hasHero)
{
HeroId = "";
_definition = null;
_portrait.ShowModel("w04");
_portrait.Dimmed = true;
_portrait.Crossed = false;
int index = 0;
foreach (HeroSpec spec in LuneContent.Heroes.Values)
_faces[index++].ShowModel(spec.Model);
// In combat nobody can recruit: say so instead of offering it.
bool planning = model.Planning;
_emptyTitle.text = planning ? "HELD ANWERBEN" : "KEIN HELD";
_emptyPrice.text = planning ? LuneContent.Heroes.Values[0].Cost + " · ein Held pro Spieler" : "Anwerben in der Planung";
LuneUi.Show(_emptyCoin, planning);
LuneUi.Show(_facesRow, planning);
LuneUi.Show(CommandSlot, false);
return;
}
HeroId = hero.HeroId;
HeroSpec heroSpec = LuneContent.Heroes[HeroId];
_definition = model.HeroDefinition(hero);
_portrait.Dimmed = false;
_portrait.Crossed = live != null && !live.Alive;
_portrait.ShowModel(heroSpec.Model);
_name.text = heroSpec.Name;
int level = LuneContent.LevelFor(hero.Xp);
_level.text = "STUFE " + level;
_roleGlyph.Glyph = LuneStats.RoleGlyph(_definition.Role);
_role.text = LuneStats.RoleName(_definition.Role);
int[] levels = LuneContent.LevelXp;
int low = levels[level - 1];
bool top = level >= levels.Length;
int high = levels[Math.Min(level, levels.Length - 1)];
_xp.Fraction = top ? 1f : (float)(hero.Xp - low) / Math.Max(1, high - low);
_xpText.text = top ? "EP " + hero.Xp + " · max" : "EP " + hero.Xp + " / " + high;
LuneUi.Show(_hpBar, live != null);
if (live == null)
_hp.text = LuneUiTheme.Number(_definition.MaxHp) + " Leben";
for (int index = 0; index < _slots.Length; index++)
{
string itemId = index < hero.Items.Count ? hero.Items[index] : "";
_slots[index].Glyph = itemId;
_slots[index].Color = LuneItemSlot.ColorOf(itemId);
}
int openTier = hero.Talents.Count;
int[] talentLevels = LuneContent.TalentLevels;
if (openTier < talentLevels.Length && level >= talentLevels[openTier])
{
_talent.SetState("TALENT!", LuneUiTheme.Hero, "star", true);
LuneUi.Show(_talent, model.Planning);
}
else if (openTier < talentLevels.Length)
{
_talent.SetState("TALENT AB " + talentLevels[openTier], LuneUiTheme.Muted);
LuneUi.Show(_talent, true);
}
else
{
LuneUi.Show(_talent, false);
}
// The three reference heroes have no command ultimate; the planned heroes will.
PlannedHero planned = PlannedHeroes.All.FirstOrDefault(p => p.Active && p.Id == HeroId);
bool command = planned != null && planned.Ultimate.Trigger == PowerTrigger.Command;
LuneUi.Show(CommandSlot, command);
if (command)
CommandSlot.Text = planned.Ultimate.Name.ToUpperInvariant();
}
LuneTip Tip()
{
if (_hero == null || _definition == null)
return new LuneTip
{
Title = "Held anwerben", Glyph = "crown", GlyphColor = LuneUiTheme.Hero,
Text = "Drei Helden stehen zur Wahl; jeder Spieler führt einen. Helden sammeln Erfahrung, steigen bis Stufe 10 auf, tragen drei Items und wählen zwei Talente.",
Note = "Klick: Heldenauswahl öffnen",
};
HeroSpec spec = LuneContent.Heroes[_hero.HeroId];
List<string> names = _hero.Items.Select(id => LuneContent.Items[id].Name).ToList();
var tip = new LuneTip
{
Title = spec.Name + " · Stufe " + LuneContent.LevelFor(_hero.Xp), Glyph = "crown", GlyphColor = LuneUiTheme.Hero, Subtitle = spec.Title,
Text = "Items: " + (names.Count > 0 ? string.Join(", ", names) : "keine"), Note = "Klick: Werte, Items und Talente · H: Kamera zum Helden",
};
tip.Stats.AddRange(LuneStats.Rows(_definition));
return tip;
}
}
/// <summary>
/// Hero window content (Godot LuneHeroDetail). Without a hero: the three heroes with portrait,
/// role, real stats and price to recruit one. With a hero: portrait, level and experience, lane
/// order, and tabs for values and ability (from the rules' definitions, including level, items
/// and talents), the three item slots with the item shop, and the two talent tiers. Only shows
/// data; every change is a command through the game.
/// </summary>
public sealed class LuneHeroDetail : VisualElement
{
public const int TabValues = 0;
public const int TabItems = 1;
public const int TabTalents = 2;
public event Action<Command> CommandRequested;
public int Tab = TabValues;
public readonly Dictionary<string, LuneItemSlot> ItemCards = new Dictionary<string, LuneItemSlot>();
public readonly Dictionary<string, LuneButton> RecruitButtons = new Dictionary<string, LuneButton>();
public readonly Dictionary<string, LuneButton> LaneButtons = new Dictionary<string, LuneButton>();
public readonly List<LuneButton> TalentCards = new List<LuneButton>();
public LuneTabBar Tabs;
LuneHudModel _model;
string _signature = "";
public LuneHeroDetail()
{
pickingMode = PickingMode.Ignore;
style.flexGrow = 1f;
}
public void ShowTab(int index)
{
Tab = index;
_signature = "";
if (_model != null)
Apply(_model);
}
public string TitleText(LuneHudModel model)
{
HeroState hero = model.OwnHero;
return hero == null ? "HELD ANWERBEN" : "HELD · " + LuneContent.Heroes[hero.HeroId].Name.ToUpperInvariant();
}
public void Apply(LuneHudModel model)
{
_model = model;
HeroState hero = model.OwnHero;
string signature = (hero != null ? hero.HeroId + ":" + hero.Xp + ":" + hero.Lane + ":" + hero.Fresh + ":" + string.Join(",", hero.Items) + ":" + string.Join(",", hero.Talents) + ":" + string.Join(",", hero.FreshItems) : "-")
+ "|" + model.OwnGold + "|" + model.Planning + "|" + model.Locked + "|" + Tab + "|" + model.State.GrottoAvailable() + "|" + model.State.RoundNumber;
if (signature == _signature)
return;
_signature = signature;
Clear();
ItemCards.Clear();
RecruitButtons.Clear();
LaneButtons.Clear();
TalentCards.Clear();
Tabs = null;
if (hero == null)
Recruit(model);
else
ShowHero(model, hero);
}
static bool CanAct(LuneHudModel model) { return model.Planning && !model.Locked; }
static string WhyNot(LuneHudModel model)
{
if (!model.Planning)
return "Nur in der Planung.";
if (model.Locked)
return "Bereit gemeldet – die Planung ist verbindlich.";
return "";
}
void Recruit(LuneHudModel model)
{
LuneUi.Wrap(LuneUi.Label("Jeder Spieler führt einen Helden. Er kämpft auf seiner Lane mit, sammelt Erfahrung aus Gefechten in seiner Nähe und aus jeder Runde, trägt drei Items und wählt ab Stufe 3 und 6 je ein Talent.", "HudMuted", this));
VisualElement row = LuneUi.Box(false, 12f, this);
row.style.alignItems = Align.Stretch;
row.style.flexGrow = 1f;
row.style.marginTop = 10f;
int gold = model.OwnGold;
foreach (KeyValuePair<string, HeroSpec> pair in LuneContent.Heroes)
{
string heroId = pair.Key;
HeroSpec spec = pair.Value;
var panel = new LunePanel(LunePanels.HudTight);
panel.style.flexGrow = 1f;
panel.style.flexBasis = 0f;
row.Add(panel);
VisualElement column = LuneUi.Box(true, 5f, panel);
column.style.flexGrow = 1f;
var portrait = new LunePortraitElement();
portrait.style.height = 170f;
column.Add(portrait);
portrait.ShowModel(spec.Model, true);
LuneUi.Label(spec.Name, "HudTitle", column);
LuneUi.Label(spec.Title, "HudMuted", column);
UnitDef definition = model.HeroDefinition(new HeroState { HeroId = heroId });
VisualElement role = LuneUi.Box(false, 5f, column);
LuneUi.Glyph(LuneStats.RoleGlyph(definition.Role), LuneUiTheme.BronzeLight, 15f, role);
LuneUi.Label(spec.RoleText, "HudText", role);
foreach (string[] entry in LuneStats.Rows(definition).Take(5))
{
VisualElement line = LuneUi.Box(false, 6f, column);
LuneUi.Glyph(entry[0], LuneUiTheme.Muted, 13f, line);
LuneUi.Label(entry[1], "HudMuted", line).style.minWidth = 104f;
LuneUi.Clip(LuneUi.Label(entry[2], "HudText", line)).style.flexGrow = 1f;
}
Label summary = LuneUi.Wrap(LuneUi.Label(spec.Summary, "HudText", column));
summary.style.flexGrow = 1f;
int cost = spec.Cost;
var buy = new LuneButton("ANWERBEN · " + cost + " G", LuneButtons.HudPrimary, column);
string why = WhyNot(model);
if (why.Length == 0 && gold < cost)
why = "Es fehlen " + (cost - gold) + " Gold.";
buy.Disabled = why.Length > 0;
buy.Clicked += () => CommandRequested?.Invoke(Command.HeroBuy(heroId));
RecruitButtons[heroId] = buy;
if (why.Length > 0)
LuneUi.SetColor(LuneUi.Wrap(LuneUi.Label(why, "HudMuted", column)), gold < cost ? LuneUiTheme.Danger : LuneUiTheme.Muted);
LuneUi.Label("Vorläufige Rolle des vorhandenen Modells.", "HudMuted", column);
}
}
void ShowHero(LuneHudModel model, HeroState hero)
{
HeroSpec spec = LuneContent.Heroes[hero.HeroId];
UnitDef definition = model.HeroDefinition(hero);
bool canAct = CanAct(model);
VisualElement body = LuneUi.Box(false, 18f, this);
body.style.alignItems = Align.Stretch;
body.style.flexGrow = 1f;
VisualElement left = LuneUi.Box(true, 6f, body);
left.style.width = 290f;
var portrait = new LunePortraitElement();
portrait.style.width = 290f;
portrait.style.height = 230f;
left.Add(portrait);
portrait.ShowModel(spec.Model, true);
LuneUi.Label(spec.Title, "HudMuted", left);
VisualElement role = LuneUi.Box(false, 6f, left);
LuneUi.Glyph(LuneStats.RoleGlyph(definition.Role), LuneUiTheme.BronzeLight, 16f, role);
LuneUi.Label(spec.RoleText, "HudText", role);
int level = LuneContent.LevelFor(hero.Xp);
int[] levels = LuneContent.LevelXp;
VisualElement levelRow = LuneUi.Box(false, 6f, left);
LuneUi.Glyph("star", LuneUiTheme.Hero, 16f, levelRow);
LuneUi.Label("STUFE " + level + " / " + levels.Length, "HudValue", levelRow);
var bar = new LuneHpBar(0f, 8f) { Fill = LuneUiTheme.BronzeLight };
left.Add(bar);
bool top = level >= levels.Length;
int low = levels[level - 1];
int high = levels[Math.Min(level, levels.Length - 1)];
bar.Fraction = top ? 1f : (float)(hero.Xp - low) / Math.Max(1, high - low);
LuneUi.Label(top ? hero.Xp + " EP · Höchststufe" : hero.Xp + " / " + high + " EP · noch " + (high - hero.Xp) + " bis Stufe " + (level + 1), "HudMuted", left);
LuneUi.Label("EINSATZ", "HudCaps", left);
VisualElement lanes = LuneUi.Box(false, 4f, left);
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, lanes);
button.style.flexGrow = 1f;
button.On = hero.Lane == lane;
button.Disabled = !canAct || (lane == LuneMatch.Grotto && !model.State.GrottoAvailable());
string uid = hero.Uid;
string captured = lane;
button.Clicked += () => CommandRequested?.Invoke(Command.Assign(uid, captured));
LaneButtons[lane] = button;
}
if (hero.Lane == LuneMatch.Grotto)
LuneUi.SetColor(LuneUi.Wrap(LuneUi.Label("Der Held fehlt seiner Lane in der nächsten Kampfphase.", "HudText", left)), LuneUiTheme.Warn);
if (hero.Fresh && model.Planning)
{
int refund = hero.Invested + hero.Items.Sum(id => LuneContent.Items[id].Cost);
// Godot: a plain button in the column fills its width.
var cancel = new LuneButton("Held zurückgeben +" + refund, LuneButtons.HudButton, left);
cancel.Disabled = !canAct;
cancel.Clicked += () => CommandRequested?.Invoke(Command.HeroCancel());
}
VisualElement right = LuneUi.Box(true, 8f, body);
right.style.flexGrow = 1f;
right.style.flexShrink = 1f;
right.style.minWidth = 0f;
Tabs = new LuneTabBar();
right.Add(Tabs);
Tabs.AddTab("WERTE & FÄHIGKEIT");
Tabs.AddTab("ITEMS " + hero.Items.Count + "/" + LuneContent.ItemSlots);
int openTier = hero.Talents.Count;
int[] talentLevels = LuneContent.TalentLevels;
bool talentReady = openTier < talentLevels.Length && level >= talentLevels[openTier];
LuneButton talents = Tabs.AddTab("TALENTE");
if (talentReady)
LuneUi.Glyph("star", LuneUiTheme.Hero, 13f, talents).style.marginLeft = 5f;
Tabs.Select(Tab);
Tabs.TabChanged += ShowTab;
VisualElement page = LuneUi.Box(true, 8f, right);
page.style.flexGrow = 1f;
if (Tab == TabItems)
Items(model, hero, level, page);
else if (Tab == TabTalents)
Talents(model, hero, level, page);
else
Values(hero, definition, spec, page);
}
static void Values(HeroState hero, UnitDef definition, HeroSpec spec, VisualElement page)
{
List<string[]> rows = LuneStats.Rows(definition, true);
rows.Add(new[] { "sword", "Schaden/Sekunde", "≈ " + LuneUiTheme.Decimal(LuneStats.Dps(definition)) + " (ohne Fähigkeit)" });
VisualElement grid = LuneUi.Box(true, 3f, page);
foreach (string[] entry in rows)
{
VisualElement line = LuneUi.Box(false, 10f, grid);
LuneUi.Glyph(entry[0], LuneUiTheme.Muted, 15f, line);
LuneUi.Label(entry[1], "HudMuted", line).style.minWidth = 130f;
LuneUi.Wrap(LuneUi.Label(entry[2], "HudText", line)).style.flexGrow = 1f;
}
UnitDef baseDef = spec.Base;
LuneUi.Wrap(LuneUi.Label("Grundwerte " + LuneUiTheme.Number(baseDef.MaxHp) + " Leben, " + LuneUiTheme.Decimal(baseDef.Damage) + " Schaden; je Stufe +" + LuneUiTheme.Round(LuneContent.HpPerLevel * 100.0)
+ " % Leben und +" + LuneUiTheme.Round(LuneContent.DamagePerLevel * 100.0) + " % Schaden (jetzt Stufe " + definition.Level + ").", "HudMuted", page));
LuneUi.Label("FÄHIGKEIT", "HudCaps", page);
LuneUi.Wrap(LuneUi.Label(DataCatalog.GetUnit(spec.Model).AbilityDescription ?? "", "HudText", page));
var chosen = new List<string>();
TalentOption[][] options = LuneContent.TalentsOf(hero.HeroId);
for (int tier = 0; tier < hero.Talents.Count; tier++)
{
foreach (TalentOption option in options[tier])
{
if (option.Id == hero.Talents[tier])
chosen.Add(option.Name + ": " + option.Text);
}
}
if (chosen.Count > 0)
{
LuneUi.Label("TALENTE", "HudCaps", page);
LuneUi.Wrap(LuneUi.Label(string.Join("\n", chosen), "HudText", page));
}
LuneUi.Wrap(LuneUi.Label("Erfahrung: Tode von Gegnern in " + (int)LuneContent.XpRadius + " m (Monster 12 + Welle, Champion 30 + 2 × Welle, Gebäude " + LuneContent.BuildingXp + ", Boss " + LuneContent.BossXp
+ ") und je Kampf " + LuneContent.XpParticipation + " + " + LuneContent.XpPerRound + " × Welle.", "HudMuted", page));
}
void Items(LuneHudModel model, HeroState hero, int level, VisualElement page)
{
bool canAct = CanAct(model);
List<string> items = hero.Items;
VisualElement slots = LuneUi.Box(false, 10f, page);
slots.style.alignItems = Align.FlexStart;
for (int index = 0; index < LuneContent.ItemSlots; index++)
{
VisualElement box = LuneUi.Box(true, 3f, slots);
box.style.flexGrow = 1f;
box.style.flexBasis = 0f;
var slot = new LuneItemSlot(true);
slot.style.width = 46f;
slot.style.height = 46f;
box.Add(slot);
if (index < items.Count)
{
string itemId = items[index];
ItemSpec item = LuneContent.Items[itemId];
slot.SetItem(itemId, LuneItemSlot.State.Equipped);
LuneTooltip.Attach(slot, () => new LuneTip { Title = item.Name, Glyph = itemId, GlyphColor = LuneItemSlot.ColorOf(itemId), Text = item.Text });
LuneUi.Label(item.Name, "HudText", box);
bool fresh = hero.FreshItems.Contains(itemId);
int refund = fresh ? item.Cost : (int)Math.Floor(item.Cost * LuneContent.ItemRefund);
var sell = new LuneButton("Verkaufen +" + refund, LuneButtons.HudButton, box);
sell.style.alignSelf = Align.FlexStart;
sell.Disabled = !canAct;
sell.Clicked += () => CommandRequested?.Invoke(Command.ItemSell(itemId));
}
else
{
slot.SetItem("", LuneItemSlot.State.Empty);
LuneUi.Label("Platz " + (index + 1) + " frei", "HudMuted", box);
}
}
LuneUi.Label("ITEMS · einzigartig je Held, bleiben dauerhaft, Rückgabe " + LuneUiTheme.Round(LuneContent.ItemRefund * 100.0) + " %", "HudCaps", page);
VisualElement grid = LuneUi.Box(true, 8f, page);
int gold = model.OwnGold;
VisualElement line = null;
int column = 0;
foreach (KeyValuePair<string, ItemSpec> pair in LuneContent.Items)
{
string itemId = pair.Key;
ItemSpec item = pair.Value;
if (column % 2 == 0)
line = LuneUi.Box(false, 8f, grid);
column++;
var card = new LuneItemSlot(false);
card.style.flexGrow = 1f;
card.style.flexBasis = 0f;
line.Add(card);
LuneItemSlot.State state = LuneItemSlot.State.Available;
if (items.Contains(itemId))
state = LuneItemSlot.State.Unique;
else if (!canAct)
state = LuneItemSlot.State.Locked;
else if (level < item.MinLevel)
state = LuneItemSlot.State.Incompatible;
else if (items.Count >= LuneContent.ItemSlots)
state = LuneItemSlot.State.Full;
else if (gold < item.Cost)
state = LuneItemSlot.State.Unaffordable;
card.SetItem(itemId, state);
ItemCards[itemId] = card;
string uid = hero.Uid;
card.Clicked += () =>
{
if (card.Current == LuneItemSlot.State.Available)
CommandRequested?.Invoke(Command.ItemBuy(itemId, uid));
};
string why = state == LuneItemSlot.State.Unique ? "Dein Held trägt es bereits; Items sind einzigartig."
: state == LuneItemSlot.State.Locked ? WhyNot(model)
: state == LuneItemSlot.State.Incompatible ? "Erfordert Heldenstufe " + item.MinLevel + "."
: state == LuneItemSlot.State.Full ? "Alle " + LuneContent.ItemSlots + " Plätze belegt; verkaufe zuerst ein Item."
: state == LuneItemSlot.State.Unaffordable ? "Es fehlen " + (item.Cost - gold) + " Gold."
: "Klick: kaufen und ausrüsten.";
LuneTooltip.Attach(card, () => new LuneTip { Title = item.Name + " · " + item.Cost + " Gold", Glyph = itemId, GlyphColor = LuneItemSlot.ColorOf(itemId), Text = item.Text, Note = why });
}
}
void Talents(LuneHudModel model, HeroState hero, int level, VisualElement page)
{
bool canAct = CanAct(model);
List<string> talents = hero.Talents;
int[] talentLevels = LuneContent.TalentLevels;
TalentOption[][] options = LuneContent.TalentsOf(hero.HeroId);
for (int tier = 0; tier < talentLevels.Length; tier++)
{
int required = talentLevels[tier];
string state = tier < talents.Count ? "gewählt" : level >= required && tier == talents.Count ? "jetzt wählbar" : "ab Stufe " + required;
LuneUi.Label("TALENT " + (tier + 1) + " · STUFE " + required + " · " + state.ToUpperInvariant(), "HudCaps", page);
VisualElement row = LuneUi.Box(false, 8f, page);
row.style.alignItems = Align.Stretch;
for (int choice = 0; choice < options[tier].Length; choice++)
{
TalentOption option = options[tier][choice];
var card = new LuneButton(null, LuneButtons.HudCard, row);
card.style.flexGrow = 1f;
card.style.flexBasis = 0f;
card.style.minHeight = 96f;
card.style.flexDirection = FlexDirection.Column;
card.style.alignItems = Align.Stretch;
card.style.justifyContent = Justify.FlexStart;
card.style.paddingLeft = 10f;
card.style.paddingRight = 10f;
card.style.paddingTop = 8f;
card.style.paddingBottom = 8f;
LuneUi.Label(option.Name, "HudValue", card);
LuneUi.Wrap(LuneUi.Label(option.Text, "HudMuted", card));
bool chosen = tier < talents.Count && talents[tier] == option.Id;
card.On = chosen;
card.Disabled = !(canAct && tier == talents.Count && level >= required) && !chosen;
int capturedTier = tier;
int capturedChoice = choice;
card.Clicked += () =>
{
if (!chosen)
CommandRequested?.Invoke(Command.Talent(capturedTier, capturedChoice));
};
TalentCards.Add(card);
}
}
LuneUi.Wrap(LuneUi.Label("Die Wahl ist endgültig; jede Stufe bietet zwei Richtungen (Einzelziel oder Fläche, Schutz oder Druck).", "HudMuted", page));
}
}
}