using System;
using System.Collections.Generic;
using System.Linq;
using Luneraria.Core;
using UnityEngine;
using UnityEngine.UIElements;
namespace Luneraria.UI
{
///
/// One lane in the lane overview (Godot LuneLaneIndicator): name and hotkey, the threat level
/// (text, symbol and colour), the tower chain from the own base to the enemy base, the two lane
/// players with their heroes, announced champions or the boss, and in combat the unit count of
/// both sides. A click moves the camera. The threat rises at once but only falls after a short
/// hold, so it does not flicker.
///
public sealed class LuneLaneIndicator : LuneButton
{
public const float Hold = 2.5f;
public readonly string Lane;
public readonly string Hotkey;
public int Level { get; private set; }
/// The threat badge pulses (a lane just turned critical; off with reduced motion).
public bool Pulsing { get { return _pulse > 0f; } }
public LuneHudModel.LaneInfo Data { get; private set; }
public readonly LuneTowerIndicator[] Towers = new LuneTowerIndicator[4];
readonly LuneStatusBadge _badge;
readonly Label _own;
readonly Label _enemy;
readonly LunePortraitElement _ownHero;
readonly LunePortraitElement _enemyHero;
readonly Label _army;
readonly VisualElement _dangers;
readonly LuneGlyphElement _mine;
readonly LuneGlyphElement _ownShape;
readonly LuneGlyphElement _enemyShape;
float _lowerHold;
float _pulse;
string _signature = "";
public LuneLaneIndicator(string lane, string hotkey) : base(null, LuneButtons.HudRow)
{
Lane = lane;
Hotkey = hotkey;
style.minHeight = 92f;
style.flexDirection = FlexDirection.Column;
style.alignItems = Align.Stretch;
style.justifyContent = Justify.FlexStart;
style.paddingLeft = 10f;
style.paddingRight = 9f;
style.paddingTop = 6f;
style.paddingBottom = 6f;
var column = (LuneBox)LuneUi.Box(true, 3f, this);
VisualElement head = LuneUi.Box(false, 6f, column);
LuneUi.Glyph("lane_" + lane, LuneUiTheme.BronzeLight, 17f, head);
LuneUi.Label(LuneUi.Upper(LuneMap.LaneNames[lane]), "HudLane", head);
_mine = LuneUi.Glyph("star", LuneUiTheme.BronzeLight, 12f, head);
LuneUi.Show(_mine, false);
LuneUi.Spacer(head);
_badge = new LuneStatusBadge();
head.Add(_badge);
VisualElement chain = LuneUi.Box(false, 3f, column);
_ownShape = LuneUi.Glyph("emblem_0", LuneUiTheme.Team[0], 13f, chain);
for (int index = 0; index < 4; index++)
{
var tower = new LuneTowerIndicator();
chain.Add(tower);
Towers[index] = tower;
int captured = index;
LuneTooltip.Attach(tower, () => TowerTip(captured));
if (index == 1)
{
VisualElement middle = LuneUi.Box(false, 4f, chain);
middle.style.flexGrow = 1f;
middle.style.justifyContent = Justify.Center;
_army = LuneUi.Label("", "HudValue", middle);
_dangers = LuneUi.Box(false, 2f, middle);
}
}
_enemyShape = LuneUi.Glyph("emblem_1", LuneUiTheme.Team[1], 13f, chain);
VisualElement names = LuneUi.Box(false, 5f, column);
// Heroes of both lane players (Dota-style "who stands where"), committed heroes only.
_ownHero = new LunePortraitElement();
_ownHero.style.width = 22f;
_ownHero.style.height = 22f;
names.Add(_ownHero);
_own = LuneUi.Clip(LuneUi.Label("", "HudMuted", names));
_own.style.flexGrow = 1f;
_own.style.flexBasis = 0f;
_enemy = LuneUi.Clip(LuneUi.Label("", "HudMuted", names));
_enemy.style.flexGrow = 1f;
_enemy.style.flexBasis = 0f;
_enemy.style.unityTextAlign = TextAnchor.MiddleRight;
_enemyHero = new LunePortraitElement();
_enemyHero.style.width = 22f;
_enemyHero.style.height = 22f;
names.Add(_enemyHero);
LuneTooltip.Attach(this, Tip);
}
/// Applies a lane from LuneHudModel.Lane.
public void Apply(LuneHudModel.LaneInfo entry, int ownTeam, float delta, bool reducedMotion)
{
Data = entry;
int threat = entry.Threat.Level;
if (threat >= Level)
{
if (threat == LuneLaneThreat.Critical && Level != threat && !reducedMotion)
_pulse = 1.6f;
Level = threat;
_lowerHold = Hold;
}
else
{
_lowerHold -= delta;
if (_lowerHold <= 0f)
{
Level = threat;
_lowerHold = Hold;
}
}
Color tint = LuneUiTheme.Threat[Level];
_badge.SetState(LuneLaneThreat.Names[Level], tint, Level >= 2 ? LuneLaneThreat.Glyphs[Level] : "", Level == LuneLaneThreat.Critical);
_ownShape.Glyph = "emblem_" + ownTeam;
_ownShape.Color = LuneUiTheme.TeamColor(ownTeam);
_enemyShape.Glyph = "emblem_" + (1 - ownTeam);
_enemyShape.Color = LuneUiTheme.TeamColor(1 - ownTeam);
for (int index = 0; index < 4; index++)
{
LuneHudModel.BuildingInfo tower = entry.Towers[index];
Towers[index].Team = tower.Team;
Towers[index].Tier = tower.Tier;
Towers[index].SetState(tower.Alive, (float)tower.Fraction, tower.Attackable);
}
_own.text = entry.OwnPlayer;
LuneUi.SetColor(_own, entry.OwnIsMe ? LuneUiTheme.BronzeLight : LuneUiTheme.Muted);
_enemy.text = entry.EnemyPlayer;
ShowHero(_ownHero, entry.OwnHero);
ShowHero(_enemyHero, entry.EnemyHero);
LuneUi.Show(_mine, entry.OwnIsMe);
_army.text = entry.HasCounts ? entry.OwnCount + " : " + entry.EnemyCount : "";
string signature = string.Join(",", entry.Dangers.Select(d => d.Kind + ":" + d.Team + ":" + d.ChampionId));
if (signature != _signature)
{
_signature = signature;
_dangers.Clear();
foreach (Announcement danger in entry.Dangers)
{
bool hostile = danger.Team != ownTeam;
LuneUi.Glyph(danger.Kind == "boss" ? "crown" : "champion", hostile ? LuneUiTheme.Danger : LuneUiTheme.TeamColor(danger.Team), 16f, _dangers);
}
}
if (_pulse > 0f)
{
_pulse = Mathf.Max(0f, _pulse - delta);
_badge.style.opacity = _pulse > 0f ? .55f + .45f * Mathf.Abs(Mathf.Cos(_pulse * Mathf.PI * 2f)) : 1f;
}
else if (_badge.style.opacity != 1f)
{
_badge.style.opacity = 1f;
}
LuneTooltip.Current?.Refresh(this);
}
static void ShowHero(LunePortraitElement portrait, LuneHudModel.HeroBrief hero)
{
LuneUi.Show(portrait, hero != null);
if (hero != null)
portrait.ShowModel(hero.Model);
}
LuneTip Tip()
{
if (Data == null)
return null;
var lines = new List();
foreach (string reason in Data.Threat.Reasons)
lines.Add("• " + reason);
if (lines.Count == 0)
lines.Add("Keine Bedrohung aus Türmen, Kampflage oder Ankündigungen.");
var tip = new LuneTip
{
Title = Data.Title + "-LANE · " + LuneLaneThreat.Names[Level], Glyph = "lane_" + Lane,
Subtitle = PlayerText(Data.OwnPlayer, Data.OwnHero) + " gegen " + PlayerText(Data.EnemyPlayer, Data.EnemyHero),
Text = string.Join("\n", lines),
};
for (int index = 0; index < 2; index++)
tip.Stat("tower", "Eigene " + LuneHudModel.BuildingTitle(Data.Towers[index].Id), TowerValue(Data.Towers[index]));
for (int index = 2; index < 4; index++)
tip.Stat("tower", "Gegner " + LuneHudModel.BuildingTitle(Data.Towers[index].Id), TowerValue(Data.Towers[index]));
if (Data.HasCounts)
tip.Stat("melee", "Einheiten", Data.OwnCount + " eigene · " + Data.EnemyCount + " gegnerische");
var notes = new List();
foreach (Announcement danger in Data.Dangers)
{
string side = LuneUiTheme.TeamName(danger.Team);
notes.Add(danger.Kind == "boss" ? "Schlundkrone (" + side + ") greift an" : LuneContent.Champions[danger.ChampionId].Name + " (" + side + ") angekündigt");
}
tip.Note = (notes.Count > 0 ? string.Join("; ", notes) + "\n" : "") + "Klick: Kamera auf die Lane" + (Hotkey.Length > 0 ? " (" + Hotkey + ")" : "");
return tip;
}
static string PlayerText(string player, LuneHudModel.HeroBrief hero)
{
return hero == null ? player : player + " (" + hero.Name + ", Stufe " + hero.Level + ")";
}
LuneTip TowerTip(int index)
{
if (Data == null)
return null;
LuneHudModel.BuildingInfo tower = Data.Towers[index];
string text = !tower.Alive ? "Zerstört – die Ruine blockiert nicht." : tower.Attackable ? "Angreifbar." : "Noch geschützt: erst fällt der Turm davor.";
return new LuneTip { Title = LuneHudModel.BuildingTitle(tower.Id) + " · " + LuneUiTheme.TeamName(tower.Team), Glyph = "tower", GlyphColor = LuneUiTheme.TeamColor(tower.Team), Text = text }
.Stat("heart", "Leben", TowerValue(tower));
}
public static string TowerValue(LuneHudModel.BuildingInfo tower)
{
if (!tower.Alive)
return "zerstört";
return LuneUiTheme.Number(tower.Hp) + " / " + LuneUiTheme.Number(tower.MaxHp) + " (" + LuneUiTheme.Round(tower.Fraction * 100.0) + " %)";
}
}
///
/// The boss grotto and the Schlundkrone at a glance (Godot LuneBossIndicator): BOSS VERFÜGBAR,
/// BOSS UMKÄMPFT, BOSS EROBERT, EINSATZ NÄCHSTE WELLE, BOSS GREIFT AN, COOLDOWN – X RUNDEN. When
/// a team owns the boss the card names target lane, deployment wave and the bounty the defenders
/// earn; the player who steers the boss chooses its lane here. A click moves the camera.
///
public sealed class LuneBossIndicator : LunePanel
{
public event Action FocusRequested;
public event Action LaneChosen;
public LuneHudModel.BossInfo Data { get; private set; }
public readonly LunePortraitElement Portrait;
public bool Compact { get; private set; }
readonly LuneStatusBadge _badge;
readonly Label _line;
readonly Label _risk;
readonly LuneHpBar _bar;
readonly VisualElement _choice;
public readonly Dictionary LaneButtons = new Dictionary();
readonly LuneGlyphElement _owner;
string _signature = "";
int _ownTeam;
int _round;
bool _planning = true;
public LuneBossIndicator() : base(LunePanels.HudTight)
{
VisualElement row = LuneUi.Box(false, 10f, this);
row.style.alignItems = Align.FlexStart;
Portrait = new LunePortraitElement();
Portrait.style.width = 64f;
Portrait.style.height = 64f;
row.Add(Portrait);
_owner = LuneUi.Glyph("emblem_2", LuneUiTheme.Team[2], 18f, Portrait);
_owner.style.position = Position.Absolute;
_owner.style.left = 2f;
_owner.style.bottom = 2f;
VisualElement column = LuneUi.Box(true, 3f, row);
column.style.flexGrow = 1f;
column.style.flexShrink = 1f;
column.style.minWidth = 0f;
VisualElement head = LuneUi.Box(false, 6f, column);
LuneUi.Glyph("grotto", LuneUiTheme.Team[2], 15f, head);
LuneUi.Label("BOSSGROTTE · G", "HudCaps", head);
_badge = new LuneStatusBadge();
_badge.style.alignSelf = Align.FlexStart;
column.Add(_badge);
_line = LuneUi.Wrap(LuneUi.Label("", "HudText", column));
_risk = LuneUi.Wrap(LuneUi.Label("", "HudMuted", column));
_bar = new LuneHpBar(0f, 5f);
LuneUi.Show(_bar, false);
column.Add(_bar);
_choice = LuneUi.Box(false, 4f, column);
LuneUi.Show(_choice, false);
foreach (string lane in LuneMap.Lanes)
{
var button = new LuneButton(LuneUi.Upper(LuneMap.LaneNames[lane]), LuneButtons.HudToggle, _choice);
button.ToggleMode = true;
button.style.flexGrow = 1f;
string captured = lane;
button.Clicked += () => LaneChosen?.Invoke(captured);
LaneButtons[lane] = button;
}
RegisterCallback(evt =>
{
if (evt.button == 0)
FocusRequested?.Invoke();
});
LuneTooltip.Attach(this, Tip);
}
/// Short form while the right column shows a selected unit: badge only.
public void SetCompact(bool value)
{
if (value == Compact)
return;
Compact = value;
Portrait.style.width = value ? 40f : 64f;
Portrait.style.height = value ? 40f : 64f;
_signature = "";
if (Data != null)
Apply(Data, _ownTeam, _round, _planning);
}
public void Apply(LuneHudModel.BossInfo boss, int ownTeam, int round, bool planning)
{
string signature = boss + "|" + ownTeam + "|" + round + "|" + planning;
if (signature == _signature)
return;
_signature = signature;
Data = boss;
_ownTeam = ownTeam;
_round = round;
_planning = planning;
Portrait.ShowModel("schlundkrone");
int owner = boss.Team;
bool own = owner == ownTeam;
string lane = LuneUi.Upper(LuneMap.LaneNames.TryGetValue(boss.Lane, out string name) ? name : "?");
string side = LuneUiTheme.TeamName(owner);
bool showOwner = owner >= 0;
_owner.Glyph = "emblem_" + Mathf.Clamp(owner, 0, 2);
_owner.Color = LuneUiTheme.TeamColor(owner);
LuneUi.Show(_bar, false);
LuneUi.Show(_choice, false);
_risk.text = "";
switch (boss.State)
{
case "available":
_badge.SetState("BOSS VERFÜGBAR", LuneUiTheme.Team[2], "crown");
_line.text = "Einheiten mit „Grotte“ entsenden. Wer den Boss besiegt, schickt ihn in der Folgewelle gegen den Gegner.";
break;
case "contested":
_badge.SetState("BOSS UMKÄMPFT", LuneUiTheme.Warn, "melee");
_line.text = "Expeditionen kämpfen in der Grotte; sie fehlen ihren Lanes bis Kampfende.";
break;
case "conquered":
_badge.SetState("BOSS EROBERT · " + LuneUi.Upper(side), LuneUiTheme.TeamColor(owner), "crown", true);
_line.text = "Einsatz in Welle " + boss.DeployRound + " für " + (own ? "dein Team" : side) + ".";
break;
case "failed":
_badge.SetState("EXPEDITION GESCHEITERT", LuneUiTheme.Muted, "close");
_line.text = "Der Boss hat überlebt; niemand erhält ihn.";
break;
case "deploy_next":
_badge.SetState("EINSATZ NÄCHSTE WELLE", own ? LuneUiTheme.TeamColor(owner) : LuneUiTheme.Danger, "crown", true);
if (own)
{
_line.text = "Deine Schlundkrone greift in Welle " + boss.DeployRound + " auf " + lane + " an.";
_risk.text = "Fällt sie, erhält " + LuneUiTheme.TeamName(1 - ownTeam) + " " + LuneUiTheme.Number(boss.Bounty) + " Gold Kopfgeld.";
}
else
{
_line.text = side + ": Schlundkrone greift in Welle " + boss.DeployRound + " auf " + lane + " an.";
_risk.text = "Besiegt ihr sie, erhält dein Team " + LuneUiTheme.Number(boss.Bounty) + " Gold Kopfgeld.";
}
if (boss.Chooser && planning)
{
LuneUi.Show(_choice, true);
foreach (KeyValuePair pair in LaneButtons)
pair.Value.On = pair.Key == boss.Lane;
_risk.text += " Du wählst die Lane:";
}
break;
case "deploying":
_badge.SetState("BOSS GREIFT AN · " + lane, own ? LuneUiTheme.TeamColor(owner) : LuneUiTheme.Danger, "warning", true);
_line.text = own ? "Deine Schlundkrone greift " + lane + " an." : "Gegnerische Schlundkrone auf " + lane + "!";
_risk.text = "Kopfgeld für die Verteidiger: " + LuneUiTheme.Number(boss.Bounty) + " Gold.";
if (boss.HasHp)
{
LuneUi.Show(_bar, true);
_bar.Fraction = (float)(boss.Hp / Math.Max(1.0, boss.MaxHp));
_bar.Fill = LuneUiTheme.TeamColor(owner);
}
break;
case "defeated":
_badge.SetState("SCHLUNDKRONE BESIEGT", !own ? LuneUiTheme.Good : LuneUiTheme.Muted, "check");
_line.text = "Das Kopfgeld von " + LuneUiTheme.Number(boss.Bounty) + " Gold geht an " + (own ? "den Gegner" : "dein Team") + ".";
break;
default:
int left = boss.RoundsLeft;
_badge.SetState("COOLDOWN – " + left + (left == 1 ? " RUNDE" : " RUNDEN"), LuneUiTheme.Muted, "clock");
_line.text = "Grotte wieder offen ab Welle " + boss.NextRound + ".";
break;
}
LuneUi.Show(_risk, _risk.text.Length > 0 && !Compact);
LuneUi.Show(_line, !Compact);
LuneUi.Show(_owner, showOwner && !Compact);
if (Compact)
LuneUi.Show(_choice, false);
LuneTooltip.Current?.Refresh(this);
}
/// Live boss HP during its attack, without rebuilding the texts.
public void SetBossHp(float fraction)
{
if (LuneUi.Visible(_bar))
_bar.Fraction = fraction;
}
public string BadgeText { get { return _badge.Text; } }
public string LineText { get { return _line.text; } }
static LuneTip Tip()
{
return new LuneTip
{
Title = "Bossgrotte · Schlundkrone", Glyph = "crown", GlyphColor = LuneUiTheme.Team[2],
Text = "Expeditionen kämpfen zuerst gegeneinander; der Boss erwacht, wenn höchstens ein Team übrig ist. Sieg: Der Boss greift in der Folgewelle für die Sieger an, die Verteidiger erhalten bei seinem Fall ein hohes Kopfgeld. Danach ist die Grotte "
+ LuneContent.BossLockRounds + " Runden gesperrt.",
Note = "Klick oder G: Kamera zur Grotte",
};
}
}
///
/// What the coming wave brings (Godot LuneNextWave): monster types with counts (identical for
/// both sides and every lane), new types, the armour most of the wave wears and the damage type
/// that hits it for 130 %, announced attack champions and an announced boss. Dangers for the
/// own team are highlighted. In combat the panel shrinks to one line about the following wave.
///
public sealed class LuneNextWave : LunePanel
{
readonly Label _title;
readonly Label _round;
readonly VisualElement _rows;
readonly VisualElement _extra;
readonly Label _note;
string _signature = "";
public bool Compact { get; private set; }
public LuneNextWave() : base(LunePanels.HudTight)
{
VisualElement column = LuneUi.Box(true, 4f, this);
VisualElement head = LuneUi.Box(false, 0f, column);
_title = LuneUi.Label("NÄCHSTE WELLE", "HudCaps", head);
_title.style.flexGrow = 1f;
_round = LuneUi.Label("", "HudValue", head);
_rows = LuneUi.Box(true, 2f, column);
_note = LuneUi.Wrap(LuneUi.Label("", "HudMuted", column));
_extra = LuneUi.Box(true, 3f, column);
}
public string NoteText { get { return _note.text; } }
public void Apply(LuneHudModel.WaveInfo wave, int ownTeam, bool fighting)
{
string signature = wave + "|" + ownTeam + "|" + fighting;
if (signature == _signature)
return;
_signature = signature;
Compact = fighting;
_title.text = fighting ? "DANACH" : "NÄCHSTE WELLE";
_round.text = "WELLE " + wave.Round;
_rows.Clear();
_extra.Clear();
if (fighting)
{
_note.text = string.Join(", ", wave.Monsters.Select(m => m.Count + "× " + m.Name + (m.New ? " (neu)" : ""))) + " je Lane";
return;
}
foreach (LuneHudModel.MonsterGroup monster in wave.Monsters)
{
VisualElement row = LuneUi.Box(false, 7f, _rows);
row.pickingMode = PickingMode.Position;
var portrait = new LunePortraitElement();
portrait.style.width = 30f;
portrait.style.height = 30f;
row.Add(portrait);
portrait.ShowModel(monster.Id);
LuneUi.Label(monster.Count + "×", "HudValue", row).style.minWidth = 30f;
Label title = LuneUi.Clip(LuneUi.Label(monster.Name, "HudText", row));
title.style.flexGrow = 1f;
LuneUi.Glyph("armor", LuneTypeColors.Armor(monster.Armor), 13f, row);
LuneUi.SetColor(LuneUi.Label(LuneStats.ArmorName(monster.Armor), "HudMuted", row), LuneTypeColors.Armor(monster.Armor));
if (monster.New)
{
var badge = new LuneStatusBadge();
badge.SetState("NEU", LuneUiTheme.Warn);
row.Add(badge);
}
LuneHudModel.MonsterGroup data = monster;
int round = wave.Round;
LuneTooltip.Attach(row, () => new LuneTip { Title = data.Name, Subtitle = "Monster der Welle " + round, Note = "Beide Teams erhalten auf jeder Lane dieselbe Welle." }
.Stat("heart", "Leben", LuneUiTheme.Number(data.Hp)).Stat("sword", "Schaden/Treffer", LuneUiTheme.Decimal(data.Damage))
.Stat("range", "Reichweite", LuneUiTheme.Decimal(data.Range) + " m").Stat("armor", "Rüstung", LuneStats.ArmorName(data.Armor)));
}
// Counter hint as in Legion TD 2: the armour most of the wave wears and the damage type
// that hits it for 130 % (DamageMatrix).
int[] armour = new int[3];
foreach (LuneHudModel.MonsterGroup monster in wave.Monsters)
armour[Mathf.Clamp(monster.Armor, 0, 2)] += monster.Count;
int main = Array.IndexOf(armour, armour.Max());
int counter = 0;
for (int attack = 0; attack < 3; attack++)
{
if (Math.Abs(LuneUnit.DamageMatrix[attack, main] - 1.30) < 1e-9)
counter = attack;
}
_note.text = wave.Count + " Monster je Lane · beide Seiten gleich\nMeist " + LuneStats.ArmorName(main) + ": " + LuneStats.DamageName(counter) + " trifft stark";
if (wave.Champions.Count == 0 && wave.Boss == null)
return;
LuneUi.Label("ANGEKÜNDIGT", "HudCaps", _extra);
foreach (LuneHudModel.ChampionNotice entry in wave.Champions)
{
bool hostile = !entry.Own;
VisualElement row = LuneUi.Box(false, 6f, _extra);
LuneUi.Glyph("champion", hostile ? LuneUiTheme.Danger : LuneUiTheme.Champion, 15f, row);
LuneUi.Clip(LuneUi.Label(entry.Name + " → " + LuneUi.Upper(LuneMap.LaneNames[entry.Lane]), "HudText", row)).style.flexGrow = 1f;
var badge = new LuneStatusBadge();
badge.SetState(hostile ? "GEGEN EUCH" : "FÜR EUCH", hostile ? LuneUiTheme.Danger : LuneUiTheme.Good);
row.Add(badge);
}
if (wave.Boss != null)
{
bool hostile = !wave.Boss.Own;
VisualElement row = LuneUi.Box(false, 6f, _extra);
LuneUi.Glyph("crown", hostile ? LuneUiTheme.Danger : LuneUiTheme.TeamColor(ownTeam), 16f, row);
LuneUi.Clip(LuneUi.Label("Schlundkrone → " + LuneUi.Upper(LuneMap.LaneNames[wave.Boss.Lane]), "HudText", row)).style.flexGrow = 1f;
var badge = new LuneStatusBadge();
badge.SetState(hostile ? "GEGEN EUCH" : "FÜR EUCH", hostile ? LuneUiTheme.Danger : LuneUiTheme.Good, "crown");
row.Add(badge);
}
}
}
}