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>
609 lines
31 KiB
C#
609 lines
31 KiB
C#
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);
|
||
var cancel = new LuneButton("Held zurückgeben +" + refund, LuneButtons.HudButton, left);
|
||
cancel.style.alignSelf = Align.FlexStart;
|
||
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));
|
||
}
|
||
}
|
||
}
|