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; /// S1 barracks: crossed swords in the own team colour (own waves reinforced) or red (enemy waves). public readonly LuneGlyphElement Barracks; /// S3: neutral hexagon while a camp guardian stands on the lane this round. public readonly LuneGlyphElement CampMark; 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); Barracks = LuneUi.Glyph("melee", LuneUiTheme.Muted, 14f, head); LuneUi.Show(Barracks, false); CampMark = LuneUi.Glyph("emblem_2", LuneUiTheme.Team[2], 14f, head); LuneUi.Show(CampMark, false); _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); LuneUi.Show(Barracks, entry.OwnBarracks || entry.EnemyBarracks); LuneUi.Show(CampMark, entry.Camp.Length > 0); Barracks.Color = entry.EnemyBarracks ? LuneUiTheme.Danger : LuneUiTheme.TeamColor(ownTeam); _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"); // S1 barracks (Dota 2): a fallen inner tower reinforces the attackers' waves on this lane for good. if (Data.OwnBarracks) tip.Stat("melee", "Kaserne", "gegnerischer innerer Turm gefallen: eure Wellen +25 % und ein Monster mehr"); if (Data.EnemyBarracks) tip.Stat("warning", "Kaserne", "euer innerer Turm ist gefallen: gegnerische Wellen +25 % und ein Monster mehr"); // S3 camps (League of Legends): a guardian in the middle of the lane this round. if (Data.Camp.Length > 0) tip.Stat("emblem_2", "Lager", Data.Camp); 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); } } } }