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>
330 lines
18 KiB
C#
330 lines
18 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|