Hinter MatchOptions.Extended: Glyphe als Teamwahl einer Lane (Türme 5 s unverwundbar, sobald einer unter 50 % fällt; 4 Runden Abklingzeit nur nach Auslösen), vier Rezepte (Bollwerk, Glutsturm, Quellherz, Splitterkrone), Ancient-Stufen 1-3 (150 x Stufe, Reparatur 1.000, Schuss 45/75/110), Mid-Bots jagen bei sicherer Lane die Grotte. Teampanel mit Glyphe und Ancient, Rezepte im Shop (TEILE FEHLEN), Glyphen-Ring und Ancient-Schüsse. Kern 566/566 (s2 35), EditMode 32/32, PlayMode 17/17, Verify 718/0, Selbsttest 30/30, Golden 0, Fingerabdruck unverändert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
769 lines
35 KiB
C#
769 lines
35 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using Luneraria.Core;
|
||
using UnityEngine;
|
||
using UnityEngine.UIElements;
|
||
|
||
namespace Luneraria.UI
|
||
{
|
||
/// <summary>Where a HUD click sends the camera: a lane, a map point or a named target (base, enemy_base, grotto, hero).</summary>
|
||
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; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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).
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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, int bonus = 0)
|
||
{
|
||
if (gold != _shown)
|
||
{
|
||
if (_shown >= 0 && !LunePreferences.ReducedMotion)
|
||
_flash = .45f;
|
||
_shown = gold;
|
||
Amount.text = LuneUiTheme.Number(gold);
|
||
}
|
||
// S1: the champion income is part of the sum; the short form fits the gold column.
|
||
string line = !showIncome ? "" : bonus > 0 ? "+" + LuneUiTheme.Number(income) + " · davon " + bonus + " Söldner" : "+" + 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));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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<Row> _rows = new List<Row>();
|
||
readonly Label _header;
|
||
/// <summary>S2 team actions (extended rules): glyph lane toggles and the Ancient level.</summary>
|
||
public event Action<Command> CommandRequested;
|
||
public readonly Dictionary<string, LuneButton> GlyphButtons = new Dictionary<string, LuneButton>();
|
||
public LuneButton CoreButton;
|
||
VisualElement _actions;
|
||
Label _glyphState;
|
||
Label _coreState;
|
||
string _actionSignature = "";
|
||
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));
|
||
}
|
||
// S2: team actions; hidden under the reference rules.
|
||
_actions = LuneUi.Box(true, 3f, column);
|
||
_actions.style.marginTop = 4f;
|
||
VisualElement glyphRow = LuneUi.Box(false, 4f, _actions);
|
||
LuneUi.Glyph("tower", LuneUiTheme.BronzeLight, 14f, glyphRow);
|
||
LuneUi.Label("GLYPHE", "HudCaps", glyphRow).style.minWidth = 64f;
|
||
foreach (string lane in LuneMap.Lanes)
|
||
{
|
||
var button = new LuneButton(lane == "bot" ? "BOT" : LuneUi.Upper(LuneMap.LaneNames[lane]), LuneButtons.HudToggle, glyphRow);
|
||
string captured = lane;
|
||
button.Clicked += () => CommandRequested?.Invoke(Command.Glyph(button.On ? "" : captured));
|
||
GlyphButtons[lane] = button;
|
||
}
|
||
_glyphState = LuneUi.Label("", "HudMuted", glyphRow);
|
||
LuneTooltip.Attach(glyphRow, () => new LuneTip
|
||
{
|
||
Title = "Glyphe (wie in Dota 2)", Glyph = "tower", GlyphColor = LuneUiTheme.BronzeLight,
|
||
Text = "Halte die Glyphe für eine Lane bereit: Fällt dort im Kampf ein eigener Turm unter 50 %, werden die Türme der Lane 5 s unverwundbar. Gilt für das ganze Team; danach 4 Runden Abklingzeit. Löst sie nicht aus, bleibt sie bereit.",
|
||
Note = "Entscheidet sich ein Mitspieler auf einem früheren Platz anders, gilt seine Wahl.",
|
||
});
|
||
VisualElement coreRow = LuneUi.Box(false, 4f, _actions);
|
||
LuneUi.Glyph("crown", LuneUiTheme.BronzeLight, 14f, coreRow);
|
||
LuneUi.Label("ANCIENT", "HudCaps", coreRow).style.minWidth = 64f;
|
||
_coreState = LuneUi.Label("", "HudText", coreRow);
|
||
_coreState.style.flexGrow = 1f;
|
||
CoreButton = new LuneButton("", LuneButtons.HudButton, coreRow);
|
||
CoreButton.Clicked += () => CommandRequested?.Invoke(Command.CoreUpgrade());
|
||
LuneTooltip.Attach(coreRow, () => new LuneTip
|
||
{
|
||
Title = "Ancient stärken (wie der König in Legion TD 2)", Glyph = "crown", GlyphColor = LuneUiTheme.BronzeLight,
|
||
Text = "Jeder im Team kann Stufen kaufen (höchstens 3). Jede Stufe repariert den Ancient um 1.000 und lässt ihn auf Angreifer in 7,5 m schießen: 45 / 75 / 110 Schaden pro Sekunde.",
|
||
Note = "Preis: 150 × nächste Stufe.",
|
||
});
|
||
LuneUi.Show(_actions, false);
|
||
}
|
||
|
||
/// <summary>S2: glyph and Ancient level of the own team (planning commands; hidden under the reference rules).</summary>
|
||
public void ApplyActions(LuneHudModel model)
|
||
{
|
||
LuneMatch state = model.State;
|
||
bool on = state.Extended;
|
||
int team = model.Team;
|
||
Draft draft = model.Me.Draft;
|
||
int ready = state.Boss.GlyphReady[team];
|
||
int level = state.Boss.CoreLevel[team];
|
||
int pending = model.Planning ? draft.CoreInvest : 0;
|
||
string held = model.Planning ? draft.GlyphLane : "";
|
||
string signature = on + "|" + model.Planning + "|" + model.Locked + "|" + ready + "|" + state.RoundNumber + "|" + level + "|" + pending + "|" + held + "|" + model.OwnGold;
|
||
if (signature == _actionSignature)
|
||
return;
|
||
_actionSignature = signature;
|
||
LuneUi.Show(_actions, on);
|
||
if (!on)
|
||
return;
|
||
bool canAct = model.Planning && !model.Locked;
|
||
bool glyphReady = state.RoundNumber >= ready;
|
||
foreach (KeyValuePair<string, LuneButton> pair in GlyphButtons)
|
||
{
|
||
pair.Value.On = held == pair.Key;
|
||
pair.Value.Disabled = !canAct || !glyphReady;
|
||
}
|
||
_glyphState.text = glyphReady ? (held.Length > 0 ? "bereit" : "") : "ab Runde " + ready;
|
||
int shown = level + pending;
|
||
_coreState.text = "STUFE " + shown + "/" + state.Balance.CoreLevelMax + (pending > 0 ? " (+" + pending + ")" : "");
|
||
bool max = shown >= state.Balance.CoreLevelMax;
|
||
int cost = max ? 0 : state.Balance.CoreUpgradeCost(shown);
|
||
CoreButton.Text = max ? "MAX" : "+" + cost + " G";
|
||
CoreButton.Disabled = !canAct || max || model.OwnGold < cost;
|
||
}
|
||
|
||
/// <summary>S1: Mondsegen stacks of the own team (-1 under the reference rules).</summary>
|
||
public int Blessing = -1;
|
||
|
||
public void Apply(List<LuneHudModel.MemberInfo> members, int team, bool planning, bool networked)
|
||
{
|
||
string signature = string.Join(",", members) + "|" + team + "|" + planning + "|" + networked + "|" + Blessing;
|
||
if (signature == _signature)
|
||
return;
|
||
_signature = signature;
|
||
_emblem.Glyph = "emblem_" + team;
|
||
_emblem.Color = LuneUiTheme.TeamColor(team);
|
||
_header.text = "DEIN TEAM · " + LuneUi.Upper(LuneUiTheme.TeamName(team)) + (Blessing > 0 ? " · SEGEN " + Blessing + "/4" : "");
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public sealed class LuneUnitCard : LuneButton
|
||
{
|
||
static readonly Dictionary<string, string> RoleGlyphs = new Dictionary<string, string>
|
||
{ { "front", "tank" }, { "melee", "melee" }, { "ranged", "ranged" }, { "splash", "splash" }, { "support", "support" }, { "schutz", "tank" }, { "druck", "tempo" }, { "siege", "siege" } };
|
||
static readonly Dictionary<string, string> RoleText = new Dictionary<string, string>
|
||
{ { "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);
|
||
}
|
||
}
|
||
|
||
/// <summary>ok = purchasable now; otherwise `why` explains it (tooltip) and the card greys out.</summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public sealed class LuneItemSlot : LuneButton
|
||
{
|
||
public enum State { Empty, Equipped, Available, Unaffordable, Incompatible, Unique, Full, Locked, MissingParts }
|
||
|
||
static readonly Dictionary<State, string> StateText = new Dictionary<State, string>
|
||
{
|
||
{ 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" }, { State.MissingParts, "TEILE FEHLEN" },
|
||
};
|
||
static readonly Dictionary<string, Color> ItemColors = new Dictionary<string, Color>
|
||
{
|
||
{ "basaltschild", LuneUiTheme.Hex("9fb2c8") }, { "glutklinge", LuneUiTheme.Hex("ff9a5a") }, { "rankenmantel", LuneUiTheme.Hex("8fd08a") },
|
||
{ "funkenfokus", LuneUiTheme.Hex("f5d76a") }, { "quellamulett", LuneUiTheme.Hex("7ad6e6") }, { "kristallsplitter", LuneUiTheme.Hex("c7a2ff") },
|
||
// S2 recipes: the colour of their first part, a little brighter.
|
||
{ "bollwerk", LuneUiTheme.Hex("c3d3e6") }, { "glutsturm", LuneUiTheme.Hex("ffb27a") }, { "quellherz", LuneUiTheme.Hex("a2e8f2") }, { "splitterkrone", LuneUiTheme.Hex("dcc2ff") },
|
||
};
|
||
|
||
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;
|
||
// Godot: the glyph fills the slot with 5 px insets at any slot size (46 in the hero window).
|
||
_glyph = LuneUi.Glyph("", Color.white, 20f, this);
|
||
_glyph.style.position = Position.Absolute;
|
||
_glyph.style.left = 5f;
|
||
_glyph.style.top = 5f;
|
||
_glyph.style.right = 5f;
|
||
_glyph.style.bottom = 5f;
|
||
_glyph.style.width = StyleKeyword.Auto;
|
||
_glyph.style.height = StyleKeyword.Auto;
|
||
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.IsRecipe ? "+" + item.RecipeFee : 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 || state == State.MissingParts ? "lock" : "";
|
||
_badge.SetState(StateText[state], badge, glyph, state == State.Equipped);
|
||
Disabled = !usable;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public sealed class LuneEventFeed : VisualElement
|
||
{
|
||
public const int Limit = 5;
|
||
public const float Lifetime = 14f;
|
||
public event Action<LuneFocusTarget> FocusRequested;
|
||
|
||
sealed class Entry
|
||
{
|
||
public string Text;
|
||
public string Level;
|
||
public float Born;
|
||
public LuneButton Node;
|
||
}
|
||
|
||
readonly List<Entry> _entries = new List<Entry>();
|
||
float _clock;
|
||
|
||
public LuneEventFeed()
|
||
{
|
||
pickingMode = PickingMode.Ignore;
|
||
LuneUi.Show(this, false);
|
||
}
|
||
|
||
public IReadOnlyList<string> 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;
|
||
}
|
||
|
||
/// <summary>level: info, good, warn, danger. target: null or a lane, point or camera target.</summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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<PointerDownEvent>(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<GeometryChangedEvent>(_ => Layout());
|
||
RegisterCallback<WheelEvent>(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);
|
||
}
|
||
}
|
||
}
|