champions-of-luneraria/Assets/Luneraria/UI/Hud/LuneHeroPanels.cs
Martin Stickelbroeck 8e4b52f4ab S1 Strategie-Regeln nach Legion TD 2, Dota 2 und League of Legends
Hinter MatchOptions.Extended (Standard im Spiel, Schalter im Startbildschirm):
Söldner-Einkommen (Angriffschampion +Preis/12 Einkommen dauerhaft), Kasernen
(gefallener innerer Turm: Gegnerwellen +25 % und ein Monster mehr), Erschöpfung
und Rückkauf (60 % Leben, Rückkauf 60 + 20 x Stufe), Mondsegen (Grottensieg
+4 % je Stapel, Mondseele bei 4). Bots wählen mit erweiterten Regeln auch
Sylvara und kaufen Helden zurück. Netz überträgt Regeln und neue Zustände.
HUD: Einkommen, Kasernen-Symbol, Rückkauf, Segen im Teamkopf.
Kern 531/531 (s1 31), EditMode 31/31, PlayMode 16/16, Verify 718/0,
Selbsttest 30/30, Golden 0 Abweichungen, Fingerabdruck unverändert.

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

733 lines
38 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. H1: the command slot shows the ultimate of a hero that has one (its
/// planned trigger rule, in combat whether it fired) and opens the hero window on it.
/// </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;
LuneUnit _live;
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;
_live = live;
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) + ":" + hero.Ultimate + ":" + hero.Skin + ":" + hero.Exhausted : "-") + "|"
+ (live != null) + "|" + (live != null ? live.UltimateSerial : 0) + "|" + 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(string.IsNullOrEmpty(hero.Skin) ? heroSpec.Model : heroSpec.Model + "#" + hero.Skin);
_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" + (hero.Exhausted && model.State.Extended ? " · ERSCHÖPFT" : "");
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);
}
// H1: heroes with an ultimate show it with its planned rule; the reference heroes have none.
bool command = LuneContent.HasUltimate(HeroId);
LuneUi.Show(CommandSlot, command);
if (command)
{
string rule = LuneContent.UltimateRuleFor(hero);
string ruleName = LuneContent.UltimateRulesOf(HeroId).First(r => r.Id == rule).Name;
bool fired = _live != null && _live.UltimateSerial > 0;
// Short: the slot shares the row with the item slots and the talent badge.
CommandSlot.Text = fired ? "ULT GENUTZT" : "ULT · " + ruleName.ToUpperInvariant();
CommandSlot.On = fired;
}
}
LuneTip Tip()
{
if (_hero == null || _definition == null)
return new LuneTip
{
Title = "Held anwerben", Glyph = "crown", GlyphColor = LuneUiTheme.Hero,
Text = LuneContent.Heroes.Count + " 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;
/// <summary>H1: ultimate rule and skin of heroes that have them.</summary>
public const int TabUltimate = 3;
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 readonly Dictionary<string, LuneButton> RuleCards = new Dictionary<string, LuneButton>();
public readonly Dictionary<string, LuneButton> SkinCards = new Dictionary<string, LuneButton>();
/// <summary>S1: buys an exhausted hero back (null when not offered).</summary>
public LuneButton BuybackButton;
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) + ":" + hero.Ultimate + ":" + hero.Skin + ":" + hero.Exhausted : "-")
+ "|" + 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();
RuleCards.Clear();
SkinCards.Clear();
BuybackButton = null;
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);
if (spec.Provisional)
LuneUi.Wrap(LuneUi.Label("Vorläufige Rolle des vorhandenen Modells.", "HudMuted", column));
else if (spec.UltimateName.Length > 0)
LuneUi.SetColor(LuneUi.Label("Ultimate: " + spec.UltimateName, "HudText", column), LuneUiTheme.Hero);
}
}
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(string.IsNullOrEmpty(hero.Skin) ? spec.Model : spec.Model + "#" + hero.Skin, 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);
// S1 (Dota 2 buyback, adapted): a fallen hero starts exhausted unless bought back.
if (hero.Exhausted && model.State.Extended)
{
int percent = (int)Math.Round(model.State.Balance.ExhaustedHp * 100.0);
LuneUi.SetColor(LuneUi.Wrap(LuneUi.Label("ERSCHÖPFT: gefallen, startet mit " + percent + " % Leben.", "HudText", left)), LuneUiTheme.Warn);
int cost = model.State.BuybackCost(hero);
BuybackButton = new LuneButton("Rückkauf · " + cost + " G", LuneButtons.HudPrimary, left);
BuybackButton.Disabled = !canAct || model.OwnGold < cost;
BuybackButton.Clicked += () => CommandRequested?.Invoke(Command.Buyback());
}
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;
bool special = LuneContent.HasUltimate(hero.HeroId) || LuneContent.SkinsOf(hero.HeroId).Length > 0;
if (special)
Tabs.AddTab("ULTIMATE & SKIN");
if (Tab == TabUltimate && !special)
Tab = TabValues;
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 if (Tab == TabUltimate)
Ultimate(model, hero, spec, 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);
// H1 heroes bring their own kit text; the reference heroes use their troop model's ability.
string ability = spec.AbilityText.Length > 0 ? spec.AbilityText : DataCatalog.GetUnit(spec.Model).AbilityDescription ?? "";
if (spec.PassiveText.Length > 0)
ability = spec.PassiveText + "\n" + ability;
if (spec.UltimateName.Length > 0)
ability += "\nUltimate " + spec.UltimateName + ": " + spec.UltimateText;
LuneUi.Wrap(LuneUi.Label(ability, "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));
}
/// <summary>
/// H1: the ultimate and its trigger rule (one charge per combat, released automatically
/// when the rule holds; the rule can change in every planning), and the cosmetic skins.
/// </summary>
void Ultimate(LuneHudModel model, HeroState hero, HeroSpec spec, VisualElement page)
{
bool canAct = CanAct(model);
TalentOption[] rules = LuneContent.UltimateRulesOf(hero.HeroId);
if (rules.Length > 0)
{
LuneUi.Label("ULTIMATE · " + spec.UltimateName.ToUpperInvariant(), "HudCaps", page);
LuneUi.Wrap(LuneUi.Label(spec.UltimateText, "HudText", page));
LuneUi.Label("AUSLÖSER · jederzeit in der Planung änderbar, kostenlos", "HudCaps", page);
string current = LuneContent.UltimateRuleFor(hero);
VisualElement row = LuneUi.Box(false, 8f, page);
row.style.alignItems = Align.Stretch;
foreach (TalentOption rule in rules)
{
var card = new LuneButton(null, LuneButtons.HudCard, row);
card.style.flexGrow = 1f;
card.style.flexBasis = 0f;
card.style.minHeight = 84f;
card.style.flexDirection = FlexDirection.Column;
card.style.alignItems = Align.Stretch;
card.style.justifyContent = Justify.FlexStart;
card.style.paddingLeft = 8f;
card.style.paddingRight = 8f;
card.style.paddingTop = 6f;
card.style.paddingBottom = 6f;
LuneUi.Label(rule.Name + (rule == rules[0] ? " (Standard)" : ""), "HudValue", card);
LuneUi.Wrap(LuneUi.Label(rule.Text, "HudMuted", card));
bool on = rule.Id == current;
card.On = on;
card.Disabled = !canAct && !on;
string id = rule.Id;
card.Clicked += () =>
{
if (!on)
CommandRequested?.Invoke(Command.UltimateRule(id));
};
RuleCards[id] = card;
}
}
TalentOption[] skins = LuneContent.SkinsOf(hero.HeroId);
if (skins.Length == 0)
return;
LuneUi.Label("SKIN · nur das Aussehen, für alle sichtbar", "HudCaps", page);
VisualElement looks = LuneUi.Box(false, 8f, page);
looks.style.alignItems = Align.Stretch;
var options = new List<TalentOption> { new TalentOption("", "Mondsilber", "Standard-Aussehen.") };
options.AddRange(skins);
foreach (TalentOption skin in options)
{
var card = new LuneButton(null, LuneButtons.HudCard, looks);
card.style.width = 170f;
card.style.flexDirection = FlexDirection.Column;
card.style.alignItems = Align.Stretch;
card.style.paddingLeft = 6f;
card.style.paddingRight = 6f;
card.style.paddingTop = 6f;
card.style.paddingBottom = 6f;
var face = new LunePortraitElement();
face.style.height = 120f;
face.pickingMode = PickingMode.Ignore;
card.Add(face);
face.ShowModel(skin.Id.Length == 0 ? spec.Model : spec.Model + "#" + skin.Id, true);
LuneUi.Label(skin.Name, "HudValue", card);
bool on = (hero.Skin ?? "") == skin.Id;
card.On = on;
card.Disabled = !canAct && !on;
string id = skin.Id;
card.Clicked += () =>
{
if (!on)
CommandRequested?.Invoke(Command.HeroSkin(id));
};
LuneTooltip.Attach(card, () => new LuneTip { Title = skin.Name, Glyph = "crown", GlyphColor = LuneUiTheme.Hero, Text = skin.Text });
SkinCards[id] = card;
}
}
}
}