champions-of-luneraria/Assets/Luneraria/UI/Hud/LuneArmyPanels.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

330 lines
18 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>
/// 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.
/// </summary>
public sealed class LuneChampionPanel : VisualElement
{
public event Action<Command> CommandRequested;
public string TargetLane = "mid";
public readonly Dictionary<string, LuneUnitCard> Cards = new Dictionary<string, LuneUnitCard>();
public readonly Dictionary<string, LuneButton> LaneButtons = new Dictionary<string, LuneButton>();
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<string, LuneButton> 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<string, ChampionSpec> 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<LuneHudModel.BookingView> 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<string, LuneUnitCard> 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;
}
}
/// <summary>
/// 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.
/// </summary>
public sealed class LuneUnitDetail : LunePanel
{
public const string GrottoWarning = "Diese Einheit fehlt deiner Lane in der nächsten Kampfphase.";
public event Action<Command> 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<string, LuneButton> LaneButtons = new Dictionary<string, LuneButton>();
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));
}
/// <summary>entry: own roster entry; live: the unit in combat or null.</summary>
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<string[]> 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<string, LuneButton> 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;
}
}
}