Hinter MatchOptions.Extended: Glyphe als Teamwahl einer Lane (Türme 5 s unverwundbar, sobald einer unter 50 % fällt; 4 Runden Abklingzeit nur nach Auslösen), vier Rezepte (Bollwerk, Glutsturm, Quellherz, Splitterkrone), Ancient-Stufen 1-3 (150 x Stufe, Reparatur 1.000, Schuss 45/75/110), Mid-Bots jagen bei sicherer Lane die Grotte. Teampanel mit Glyphe und Ancient, Rezepte im Shop (TEILE FEHLEN), Glyphen-Ring und Ancient-Schüsse. Kern 566/566 (s2 35), EditMode 32/32, PlayMode 17/17, Verify 718/0, Selbsttest 30/30, Golden 0, Fingerabdruck unverändert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
742 lines
39 KiB
C#
742 lines
39 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. 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;
|
||
// S2 recipes exist only under the extended rules.
|
||
if (item.IsRecipe && !model.State.Extended)
|
||
continue;
|
||
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;
|
||
bool partsMissing = item.IsRecipe && !item.Parts.All(items.Contains);
|
||
int price = item.IsRecipe ? item.RecipeFee : item.Cost;
|
||
if (items.Contains(itemId))
|
||
state = LuneItemSlot.State.Unique;
|
||
else if (!canAct)
|
||
state = LuneItemSlot.State.Locked;
|
||
else if (partsMissing)
|
||
state = LuneItemSlot.State.MissingParts;
|
||
else if (level < item.MinLevel)
|
||
state = LuneItemSlot.State.Incompatible;
|
||
else if (!item.IsRecipe && items.Count >= LuneContent.ItemSlots)
|
||
state = LuneItemSlot.State.Full;
|
||
else if (gold < price)
|
||
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.MissingParts ? "Braucht " + string.Join(" und ", item.Parts.Select(id => LuneContent.Items[id].Name)) + " im Inventar."
|
||
: 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 " + (price - gold) + " Gold."
|
||
: item.IsRecipe ? "Klick: schmieden (beide Teile werden eins, ein Platz wird frei)." : "Klick: kaufen und ausrüsten.";
|
||
string title = item.IsRecipe ? item.Name + " · Rezept +" + item.RecipeFee + " Gold" : item.Name + " · " + item.Cost + " Gold";
|
||
LuneTooltip.Attach(card, () => new LuneTip { Title = title, 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;
|
||
}
|
||
}
|
||
}
|
||
}
|