Erweiterte Regeln: in jeder dritten Runde ein neutraler Wächter in jeder Lanemitte; wer beim Fall mehr Einheiten (dann mehr Leben) in 8 m hat, nimmt das Lager: Glutsegen (+10 % Schaden), Mondschild (Schild 10 %), Beute (50 Gold je Spieler). Fog of War (LuneFog, nur Darstellung): Gegner sichtbar in 9 m um eigene Einheiten, 10 m um Gebäude, 12 m um Späher; Späher (40 Gold, 3 Runden, Teamwirkung) zeigen in der Planung die gegnerische Armee ihrer Lane. HUD: Teampanel SPÄHER und LAGER, Lager-Sechseck, Nebel in Feld, Minimap, Namensschildern und Lane-Zählung. Kern 597/597 (s3 31), EditMode 33/33, PlayMode 18/18, Verify 718/0, Selbsttest 30/30, Golden 0, Fingerabdruck unverändert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
821 lines
39 KiB
C#
821 lines
39 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;
|
||
/// <summary>S3 wards per lane (fog of war).</summary>
|
||
public readonly Dictionary<string, LuneButton> WardButtons = new Dictionary<string, LuneButton>();
|
||
VisualElement _actions;
|
||
Label _glyphState;
|
||
Label _coreState;
|
||
/// <summary>S3: running camp buffs of the own team and the next camp round.</summary>
|
||
public Label CampState;
|
||
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.",
|
||
});
|
||
VisualElement wardRow = LuneUi.Box(false, 4f, _actions);
|
||
LuneUi.Glyph("star", LuneUiTheme.BronzeLight, 14f, wardRow);
|
||
LuneUi.Label("SPÄHER", "HudCaps", wardRow).style.minWidth = 64f;
|
||
foreach (string lane in LuneMap.Lanes)
|
||
{
|
||
var button = new LuneButton("", LuneButtons.HudToggle, wardRow);
|
||
string captured = lane;
|
||
button.Clicked += () => CommandRequested?.Invoke(Command.Ward(captured));
|
||
WardButtons[lane] = button;
|
||
}
|
||
LuneTooltip.Attach(wardRow, () => new LuneTip
|
||
{
|
||
Title = "Späher (wie die Wards in Dota 2)", Glyph = "star", GlyphColor = LuneUiTheme.BronzeLight,
|
||
Text = "Nebel des Krieges: Gegner siehst du nur nahe deiner Einheiten (9 m) und Gebäude (10 m). Ein Späher kostet 40 Gold, wacht 3 Runden über die Mitte seiner Lane (12 m) und zeigt in der Planung die gegnerische Aufstellung dort. Ist der äußere Turm des Gegners gefallen, ist die Lane auch ohne Späher aufgeklärt.",
|
||
Note = "Die Zahl auf dem Knopf: verbleibende Runden des Team-Spähers.",
|
||
});
|
||
VisualElement campRow = LuneUi.Box(false, 4f, _actions);
|
||
LuneUi.Glyph("emblem_2", LuneUiTheme.Team[2], 14f, campRow);
|
||
LuneUi.Label("LAGER", "HudCaps", campRow).style.minWidth = 64f;
|
||
CampState = LuneUi.Clip(LuneUi.Label("", "HudText", campRow));
|
||
CampState.style.flexGrow = 1f;
|
||
LuneTooltip.Attach(campRow, () => new LuneTip
|
||
{
|
||
Title = "Neutrallager (wie die Buff-Camps in League of Legends)", Glyph = "emblem_2", GlyphColor = LuneUiTheme.Team[2],
|
||
Text = "Jede dritte Runde steht in der Mitte jeder Lane ein neutraler Wächter, der beide Teams angreift. Fällt er, nimmt das Team mit mehr Einheiten in 8 m das Lager (bei Gleichstand: mehr Leben).\nTop · Glutsegen: +10 % Schaden, 2 Kämpfe.\nMid · Mondschild: Schild von 10 % Leben zu Kampfbeginn, 2 Kämpfe.\nBot · Beute: sofort 50 Gold für jeden im Team.",
|
||
});
|
||
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 : "";
|
||
int[] buffs = state.Boss.CampBuff;
|
||
string signature = string.Join(",", buffs) + "|" + string.Join(",", state.Boss.Wards) + "|" + string.Join(",", draft.WardLanes) + "|" + 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;
|
||
foreach (KeyValuePair<string, LuneButton> pair in WardButtons)
|
||
{
|
||
int left = state.Boss.Wards[team * 3 + LuneMap.LaneIndex(pair.Key)];
|
||
bool placed = model.Planning && draft.WardLanes.Contains(pair.Key);
|
||
string name = pair.Key == "bot" ? "BOT" : LuneUi.Upper(LuneMap.LaneNames[pair.Key]);
|
||
pair.Value.Text = placed ? name + " +" + state.Balance.WardRounds : left > 0 ? name + " " + left : name;
|
||
pair.Value.On = placed || left > 0;
|
||
pair.Value.Disabled = !canAct || placed || model.OwnGold < state.Balance.WardCost;
|
||
}
|
||
var parts = new List<string>();
|
||
for (int lane = 0; lane < 2; lane++)
|
||
{
|
||
int left = buffs[team * 3 + lane];
|
||
if (left > 0)
|
||
parts.Add(LuneMatch.CampBuffNames[lane] + " " + left);
|
||
}
|
||
int next = state.RoundNumber;
|
||
while (!state.CampRound(next) && next < state.RoundNumber + 10)
|
||
next++;
|
||
string upcoming = next == state.RoundNumber ? "Wächter diese Runde" : "Wächter in Runde " + next;
|
||
CampState.text = parts.Count > 0 ? string.Join(" · ", parts) + " · " + upcoming : upcoming;
|
||
}
|
||
|
||
/// <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);
|
||
}
|
||
}
|
||
}
|