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