using System; using System.Collections.Generic; using Luneraria.Core; using UnityEngine; using UnityEngine.UIElements; namespace Luneraria.UI { /// Where a HUD click sends the camera: a lane, a map point or a named target (base, enemy_base, grotto, hero). public sealed class LuneFocusTarget { public string Lane = ""; public Vec2? Point; public string Camera = ""; public bool IsEmpty { get { return Lane.Length == 0 && Point == null && Camera.Length == 0; } } public static LuneFocusTarget ForLane(string lane) { return new LuneFocusTarget { Lane = lane ?? "" }; } public static LuneFocusTarget ForPoint(Vec2 point) { return new LuneFocusTarget { Point = point }; } public static LuneFocusTarget ForCamera(string target) { return new LuneFocusTarget { Camera = target ?? "" }; } public override string ToString() { return Lane + "|" + (Point.HasValue ? Point.Value.ToString() : "") + "|" + Camera; } } /// /// Colours of the armour and damage types (G3 open point, Legion TD principle): a damage type /// has the colour of the armour it hits for 130 % (DamageMatrix: type i is strong against armour i). /// public static class LuneTypeColors { public static readonly Color[] Types = { LuneUiTheme.Hex("e2b46b"), LuneUiTheme.Hex("7fb2e5"), LuneUiTheme.Hex("c39bf0") }; public static Color Armor(int armorType) { return Types[Mathf.Clamp(armorType, 0, 2)]; } public static Color Damage(int attackType) { int row = Mathf.Clamp(attackType, 0, 2); for (int armor = 0; armor < 3; armor++) { if (LuneUnit.DamageMatrix[row, armor] == 1.30) return Types[armor]; } return LuneUiTheme.Muted; } } /// /// The viewer's own gold with the coin symbol and the base income of the next settlement /// (Godot LuneResourceDisplay). It is only ever fed the own seat's gold. /// public sealed class LuneResourceDisplay : VisualElement { public readonly Label Amount; public readonly Label Detail; int _shown = -1; float _flash; public LuneResourceDisplay() { style.flexDirection = FlexDirection.Row; style.alignItems = Align.Center; pickingMode = PickingMode.Position; LuneUi.Glyph("coin", LuneUiTheme.Gold, 30f, this); VisualElement column = LuneUi.Box(true, -2f, this); column.style.marginLeft = 8f; Amount = LuneUi.Label("0", "HudGold", column); Detail = LuneUi.Label("", "HudMuted", column); } public void SetValues(int gold, int income, bool showIncome = true) { if (gold != _shown) { if (_shown >= 0 && !LunePreferences.ReducedMotion) _flash = .45f; _shown = gold; Amount.text = LuneUiTheme.Number(gold); } string line = showIncome ? "+" + LuneUiTheme.Number(income) + " nächste Runde" : ""; if (Detail.text != line) Detail.text = line; LuneUi.Show(Detail, showIncome); } public void Tick(float delta) { if (_flash <= 0f) return; _flash = Mathf.Max(0f, _flash - delta); // Godot modulate (1.35, 1.2, .8) fading back to white. float t = _flash / .45f; Color gold = LuneUiTheme.Gold; var flash = new Color(Mathf.Min(1f, gold.r * 1.35f), Mathf.Min(1f, gold.g * 1.2f), gold.b * .8f, 1f); LuneUi.SetColor(Amount, Color.Lerp(gold, flash, t)); } } /// /// The own team (Godot LuneTeamPanel): per seat the name, lane, hero (committed hero of the /// others, own draft for the viewer), readiness during planning and whether a human, a bot or – /// after a disconnect – a bot on a human's seat plays it. Gold or plans of others never appear. /// public sealed class LuneTeamPanel : VisualElement { sealed class Row { public VisualElement Element; public LuneGlyphElement Status; public LunePortraitElement Portrait; public Label Name; public Label Lane; public LuneStatusBadge Badge; public LuneHudModel.MemberInfo Data; } readonly List _rows = new List(); readonly Label _header; readonly LuneGlyphElement _emblem; string _signature = ""; public LuneTeamPanel() { pickingMode = PickingMode.Ignore; var column = (LuneBox)LuneUi.Box(true, 3f, this); VisualElement head = LuneUi.Box(false, 6f, column); _emblem = LuneUi.Glyph("emblem_0", LuneUiTheme.Team[0], 15f, head); _header = LuneUi.Label("DEIN TEAM", "HudCaps", head); for (int index = 0; index < 3; index++) { VisualElement element = LuneUi.Box(false, 6f, column); element.pickingMode = PickingMode.Position; element.style.minHeight = 28f; var row = new Row { Element = element }; row.Status = LuneUi.Glyph("human", LuneUiTheme.Muted, 15f, element); row.Portrait = new LunePortraitElement(); row.Portrait.style.width = 26f; row.Portrait.style.height = 26f; element.Add(row.Portrait); row.Name = LuneUi.Clip(LuneUi.Label("", "HudText", element)); row.Name.style.flexGrow = 1f; row.Lane = LuneUi.Label("", "HudMuted", element); row.Badge = new LuneStatusBadge(); element.Add(row.Badge); _rows.Add(row); int captured = index; LuneTooltip.Attach(element, () => Tip(captured)); } } public void Apply(List members, int team, bool planning, bool networked) { string signature = string.Join(",", members) + "|" + team + "|" + planning + "|" + networked; if (signature == _signature) return; _signature = signature; _emblem.Glyph = "emblem_" + team; _emblem.Color = LuneUiTheme.TeamColor(team); _header.text = "DEIN TEAM · " + LuneUi.Upper(LuneUiTheme.TeamName(team)); for (int index = 0; index < Mathf.Min(3, members.Count); index++) { LuneHudModel.MemberInfo member = members[index]; Row row = _rows[index]; row.Data = member; string status = member.Status; row.Status.Glyph = status == "self" ? "star" : status == "bot" ? "bot" : status == "offline" ? "offline" : "human"; row.Status.Color = status == "self" ? LuneUiTheme.BronzeLight : status == "offline" ? LuneUiTheme.Danger : LuneUiTheme.Muted; row.Name.text = member.Name; LuneUi.SetColor(row.Name, member.IsSelf ? LuneUiTheme.BronzeLight : LuneUiTheme.Text); row.Lane.text = LuneUi.Upper(LuneMap.LaneNames[member.Lane]); LuneUi.Show(row.Portrait, member.HeroId.Length > 0); if (member.HeroId.Length > 0) row.Portrait.ShowModel(LuneContent.Heroes[member.HeroId].Model); if (status == "offline") row.Badge.SetState("BOT ÜBERNIMMT", LuneUiTheme.Danger, "offline"); else if (status == "bot") row.Badge.SetState("BOT", LuneUiTheme.Muted, "bot"); else if (planning && member.Ready) row.Badge.SetState("BEREIT", LuneUiTheme.Good, "check"); else if (planning) row.Badge.SetState("PLANT", LuneUiTheme.Warn, "clock"); else row.Badge.SetState("IM KAMPF", LuneUiTheme.Info); } } LuneTip Tip(int index) { LuneHudModel.MemberInfo member = _rows[index].Data; if (member == null) return null; string kind = member.Status == "self" ? "Du" : member.Status == "human" ? "Mensch" : member.Status == "bot" ? "Bot" : "Verbindung getrennt – ein Bot spielt den Platz weiter"; string hero = "kein Held"; if (member.HeroId.Length > 0) hero = LuneContent.Heroes[member.HeroId].Name + " · Stufe " + member.HeroLevel; var tip = new LuneTip { Title = member.Name, Subtitle = LuneMap.LaneNames[member.Lane] + "-Lane · " + ContentData.FactionNames.GetValueOrDefault(member.Bund, ""), Note = member.IsSelf ? "" : "Gold und laufende Planung anderer Spieler bleiben privat.", }; tip.Stat("human", "Spieler", kind).Stat("crown", "Held", hero).Stat("check", "Planung", member.Ready ? "bereit" : "plant noch"); return tip; } } /// /// Purchase card (Godot LuneUnitCard): real-model portrait, name, role, gold price, hotkey and /// availability; the armour and damage type as coloured symbols (G3 open point). The champion /// variant uses the copper card style, a hatched portrait and the "1 WELLE" band at the bottom /// of the portrait, so temporary attack champions never read like permanent troops. /// public sealed class LuneUnitCard : LuneButton { static readonly Dictionary RoleGlyphs = new Dictionary { { "front", "tank" }, { "melee", "melee" }, { "ranged", "ranged" }, { "splash", "splash" }, { "support", "support" }, { "schutz", "tank" }, { "druck", "tempo" }, { "siege", "siege" } }; static readonly Dictionary RoleText = new Dictionary { { "front", "Tank" }, { "melee", "Nahkampf" }, { "ranged", "Fernkampf" }, { "splash", "Fläche" }, { "support", "Support" }, { "schutz", "Schutz" }, { "druck", "Druck" }, { "siege", "Belagerung" } }; public string TypeId = ""; public bool Champion; public int Cost; public string Reason = ""; public readonly LunePortraitElement Portrait; readonly Label _name; readonly LuneGlyphElement _roleGlyph; readonly Label _role; readonly Label _cost; readonly Label _key; readonly VisualElement _band; readonly LuneGlyphElement _lock; readonly LuneGlyphElement _armor; readonly LuneGlyphElement _attack; public LuneUnitCard() : base(null, LuneButtons.HudCard) { style.width = 116f; style.height = 132f; style.flexDirection = FlexDirection.Column; style.alignItems = Align.Stretch; style.justifyContent = Justify.FlexStart; style.paddingLeft = 6f; style.paddingRight = 6f; style.paddingTop = 6f; style.paddingBottom = 5f; Portrait = new LunePortraitElement(); Portrait.style.minHeight = 62f; Portrait.style.flexGrow = 1f; Add(Portrait); _key = LuneUi.Label("", "HudCaps", Portrait); _key.style.position = Position.Absolute; _key.style.left = 4f; _key.style.top = 1f; // Armour and damage type, top right (Legion TD: what it is good against). VisualElement types = LuneUi.Box(false, 2f, Portrait); types.style.position = Position.Absolute; types.style.right = 3f; types.style.top = 3f; _attack = LuneUi.Glyph("sword", LuneUiTheme.Muted, 13f, types); _armor = LuneUi.Glyph("armor", LuneUiTheme.Muted, 13f, types); _band = LuneUi.Box(false, 2f, Portrait); _band.style.position = Position.Absolute; _band.style.left = 0f; _band.style.right = 0f; _band.style.bottom = 0f; _band.style.height = 16f; _band.style.justifyContent = Justify.Center; _band.style.backgroundColor = new Color(0f, 0f, 0f, .62f); LuneUi.Glyph("hourglass", LuneUiTheme.Champion, 12f, _band); LuneUi.SetColor(LuneUi.Label("1 WELLE", "HudCaps", _band, 14f), LuneUiTheme.Champion); LuneUi.Show(_band, false); _lock = LuneUi.Glyph("lock", LuneUiTheme.Muted, 18f, Portrait); _lock.style.position = Position.Absolute; _lock.style.left = new Length(50f, LengthUnit.Percent); _lock.style.top = new Length(50f, LengthUnit.Percent); _lock.style.translate = new Translate(new Length(-50f, LengthUnit.Percent), new Length(-50f, LengthUnit.Percent)); LuneUi.Show(_lock, false); _name = LuneUi.Clip(LuneUi.Label("", "HudText", this)); _name.style.unityTextAlign = TextAnchor.MiddleCenter; _name.style.marginTop = 1f; VisualElement roleRow = LuneUi.Box(false, 4f, this); roleRow.style.justifyContent = Justify.Center; roleRow.style.marginTop = 1f; _roleGlyph = LuneUi.Glyph("", LuneUiTheme.Muted, 14f, roleRow); _role = LuneUi.Label("", "HudMuted", roleRow); VisualElement costRow = LuneUi.Box(false, 4f, this); costRow.style.justifyContent = Justify.Center; costRow.style.marginTop = 1f; LuneUi.Glyph("coin", LuneUiTheme.Gold, 15f, costRow); _cost = LuneUi.Label("", "HudValue", costRow); } public void Setup(string id, string modelId, string title, string roleKey, int price, string hotkey = "", bool champion = false, UnitDef definition = null) { TypeId = id; Champion = champion; Cost = price; Look = champion ? LuneButtons.HudChampionCard : LuneButtons.HudCard; Portrait.ShowModel(modelId); _name.text = title; _roleGlyph.Glyph = RoleGlyphs.TryGetValue(roleKey ?? "", out string glyph) ? glyph : "melee"; _role.text = RoleText.TryGetValue(roleKey ?? "", out string text) ? text : roleKey; _cost.text = price.ToString(); _key.text = hotkey; LuneUi.Show(_key, hotkey.Length > 0); LuneUi.Show(_band, champion); Portrait.Hatch = champion ? LuneUiTheme.WithAlpha(LuneUiTheme.Champion, .13f) : Color.clear; if (definition != null) { _attack.Color = LuneTypeColors.Damage(definition.AttackType); _armor.Color = LuneTypeColors.Armor(definition.ArmorType); } } /// ok = purchasable now; otherwise `why` explains it (tooltip) and the card greys out. public void SetAvailable(bool ok, string why = "", bool locked = false, bool unaffordable = false) { Reason = why ?? ""; Disabled = !ok; Portrait.Dimmed = !ok; LuneUi.Show(_lock, locked); LuneUi.SetColor(_cost, unaffordable ? LuneUiTheme.Danger : LuneUiTheme.Bright); LuneUi.SetColor(_name, ok ? LuneUiTheme.Text : LuneUiTheme.Muted); } } /// /// One own troop in the army strip (Godot LuneArmyChip): real-model portrait, a band with its /// lane (T, M, B or G) and stage (II for upgraded troops). Selected = pressed style. In combat a /// fallen troop is dimmed and crossed out. /// public sealed class LuneArmyChip : LuneButton { public const float Band = 15f; public string Uid = ""; public string TypeId = ""; public string Lane = "mid"; public readonly LunePortraitElement Portrait; readonly Label _lane; readonly Label _stage; bool _fallen; public LuneArmyChip() : base(null, LuneButtons.HudChip) { style.flexDirection = FlexDirection.Column; style.alignItems = Align.Stretch; style.justifyContent = Justify.FlexStart; style.paddingLeft = 2f; style.paddingRight = 2f; style.paddingTop = 2f; style.paddingBottom = 1f; Portrait = new LunePortraitElement(); Portrait.style.flexGrow = 1f; Add(Portrait); VisualElement band = LuneUi.Box(false, 0f, this); band.style.height = Band; band.style.backgroundColor = new Color(0f, 0f, 0f, .55f); band.style.justifyContent = Justify.SpaceBetween; band.style.paddingLeft = 3f; band.style.paddingRight = 3f; _lane = LuneUi.Label("", "HudCaps", band, 14f); _stage = LuneUi.Label("II", "HudCaps", band, 14f); LuneUi.SetColor(_stage, LuneUiTheme.Gold); } public void Setup(RosterEntry entry, float edge) { Uid = entry.Uid; TypeId = entry.TypeId; Lane = entry.Lane; style.width = edge; style.height = edge + Band; Portrait.ShowModel(TypeId); bool grotto = Lane == LuneMatch.Grotto; _lane.text = grotto ? "G" : LuneMap.LaneNames.TryGetValue(Lane, out string name) ? name.Substring(0, 1) : "?"; LuneUi.SetColor(_lane, grotto ? LuneUiTheme.Team[2] : LuneUiTheme.BronzeLight); LuneUi.Show(_stage, LuneStats.StageOf(TypeId) == 2); } public bool Fallen { get { return _fallen; } set { if (value == _fallen) return; _fallen = value; Portrait.Dimmed = value; Portrait.Crossed = value; } } } /// /// A hero item (Godot LuneItemSlot): one of the three slots (compact: symbol only) or a card in /// the item shop (symbol, name, cost, main effect, state). States say why something cannot be /// done: equipped, available, unaffordable, incompatible (hero level), unique (already carried), /// slots full, locked (plan committed) or an empty slot. /// public sealed class LuneItemSlot : LuneButton { public enum State { Empty, Equipped, Available, Unaffordable, Incompatible, Unique, Full, Locked } static readonly Dictionary StateText = new Dictionary { { State.Equipped, "AUSGERÜSTET" }, { State.Available, "KAUFBAR" }, { State.Unaffordable, "ZU TEUER" }, { State.Incompatible, "STUFE FEHLT" }, { State.Unique, "GETRAGEN" }, { State.Full, "PLÄTZE VOLL" }, { State.Locked, "GESPERRT" }, { State.Empty, "LEER" }, }; static readonly Dictionary ItemColors = new Dictionary { { "basaltschild", LuneUiTheme.Hex("9fb2c8") }, { "glutklinge", LuneUiTheme.Hex("ff9a5a") }, { "rankenmantel", LuneUiTheme.Hex("8fd08a") }, { "funkenfokus", LuneUiTheme.Hex("f5d76a") }, { "quellamulett", LuneUiTheme.Hex("7ad6e6") }, { "kristallsplitter", LuneUiTheme.Hex("c7a2ff") }, }; public string ItemId = ""; public State Current = State.Empty; public readonly bool Compact; readonly LuneGlyphElement _glyph; readonly Label _name; readonly Label _effect; readonly Label _cost; readonly LuneStatusBadge _badge; public static Color ColorOf(string id) { return ItemColors.TryGetValue(id ?? "", out Color color) ? color : LuneUiTheme.Muted; } public LuneItemSlot(bool compact = true) : base(null, compact ? LuneButtons.HudChip : LuneButtons.HudCard) { Compact = compact; if (compact) { style.width = 30f; style.height = 30f; style.paddingLeft = 5f; style.paddingRight = 5f; style.paddingTop = 5f; style.paddingBottom = 5f; _glyph = LuneUi.Glyph("", Color.white, 20f, this); _glyph.style.flexGrow = 1f; return; } style.minHeight = 74f; style.justifyContent = Justify.FlexStart; style.paddingLeft = 10f; style.paddingRight = 8f; style.paddingTop = 6f; style.paddingBottom = 6f; _glyph = LuneUi.Glyph("", Color.white, 34f, this); VisualElement column = LuneUi.Box(true, 0f, this); column.style.marginLeft = 10f; column.style.flexGrow = 1f; column.style.flexShrink = 1f; column.style.minWidth = 0f; column.style.justifyContent = Justify.Center; VisualElement head = LuneUi.Box(false, 6f, column); _name = LuneUi.Clip(LuneUi.Label("", "HudValue", head)); _name.style.flexGrow = 1f; LuneUi.Glyph("coin", LuneUiTheme.Gold, 14f, head); _cost = LuneUi.Label("", "HudValue", head); _effect = LuneUi.Clip(LuneUi.Label("", "HudMuted", column)); _badge = new LuneStatusBadge(); _badge.style.alignSelf = Align.FlexStart; column.Add(_badge); } public void SetItem(string id, State state) { ItemId = id ?? ""; Current = state; Color tint = ColorOf(ItemId); bool usable = state == State.Available || state == State.Equipped; _glyph.Glyph = ItemId; _glyph.Color = usable || Compact ? tint : LuneUiTheme.WithAlpha(tint, .45f); if (Compact) { Disabled = ItemId.Length == 0; return; } LuneContent.Items.TryGetValue(ItemId, out ItemSpec item); _name.text = item != null ? item.Name : ""; _cost.text = item != null ? item.Cost.ToString() : ""; LuneUi.SetColor(_cost, state == State.Unaffordable ? LuneUiTheme.Danger : LuneUiTheme.Bright); _effect.text = item != null ? item.Text : ""; Color badge = state == State.Equipped ? LuneUiTheme.Good : state == State.Available ? LuneUiTheme.BronzeLight : LuneUiTheme.Muted; if (state == State.Unaffordable) badge = LuneUiTheme.Danger; string glyph = state == State.Equipped || state == State.Unique ? "check" : state == State.Incompatible || state == State.Locked ? "lock" : ""; _badge.SetState(StateText[state], badge, glyph, state == State.Equipped); Disabled = !usable; } } /// /// Short event list (Godot LuneEventFeed): tower falls, champions, boss, critical Ancients, /// round results. At most five entries, newest on top; each fades after a while. A click on an /// entry with a place moves the camera there. It never lists every kill. /// public sealed class LuneEventFeed : VisualElement { public const int Limit = 5; public const float Lifetime = 14f; public event Action FocusRequested; sealed class Entry { public string Text; public string Level; public float Born; public LuneButton Node; } readonly List _entries = new List(); float _clock; public LuneEventFeed() { pickingMode = PickingMode.Ignore; LuneUi.Show(this, false); } public IReadOnlyList Texts { get { return _entries.ConvertAll(e => e.Text); } } static Color LevelColor(string level) { return level == "good" ? LuneUiTheme.Good : level == "warn" ? LuneUiTheme.Warn : level == "danger" ? LuneUiTheme.Danger : LuneUiTheme.Info; } /// level: info, good, warn, danger. target: null or a lane, point or camera target. public void Push(string text, string level = "info", string glyph = "", LuneFocusTarget target = null) { foreach (Entry entry in _entries) { if (entry.Text == text && _clock - entry.Born < 4f) return; } var button = new LuneButton(null, LuneButtons.HudRow); button.style.minHeight = 30f; button.style.justifyContent = Justify.FlexStart; button.style.paddingLeft = 8f; button.style.paddingRight = 8f; button.style.marginBottom = 3f; Color tint = LevelColor(level); LuneUi.Glyph(glyph.Length > 0 ? glyph : "star", tint, 15f, button).style.marginRight = 7f; Label label = LuneUi.Clip(LuneUi.Label(text, "HudText", button)); label.style.flexGrow = 1f; LuneUi.SetColor(label, Color.Lerp(tint, LuneUiTheme.Text, .45f)); bool hasTarget = target != null && !target.IsEmpty; button.Clicked += () => { if (hasTarget) FocusRequested?.Invoke(target); }; LuneTooltip.Attach(button, () => new LuneTip { Title = text, Note = hasTarget ? "Klick: Kamera dorthin" : "" }); Insert(0, button); _entries.Insert(0, new Entry { Text = text, Level = level, Born = _clock, Node = button }); while (_entries.Count > Limit) { Entry old = _entries[_entries.Count - 1]; _entries.RemoveAt(_entries.Count - 1); old.Node.RemoveFromHierarchy(); } LuneUi.Show(this, true); } public void ClearAll() { foreach (Entry entry in _entries) entry.Node.RemoveFromHierarchy(); _entries.Clear(); LuneUi.Show(this, false); } public void Tick(float delta) { _clock += delta; for (int index = _entries.Count - 1; index >= 0; index--) { Entry entry = _entries[index]; float age = _clock - entry.Born; if (age > Lifetime) { entry.Node.RemoveFromHierarchy(); _entries.RemoveAt(index); } else if (age > Lifetime - 2f) { entry.Node.style.opacity = Mathf.Clamp01((Lifetime - age) / 2f); } } LuneUi.Show(this, _entries.Count > 0); } } /// /// A window over the field for detail work (hero, recruiting; Godot LuneModalPanel). A light /// backdrop keeps clicks away from the world and closes the window; Esc and the × close it too. /// The window never covers the whole screen and only opens on request. /// public sealed class LuneModalPanel : VisualElement { public event Action Closed; public readonly LunePanel Panel; public readonly VisualElement Body; public readonly Label Title; readonly Vector2 _box; public LuneModalPanel(Vector2 size) { _box = size; style.position = Position.Absolute; style.left = 0f; style.top = 0f; style.right = 0f; style.bottom = 0f; pickingMode = PickingMode.Position; var backdrop = new VisualElement(); backdrop.style.position = Position.Absolute; backdrop.style.left = 0f; backdrop.style.top = 0f; backdrop.style.right = 0f; backdrop.style.bottom = 0f; backdrop.style.backgroundColor = new Color(.01f, .015f, .025f, .38f); backdrop.RegisterCallback(evt => { if (evt.button == 0) Close(); }); Add(backdrop); Panel = new LunePanel(LunePanels.Modal); Panel.style.position = Position.Absolute; Add(Panel); VisualElement column = LuneUi.Box(true, 12f, Panel); column.style.flexGrow = 1f; VisualElement head = LuneUi.Box(false, 0f, column); Title = LuneUi.Label("", "HudTitle", head); Title.style.flexGrow = 1f; var close = new LuneButton(null, LuneButtons.HudIconButton, head); close.style.width = 36f; close.style.height = 34f; LuneUi.Glyph("close", LuneUiTheme.Muted, 14f, close); close.Clicked += Close; LuneTooltip.Attach(close, () => new LuneTip { Title = "Schließen", Text = "Esc oder Klick neben das Fenster." }); Body = LuneUi.Box(true, 10f, column); Body.style.flexGrow = 1f; RegisterCallback(_ => Layout()); RegisterCallback(evt => evt.StopPropagation()); LuneUi.Show(this, false); } public bool Open { get { return style.display != DisplayStyle.None; } } public void OpenWith(string heading) { Title.text = heading; LuneUi.Show(this, true); BringToFront(); Layout(); } public void Close() { if (!Open) return; LuneUi.Show(this, false); Closed?.Invoke(); } void Layout() { float width = layout.width; float height = layout.height; if (float.IsNaN(width) || width <= 0f) return; var edge = new Vector2(Mathf.Min(_box.x, width - 40f), Mathf.Min(_box.y, height - 40f)); Panel.style.width = edge.x; Panel.style.height = edge.y; Panel.style.left = Mathf.Floor((width - edge.x) * .5f); Panel.style.top = Mathf.Floor((height - edge.y) * .5f); } } }