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 " + (_model != null ? _model.State.ChampionBountyFor(definition.Bounty) : 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; } } }