Hinter MatchOptions.Extended (Standard im Spiel, Schalter im Startbildschirm): Söldner-Einkommen (Angriffschampion +Preis/12 Einkommen dauerhaft), Kasernen (gefallener innerer Turm: Gegnerwellen +25 % und ein Monster mehr), Erschöpfung und Rückkauf (60 % Leben, Rückkauf 60 + 20 x Stufe), Mondsegen (Grottensieg +4 % je Stapel, Mondseele bei 4). Bots wählen mit erweiterten Regeln auch Sylvara und kaufen Helden zurück. Netz überträgt Regeln und neue Zustände. HUD: Einkommen, Kasernen-Symbol, Rückkauf, Segen im Teamkopf. Kern 531/531 (s1 31), EditMode 31/31, PlayMode 16/16, Verify 718/0, Selbsttest 30/30, Golden 0 Abweichungen, Fingerabdruck unverändert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1475 lines
66 KiB
C#
1475 lines
66 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using Luneraria.Core;
|
||
using Luneraria.View;
|
||
using UnityEngine;
|
||
using UnityEngine.UIElements;
|
||
|
||
namespace Luneraria.UI
|
||
{
|
||
/// <summary>
|
||
/// Champions of Luneraria in-match HUD (port of scripts/luneraria/ui/lune_hud.gd, G3).
|
||
/// Presentation only: it reads the match through LuneHudModel (the privacy filter) and raises
|
||
/// the human player's commands as events for the game controller; no UI state creates
|
||
/// ownership or changes a rule.
|
||
///
|
||
/// Layout in logical pixels (1706×960 at 16:9, UI scale applied on top):
|
||
/// top match status: own Ancient, WELLE n and phase, enemy Ancient; options and menu
|
||
/// left lanes (threat, towers, players, announced dangers), own team, event feed
|
||
/// bottom gold, unit cards or attack champions, own army, hero
|
||
/// right context only: boss grotto, next wave, the selected unit
|
||
/// corner Bereit (planning) or the combat clock above the minimap
|
||
/// Updates follow changes: every component compares a signature and rebuilds only when its
|
||
/// data changed; the HUD polls at 10 Hz in planning and 5 Hz in combat, the minimap at 10 Hz.
|
||
/// </summary>
|
||
public sealed class LuneHud : MonoBehaviour
|
||
{
|
||
public const float Margin = 12f;
|
||
public const float LeftWidth = 312f;
|
||
public const float RightWidth = 322f;
|
||
public const float MapEdge = 224f;
|
||
public const float ReadyHeight = 84f;
|
||
public const float ReadyCombat = 54f;
|
||
public const float BarPlanning = 176f;
|
||
public const float BarCombat = 128f;
|
||
/// <summary>Smallest logical area the layout is built for; the UI scale never shrinks below it.</summary>
|
||
public static readonly Vector2 MinArea = new Vector2(1560f, 880f);
|
||
/// <summary>Godot base size of the content scale (canvas_items, expand).</summary>
|
||
public static readonly Vector2Int Reference = new Vector2Int(1600, 960);
|
||
public static readonly Dictionary<string, string> LaneKeys = new Dictionary<string, string> { { "top", "F2" }, { "mid", "F3" }, { "bot", "F4" } };
|
||
const float PlanningRate = .1f;
|
||
const float CombatRate = .2f;
|
||
const float MapRate = .1f;
|
||
const float FeedRate = .25f;
|
||
|
||
public event Action<Command> CommandRequested;
|
||
public event Action UndoRequested;
|
||
public event Action ReadyRequested;
|
||
public event Action StarterRequested;
|
||
public event Action SpeedRequested;
|
||
public event Action PauseRequested;
|
||
public event Action<string> LaneFocusRequested;
|
||
public event Action<Vec2> MapFocusRequested;
|
||
public event Action<string> CameraRequested;
|
||
public event Action MenuRequested;
|
||
public event Action RematchRequested;
|
||
public event Action SettingsRequested;
|
||
public event Action HeroCommandRequested;
|
||
|
||
public LuneMatch Match { get; private set; }
|
||
public int HumanSlot { get; private set; } = 1;
|
||
public LuneHudModel Model { get; private set; }
|
||
public UIDocument Document { get; private set; }
|
||
public PanelSettings Panel { get; private set; }
|
||
public VisualElement Root { get; private set; }
|
||
public float UiScale { get; private set; } = 1f;
|
||
public LuneMinimap Minimap { get; private set; }
|
||
/// <summary>Name tags of heroes, champions and the boss in the field, behind every region.</summary>
|
||
public LuneFieldTags FieldTags { get; private set; }
|
||
public LuneTooltip Tooltip { get; private set; }
|
||
public LuneHeroCard HeroCard { get; private set; }
|
||
public LuneHeroDetail HeroDetail { get; private set; }
|
||
public LuneChampionPanel ChampionPanel { get; private set; }
|
||
public LuneBossIndicator BossCard { get; private set; }
|
||
public LuneNextWave NextWave { get; private set; }
|
||
public LuneUnitDetail UnitDetail { get; private set; }
|
||
public LuneEventFeed Feed { get; private set; }
|
||
public LuneSettingsPanel Settings { get; private set; }
|
||
public readonly Dictionary<string, LuneLaneIndicator> Lanes = new Dictionary<string, LuneLaneIndicator>();
|
||
public readonly Dictionary<string, LuneUnitCard> ShopCards = new Dictionary<string, LuneUnitCard>();
|
||
public readonly Dictionary<string, LuneArmyChip> Chips = new Dictionary<string, LuneArmyChip>();
|
||
public string SelectedUid { get; private set; } = "";
|
||
/// <summary>Counters for tests and the performance log.</summary>
|
||
public int Syncs { get; private set; }
|
||
public double SyncMs { get; private set; }
|
||
|
||
sealed class AncientBlock
|
||
{
|
||
public VisualElement Box;
|
||
public LuneGlyphElement Emblem;
|
||
public Label Caption;
|
||
public LuneStatusBadge Badge;
|
||
public LuneHpBar Bar;
|
||
public Label Value;
|
||
public LuneTowerIndicator[] Towers;
|
||
public float Pulse;
|
||
public bool Own;
|
||
public LuneHudModel.BuildingInfo Data;
|
||
}
|
||
|
||
RenderTexture _target;
|
||
VisualElement _hud;
|
||
LunePanel _top;
|
||
readonly List<AncientBlock> _ancients = new List<AncientBlock>();
|
||
Label _wave;
|
||
LuneStatusBadge _phaseBadge;
|
||
VisualElement _system;
|
||
VisualElement _left;
|
||
LuneTeamPanel _team;
|
||
VisualElement _right;
|
||
LunePanel _bottom;
|
||
LuneResourceDisplay _gold;
|
||
LuneButton _undo;
|
||
LuneButton _starter;
|
||
VisualElement _purchase;
|
||
LuneTabBar _purchaseTabs;
|
||
VisualElement _troopRow;
|
||
VisualElement _army;
|
||
Label _armyTitle;
|
||
Label _armyLanes;
|
||
Label _armyValue;
|
||
VisualElement _strip;
|
||
string _chipSignature = "@";
|
||
LunePanel _readyBox;
|
||
LuneButton _readyButton;
|
||
LuneGlyphElement _readyCheck;
|
||
Label _readyNote;
|
||
VisualElement _combatRow;
|
||
Label _clock;
|
||
Label _auto;
|
||
float _readyBlockUntil;
|
||
LuneButton _pause;
|
||
LuneGlyphElement _pauseGlyph;
|
||
LuneButton _speed;
|
||
LunePanel _mapPanel;
|
||
VisualElement _banners;
|
||
string _bannerSignature = "@";
|
||
VisualElement _statusWrap;
|
||
LunePanel _status;
|
||
Label _report;
|
||
Label _toast;
|
||
float _toastLeft;
|
||
LuneModalPanel _modal;
|
||
VisualElement _over;
|
||
Label _overTitle;
|
||
Label _overBody;
|
||
float _tick;
|
||
float _mapTick;
|
||
float _feedTick;
|
||
bool _dirty = true;
|
||
int _lastPhase = -1;
|
||
int _layoutPhase = -1;
|
||
readonly Dictionary<string, bool> _knownAlive = new Dictionary<string, bool>();
|
||
readonly int[] _ancientLevel = new int[2];
|
||
readonly Dictionary<string, bool> _bossSeen = new Dictionary<string, bool>();
|
||
int _heroLevel;
|
||
Vector2 _area;
|
||
|
||
// ---------------------------------------------------------------- build
|
||
/// <summary>
|
||
/// Builds the HUD for one seat. target: null for the screen, or a render texture (tests and
|
||
/// captures in batch mode, any resolution).
|
||
/// </summary>
|
||
public void Init(LuneMatch match, int humanSlot, LuneUnitLibrary library, LuneUiAssets assets, RenderTexture target = null)
|
||
{
|
||
// A rebuild (rematch) starts with fresh listeners and a fresh panel.
|
||
CommandRequested = null;
|
||
UndoRequested = null;
|
||
ReadyRequested = null;
|
||
StarterRequested = null;
|
||
SpeedRequested = null;
|
||
PauseRequested = null;
|
||
LaneFocusRequested = null;
|
||
MapFocusRequested = null;
|
||
CameraRequested = null;
|
||
MenuRequested = null;
|
||
RematchRequested = null;
|
||
SettingsRequested = null;
|
||
HeroCommandRequested = null;
|
||
if (Panel != null)
|
||
Destroy(Panel);
|
||
Lanes.Clear();
|
||
ShopCards.Clear();
|
||
Chips.Clear();
|
||
_ancients.Clear();
|
||
_knownAlive.Clear();
|
||
_bossSeen.Clear();
|
||
_chipSignature = "@";
|
||
_bannerSignature = "@";
|
||
SelectedUid = "";
|
||
Match = match;
|
||
HumanSlot = Math.Max(0, humanSlot);
|
||
LuneUi.Assets = assets;
|
||
LunePortraits.Create(library);
|
||
Model = new LuneHudModel(match, HumanSlot);
|
||
_target = target;
|
||
Panel = ScriptableObject.CreateInstance<PanelSettings>();
|
||
Panel.name = "LuneHudPanel";
|
||
Panel.themeStyleSheet = assets != null ? assets.Theme : null;
|
||
Panel.scaleMode = PanelScaleMode.ScaleWithScreenSize;
|
||
Panel.referenceResolution = Reference;
|
||
Panel.screenMatchMode = PanelScreenMatchMode.Expand;
|
||
Panel.sortingOrder = 10;
|
||
if (target != null)
|
||
{
|
||
Panel.targetTexture = target;
|
||
Panel.clearColor = true;
|
||
Panel.colorClearValue = new Color(0f, 0f, 0f, 0f);
|
||
}
|
||
Document = gameObject.GetComponent<UIDocument>() ?? gameObject.AddComponent<UIDocument>();
|
||
Document.panelSettings = Panel;
|
||
Root = Document.rootVisualElement;
|
||
Root.pickingMode = PickingMode.Ignore;
|
||
Root.Clear();
|
||
_hud = new VisualElement { name = "lune-hud", pickingMode = PickingMode.Ignore };
|
||
_hud.style.position = Position.Absolute;
|
||
_hud.style.left = 0f;
|
||
_hud.style.top = 0f;
|
||
_hud.style.right = 0f;
|
||
_hud.style.bottom = 0f;
|
||
Root.Add(_hud);
|
||
FieldTags = new LuneFieldTags();
|
||
_hud.Add(FieldTags);
|
||
BuildTop();
|
||
BuildSystem();
|
||
BuildLeft();
|
||
BuildRight();
|
||
BuildBottom();
|
||
BuildReady();
|
||
BuildMap();
|
||
BuildBanner();
|
||
BuildStatus();
|
||
BuildModal();
|
||
BuildOver();
|
||
Settings = new LuneSettingsPanel();
|
||
_hud.Add(Settings);
|
||
Tooltip = new LuneTooltip();
|
||
_hud.Add(Tooltip);
|
||
_hud.RegisterCallback<PointerMoveEvent>(evt => Tooltip.Pointer = evt.position, TrickleDown.TrickleDown);
|
||
_hud.RegisterCallback<PointerDownEvent>(evt => Tooltip.Pointer = evt.position, TrickleDown.TrickleDown);
|
||
// Banner, status line and right column follow the real heights of the top panel, the
|
||
// bottom bar and the system row (Godot _layout on resize); before the first layout
|
||
// Layout() uses estimates.
|
||
_top.RegisterCallback<GeometryChangedEvent>(_ => Layout());
|
||
_bottom.RegisterCallback<GeometryChangedEvent>(_ => Layout());
|
||
_system.RegisterCallback<GeometryChangedEvent>(_ => Layout());
|
||
LunePreferences.Changed -= Fit;
|
||
LunePreferences.Changed += Fit;
|
||
Fit();
|
||
RememberBuildings();
|
||
_heroLevel = OwnHeroLevel();
|
||
_lastPhase = -1;
|
||
_dirty = true;
|
||
Sync(0f);
|
||
}
|
||
|
||
void OnDestroy()
|
||
{
|
||
LunePreferences.Changed -= Fit;
|
||
if (Panel != null)
|
||
Destroy(Panel);
|
||
}
|
||
|
||
LunePanel AddPanel(LunePlateStyle style)
|
||
{
|
||
var panel = new LunePanel(style);
|
||
panel.style.position = Position.Absolute;
|
||
panel.RegisterCallback<WheelEvent>(evt => evt.StopPropagation());
|
||
_hud.Add(panel);
|
||
return panel;
|
||
}
|
||
|
||
void BuildTop()
|
||
{
|
||
// A full-width row that centres the panel and lets clicks through elsewhere.
|
||
var wrap = new VisualElement { pickingMode = PickingMode.Ignore };
|
||
wrap.style.position = Position.Absolute;
|
||
wrap.style.left = 0f;
|
||
wrap.style.right = 0f;
|
||
wrap.style.top = Margin;
|
||
wrap.style.flexDirection = FlexDirection.Row;
|
||
wrap.style.justifyContent = Justify.Center;
|
||
_hud.Add(wrap);
|
||
_top = new LunePanel(LunePanels.Hud);
|
||
_top.RegisterCallback<WheelEvent>(evt => evt.StopPropagation());
|
||
wrap.Add(_top);
|
||
VisualElement row = LuneUi.Box(false, 20f, _top);
|
||
for (int side = 0; side < 2; side++)
|
||
{
|
||
AncientBlock block = AncientBlockFor(side == 0);
|
||
_ancients.Add(block);
|
||
row.Add(block.Box);
|
||
if (side == 0)
|
||
{
|
||
VisualElement center = LuneUi.Box(true, 2f, row);
|
||
center.style.minWidth = 190f;
|
||
center.style.alignItems = Align.Center;
|
||
center.style.justifyContent = Justify.Center;
|
||
center.pickingMode = PickingMode.Position;
|
||
_wave = LuneUi.Label("WELLE 1", "HudWave", center);
|
||
_phaseBadge = new LuneStatusBadge();
|
||
center.Add(_phaseBadge);
|
||
LuneTooltip.Attach(center, WaveTip);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>Ancient block: team emblem, name, HP bar and value, the two base towers, critical badge.</summary>
|
||
AncientBlock AncientBlockFor(bool own)
|
||
{
|
||
var block = new AncientBlock { Own = own, Towers = new LuneTowerIndicator[2] };
|
||
block.Box = LuneUi.Box(false, 10f);
|
||
block.Box.style.minWidth = 300f;
|
||
block.Box.pickingMode = PickingMode.Position;
|
||
block.Emblem = LuneUi.Glyph("emblem_0", LuneUiTheme.Team[0], 36f);
|
||
var column = (LuneBox)LuneUi.Box(true, 3f);
|
||
column.style.flexGrow = 1f;
|
||
if (own)
|
||
{
|
||
block.Box.Add(block.Emblem);
|
||
block.Box.Add(column);
|
||
}
|
||
else
|
||
{
|
||
block.Box.Add(column);
|
||
block.Box.Add(block.Emblem);
|
||
}
|
||
VisualElement head = LuneUi.Box(false, 6f, column);
|
||
block.Caption = LuneUi.Label("", "HudCaps");
|
||
block.Badge = new LuneStatusBadge();
|
||
LuneUi.Show(block.Badge, false);
|
||
if (own)
|
||
{
|
||
head.Add(block.Caption);
|
||
LuneUi.Spacer(head);
|
||
head.Add(block.Badge);
|
||
}
|
||
else
|
||
{
|
||
head.Add(block.Badge);
|
||
LuneUi.Spacer(head);
|
||
head.Add(block.Caption);
|
||
}
|
||
block.Bar = new LuneHpBar(240f, 10f) { Ticks = 4 };
|
||
column.Add(block.Bar);
|
||
VisualElement row = LuneUi.Box(false, 5f, column);
|
||
block.Value = LuneUi.Label("", "HudValue");
|
||
Label baseLabel = LuneUi.Label("BASIS", "HudMuted");
|
||
for (int index = 0; index < 2; index++)
|
||
block.Towers[index] = new LuneTowerIndicator(18f, 22f) { Tier = "base" };
|
||
if (own)
|
||
{
|
||
row.Add(block.Value);
|
||
LuneUi.Spacer(row);
|
||
row.Add(baseLabel);
|
||
foreach (LuneTowerIndicator tower in block.Towers)
|
||
row.Add(tower);
|
||
}
|
||
else
|
||
{
|
||
foreach (LuneTowerIndicator tower in block.Towers)
|
||
row.Add(tower);
|
||
row.Add(baseLabel);
|
||
LuneUi.Spacer(row);
|
||
row.Add(block.Value);
|
||
}
|
||
LuneTooltip.Attach(block.Box, () => AncientTip(block));
|
||
for (int index = 0; index < 2; index++)
|
||
{
|
||
int captured = index;
|
||
LuneTooltip.Attach(block.Towers[index], () => BaseTip(block, captured));
|
||
}
|
||
return block;
|
||
}
|
||
|
||
void BuildSystem()
|
||
{
|
||
_system = LuneUi.Box(false, 6f, _hud);
|
||
_system.style.position = Position.Absolute;
|
||
_system.style.top = Margin;
|
||
_system.style.right = Margin;
|
||
// Pause and tempo live up here, far from Bereit: a late click at the end of a combat must
|
||
// never land on Bereit (found in the G3 playtest).
|
||
_pause = IconButton("pause", "P", _system, out _pauseGlyph);
|
||
LuneUi.Show(_pause, false);
|
||
_pause.Clicked += () => PauseRequested?.Invoke();
|
||
LuneTooltip.Attach(_pause, () => new LuneTip { Title = "Pause", Text = "Hält die Simulation an (offline). Im Netzspiel bestimmt der Host.", Note = "P oder Leertaste" });
|
||
_speed = new LuneButton("1×", LuneButtons.HudIconButton, _system);
|
||
_speed.style.minWidth = 46f;
|
||
_speed.style.height = 36f;
|
||
LuneUi.Show(_speed, false);
|
||
_speed.Clicked += () => SpeedRequested?.Invoke();
|
||
LuneTooltip.Attach(_speed, () => new LuneTip { Title = "Tempo", Text = "Wechselt 1× · 2× · 4×; die Regeln bleiben gleich." });
|
||
LuneButton options = IconButton("gear", "F1", _system, out _);
|
||
options.Clicked += () => SettingsRequested?.Invoke();
|
||
LuneTooltip.Attach(options, () => new LuneTip { Title = "Optionen", Text = "Lautstärke, Vollbild, Bewegung reduzieren, UI-Größe und alle Tasten.", Note = "F1" });
|
||
LuneButton menu = IconButton("menu", "Menü", _system, out _);
|
||
menu.Clicked += () => MenuRequested?.Invoke();
|
||
LuneTooltip.Attach(menu, () => new LuneTip { Title = "Hauptmenü", Text = "Verlässt die Partie." });
|
||
}
|
||
|
||
static LuneButton IconButton(string glyph, string text, VisualElement parent, out LuneGlyphElement glyphElement)
|
||
{
|
||
var button = new LuneButton(null, LuneButtons.HudIconButton, parent);
|
||
glyphElement = LuneUi.Glyph(glyph, LuneUiTheme.Text, 15f, button);
|
||
Label label = LuneUi.Label(text, "HudText", button);
|
||
label.style.marginLeft = 5f;
|
||
button.style.minWidth = Mathf.Max(40f, 30f + text.Length * 8.5f);
|
||
button.style.height = 36f;
|
||
return button;
|
||
}
|
||
|
||
void BuildLeft()
|
||
{
|
||
_left = LuneUi.Box(true, 8f, _hud);
|
||
_left.style.position = Position.Absolute;
|
||
_left.style.left = Margin;
|
||
_left.style.top = Margin;
|
||
_left.style.width = LeftWidth;
|
||
var lanePanel = new LunePanel(LunePanels.Hud);
|
||
lanePanel.RegisterCallback<WheelEvent>(evt => evt.StopPropagation());
|
||
_left.Add(lanePanel);
|
||
VisualElement column = LuneUi.Box(true, 5f, lanePanel);
|
||
VisualElement head = LuneUi.Box(false, 6f, column);
|
||
LuneUi.Label("LANES", "HudCaps", head);
|
||
LuneUi.Spacer(head);
|
||
LuneUi.Label("Klick oder F2–F4", "HudMuted", head);
|
||
foreach (string lane in LuneMap.Lanes)
|
||
{
|
||
var indicator = new LuneLaneIndicator(lane, LaneKeys[lane]);
|
||
column.Add(indicator);
|
||
string captured = lane;
|
||
indicator.Clicked += () => LaneFocusRequested?.Invoke(captured);
|
||
Lanes[lane] = indicator;
|
||
}
|
||
var teamPanel = new LunePanel(LunePanels.Hud);
|
||
teamPanel.RegisterCallback<WheelEvent>(evt => evt.StopPropagation());
|
||
_left.Add(teamPanel);
|
||
_team = new LuneTeamPanel();
|
||
teamPanel.Add(_team);
|
||
Feed = new LuneEventFeed();
|
||
_left.Add(Feed);
|
||
Feed.FocusRequested += FocusTarget;
|
||
}
|
||
|
||
void BuildRight()
|
||
{
|
||
_right = LuneUi.Box(true, 8f, _hud);
|
||
_right.style.position = Position.Absolute;
|
||
_right.style.right = Margin;
|
||
_right.style.top = Margin + 36f + 8f;
|
||
_right.style.width = RightWidth;
|
||
BossCard = new LuneBossIndicator();
|
||
BossCard.RegisterCallback<WheelEvent>(evt => evt.StopPropagation());
|
||
_right.Add(BossCard);
|
||
BossCard.FocusRequested += () => CameraRequested?.Invoke("grotto");
|
||
BossCard.LaneChosen += lane => CommandRequested?.Invoke(Command.BossLaneChoice(lane));
|
||
NextWave = new LuneNextWave();
|
||
NextWave.RegisterCallback<WheelEvent>(evt => evt.StopPropagation());
|
||
_right.Add(NextWave);
|
||
UnitDetail = new LuneUnitDetail();
|
||
UnitDetail.RegisterCallback<WheelEvent>(evt => evt.StopPropagation());
|
||
LuneUi.Show(UnitDetail, false);
|
||
_right.Add(UnitDetail);
|
||
UnitDetail.CommandRequested += command => CommandRequested?.Invoke(command);
|
||
UnitDetail.Closed += ClearSelection;
|
||
}
|
||
|
||
void BuildBottom()
|
||
{
|
||
_bottom = AddPanel(LunePanels.Hud);
|
||
_bottom.style.left = Margin;
|
||
_bottom.style.bottom = Margin;
|
||
_bottom.style.right = Margin + MapEdge + 16f + 10f;
|
||
_bottom.style.minHeight = BarPlanning;
|
||
VisualElement row = LuneUi.Box(false, 14f, _bottom);
|
||
row.style.alignItems = Align.FlexStart;
|
||
row.style.flexGrow = 1f;
|
||
// Gold and undo.
|
||
VisualElement goldColumn = LuneUi.Box(true, 4f, row);
|
||
goldColumn.style.minWidth = 150f;
|
||
LuneUi.Label("GOLD", "HudCaps", goldColumn);
|
||
_gold = new LuneResourceDisplay();
|
||
goldColumn.Add(_gold);
|
||
LuneTooltip.Attach(_gold, GoldTip);
|
||
_undo = new LuneButton("Rückgängig", LuneButtons.HudButton, goldColumn);
|
||
_undo.style.alignSelf = Align.FlexStart;
|
||
_undo.Clicked += () => UndoRequested?.Invoke();
|
||
LuneTooltip.Attach(_undo, () => new LuneTip { Title = "Rückgängig", Text = "Nimmt den letzten Befehl dieser Planung zurück (Kauf, Einsatz, Item, Buchung)." });
|
||
// Purchases: troops (1–5) or attack champions.
|
||
_purchase = LuneUi.Box(true, 4f, row);
|
||
_purchaseTabs = new LuneTabBar();
|
||
_purchase.Add(_purchaseTabs);
|
||
_purchaseTabs.AddTab("TRUPPEN · 1–5", "Dauerhafte Truppen", "Bleiben in deiner Armee, kämpfen jede Welle, lassen sich aufwerten.");
|
||
_purchaseTabs.AddTab("ANGRIFFSCHAMPIONS", "Angriffschampions", "Einmalige Söldner: jetzt kaufen, eine Welle später auf der Ziel-Lane angreifen, danach weg.");
|
||
_purchaseTabs.TabChanged += ShowPurchase;
|
||
_troopRow = LuneUi.Box(false, 6f, _purchase);
|
||
_troopRow.style.alignItems = Align.FlexStart;
|
||
for (int index = 0; index < 5; index++)
|
||
_troopRow.Add(new LuneUnitCard());
|
||
ChampionPanel = new LuneChampionPanel();
|
||
LuneUi.Show(ChampionPanel, false);
|
||
_purchase.Add(ChampionPanel);
|
||
ChampionPanel.CommandRequested += command => CommandRequested?.Invoke(command);
|
||
// Own army, ordered by lane.
|
||
_army = LuneUi.Box(true, 4f, row);
|
||
_army.style.flexGrow = 1f;
|
||
_army.style.flexShrink = 1f;
|
||
_army.style.minWidth = 0f;
|
||
_army.RegisterCallback<GeometryChangedEvent>(_ => MarkDirty());
|
||
VisualElement head = LuneUi.Box(false, 8f, _army);
|
||
_armyTitle = LuneUi.Label("ARMEE", "HudCaps", head);
|
||
_armyLanes = LuneUi.Clip(LuneUi.Label("", "HudMuted", head));
|
||
_armyLanes.style.flexGrow = 1f;
|
||
_armyValue = LuneUi.Label("", "HudMuted", head);
|
||
LuneTooltip.Attach(_armyValue, () => new LuneTip { Title = "Armeewert", Glyph = "coin", GlyphColor = LuneUiTheme.Gold,
|
||
Text = "Summe des Einsatzes aller eigenen Truppen (Kauf und Aufwertungen), wie der Armeewert in Legion TD 2." });
|
||
_strip = new VisualElement { pickingMode = PickingMode.Ignore };
|
||
_strip.style.flexDirection = FlexDirection.Row;
|
||
_strip.style.flexWrap = Wrap.Wrap;
|
||
_army.Add(_strip);
|
||
_starter = new LuneButton("Startaufstellung", LuneButtons.HudButton, _army);
|
||
_starter.style.alignSelf = Align.FlexStart;
|
||
_starter.Clicked += () => StarterRequested?.Invoke();
|
||
LuneTooltip.Attach(_starter, StarterTip);
|
||
HeroCard = new LuneHeroCard();
|
||
row.Add(HeroCard);
|
||
HeroCard.Clicked += () => OpenHero();
|
||
HeroCard.CommandRequested += () =>
|
||
{
|
||
HeroCommandRequested?.Invoke();
|
||
OpenHero(LuneHeroDetail.TabUltimate);
|
||
};
|
||
}
|
||
|
||
void BuildReady()
|
||
{
|
||
_readyBox = AddPanel(LunePanels.HudTight);
|
||
_readyBox.style.right = Margin;
|
||
_readyBox.style.bottom = Margin + MapEdge + 16f + 8f;
|
||
_readyBox.style.width = MapEdge + 16f;
|
||
_readyBox.style.height = ReadyHeight;
|
||
VisualElement column = LuneUi.Box(true, 2f, _readyBox);
|
||
column.style.flexGrow = 1f;
|
||
_readyButton = new LuneButton("BEREIT", LuneButtons.Primary, column);
|
||
_readyButton.style.height = 44f;
|
||
_readyButton.style.paddingTop = 0f;
|
||
_readyButton.style.paddingBottom = 0f;
|
||
_readyButton.Clicked += () =>
|
||
{
|
||
if (!ReadyBlocked())
|
||
ReadyRequested?.Invoke();
|
||
};
|
||
LuneTooltip.Attach(_readyButton, ReadyTip);
|
||
_readyCheck = LuneUi.Glyph("check", LuneUiTheme.Ink, 22f, _readyButton);
|
||
_readyCheck.style.position = Position.Absolute;
|
||
_readyCheck.style.left = 16f;
|
||
LuneUi.Show(_readyCheck, false);
|
||
_readyNote = LuneUi.Label("Leertaste", "HudMuted", column);
|
||
_readyNote.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||
_combatRow = LuneUi.Box(false, 8f, column);
|
||
_combatRow.style.justifyContent = Justify.Center;
|
||
_combatRow.style.flexGrow = 1f;
|
||
LuneUi.Show(_combatRow, false);
|
||
LuneUi.Glyph("melee", LuneUiTheme.Lightened(LuneUiTheme.Danger, .15f), 16f, _combatRow);
|
||
_clock = LuneUi.Label("", "HudValue", _combatRow);
|
||
_auto = LuneUi.Clip(LuneUi.Label("", "HudMuted", _combatRow));
|
||
}
|
||
|
||
void BuildMap()
|
||
{
|
||
_mapPanel = AddPanel(LunePanels.HudTight);
|
||
_mapPanel.style.right = Margin;
|
||
_mapPanel.style.bottom = Margin;
|
||
_mapPanel.style.width = MapEdge + 16f;
|
||
_mapPanel.style.height = MapEdge + 16f;
|
||
Minimap = new LuneMinimap { Match = Match, HumanSlot = HumanSlot };
|
||
Minimap.style.flexGrow = 1f;
|
||
_mapPanel.Add(Minimap);
|
||
Minimap.FocusRequested += point => MapFocusRequested?.Invoke(point);
|
||
LuneTooltip.Attach(Minimap, () => new LuneTip
|
||
{
|
||
Title = "Minimap", Text = "Klicken oder ziehen bewegt die Kamera. Runde Marker: Blau · eckige Marker: Rot. Krone: Boss, Pfeil: Angriffschampion, Porträt im Goldring: Held.",
|
||
Note = "Die Minimap löst nie Käufe oder Befehle aus.",
|
||
});
|
||
}
|
||
|
||
void BuildBanner()
|
||
{
|
||
_banners = LuneUi.Box(true, 6f, _hud);
|
||
_banners.style.position = Position.Absolute;
|
||
}
|
||
|
||
void BuildStatus()
|
||
{
|
||
_statusWrap = new VisualElement { pickingMode = PickingMode.Ignore };
|
||
_statusWrap.style.position = Position.Absolute;
|
||
_statusWrap.style.left = Margin;
|
||
_statusWrap.style.right = Margin + MapEdge + 16f + 10f;
|
||
_statusWrap.style.flexDirection = FlexDirection.Row;
|
||
_statusWrap.style.justifyContent = Justify.Center;
|
||
_hud.Add(_statusWrap);
|
||
_status = new LunePanel(LunePanels.HudTight);
|
||
_statusWrap.Add(_status);
|
||
VisualElement column = LuneUi.Box(true, 0f, _status);
|
||
_report = LuneUi.Label("", "HudText", column);
|
||
_report.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||
_toast = LuneUi.Label("", "HudMuted", column);
|
||
_toast.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||
LuneUi.Show(_statusWrap, false);
|
||
}
|
||
|
||
void BuildModal()
|
||
{
|
||
_modal = new LuneModalPanel(new Vector2(1040f, 660f));
|
||
_hud.Add(_modal);
|
||
HeroDetail = new LuneHeroDetail();
|
||
_modal.Body.Add(HeroDetail);
|
||
HeroDetail.CommandRequested += command => CommandRequested?.Invoke(command);
|
||
}
|
||
|
||
void BuildOver()
|
||
{
|
||
_over = new VisualElement();
|
||
_over.style.position = Position.Absolute;
|
||
_over.style.left = 0f;
|
||
_over.style.top = 0f;
|
||
_over.style.right = 0f;
|
||
_over.style.bottom = 0f;
|
||
_over.style.backgroundColor = new Color(.02f, .03f, .04f, .82f);
|
||
_over.style.justifyContent = Justify.Center;
|
||
_over.style.alignItems = Align.Center;
|
||
_over.RegisterCallback<WheelEvent>(evt => evt.StopPropagation());
|
||
LuneUi.Show(_over, false);
|
||
_hud.Add(_over);
|
||
var panel = new LunePanel(LunePanels.Menu);
|
||
panel.style.minWidth = 620f;
|
||
_over.Add(panel);
|
||
VisualElement column = LuneUi.Box(true, 14f, panel);
|
||
column.style.alignItems = Align.Center;
|
||
_overTitle = LuneUi.Label("", "TitleLabel", column, 60f);
|
||
_overBody = LuneUi.Wrap(LuneUi.Label("", "BodyLabel", column));
|
||
_overBody.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||
_overBody.style.maxWidth = 560f;
|
||
VisualElement row = LuneUi.Box(false, 12f, column);
|
||
row.style.justifyContent = Justify.Center;
|
||
new LuneButton("NEUE PARTIE", LuneButtons.Primary, row).Clicked += () => RematchRequested?.Invoke();
|
||
new LuneButton("Menü", LuneButtons.Secondary, row).Clicked += () => MenuRequested?.Invoke();
|
||
}
|
||
|
||
// ---------------------------------------------------------------- layout
|
||
/// <summary>Size of the render target in pixels (screen or texture).</summary>
|
||
public Vector2 TargetSize { get { return _target != null ? new Vector2(_target.width, _target.height) : new Vector2(Screen.width, Screen.height); } }
|
||
|
||
/// <summary>
|
||
/// Logical area at UI scale 1 (Godot: content scale canvas_items/expand from 1600×960) and
|
||
/// the UI scale: the preference, capped so the layout always has MinArea logical pixels.
|
||
/// </summary>
|
||
public static float ScaleFor(Vector2 pixels, float wanted, out Vector2 area)
|
||
{
|
||
float k = Mathf.Min(pixels.x / Reference.x, pixels.y / Reference.y);
|
||
area = pixels / Mathf.Max(.0001f, k);
|
||
float fits = Mathf.Min(area.x / MinArea.x, area.y / MinArea.y);
|
||
return Mathf.Clamp(Mathf.Min(Mathf.Clamp(wanted, .75f, 1.5f), fits), .6f, 1.5f);
|
||
}
|
||
|
||
void Fit()
|
||
{
|
||
if (Panel == null)
|
||
return;
|
||
UiScale = ScaleFor(TargetSize, LunePreferences.UiScale, out _area);
|
||
Panel.scale = UiScale;
|
||
Layout();
|
||
}
|
||
|
||
/// <summary>Logical size of the HUD (area / UI scale).</summary>
|
||
public Vector2 LogicalSize { get { return _area / Mathf.Max(.0001f, UiScale); } }
|
||
|
||
void Layout()
|
||
{
|
||
if (_over == null)
|
||
return;
|
||
Vector2 size = LogicalSize;
|
||
bool planning = Match == null || Match.Phase == Phase.Planning;
|
||
_readyBox.style.height = planning ? ReadyHeight : ReadyCombat;
|
||
_bottom.style.minHeight = planning ? BarPlanning : BarCombat;
|
||
float bannerWidth = Mathf.Min(700f, size.x - 2f * (LeftWidth + 2f * Margin));
|
||
_banners.style.width = bannerWidth;
|
||
_banners.style.left = Mathf.Floor((size.x - bannerWidth) * .5f);
|
||
float topHeight = float.IsNaN(_top.layout.height) || _top.layout.height <= 0f ? 96f : _top.layout.height;
|
||
_banners.style.top = Margin + topHeight + 8f;
|
||
float barHeight = float.IsNaN(_bottom.layout.height) || _bottom.layout.height <= 0f ? (planning ? BarPlanning : BarCombat) : _bottom.layout.height;
|
||
_statusWrap.style.bottom = Margin + barHeight + 8f;
|
||
float systemHeight = float.IsNaN(_system.layout.height) || _system.layout.height <= 0f ? 36f : _system.layout.height;
|
||
_right.style.top = Margin + systemHeight + 8f;
|
||
_layoutPhase = Match != null ? (int)Match.Phase : -1;
|
||
}
|
||
|
||
/// <summary>Rectangles of the main regions in logical pixels (layout tests).</summary>
|
||
public Dictionary<string, Rect> Regions()
|
||
{
|
||
var result = new Dictionary<string, VisualElement>
|
||
{
|
||
{ "top", _top }, { "system", _system }, { "left", _left }, { "right", _right }, { "bottom", _bottom }, { "ready", _readyBox }, { "minimap", _mapPanel },
|
||
};
|
||
var rects = new Dictionary<string, Rect>();
|
||
foreach (KeyValuePair<string, VisualElement> pair in result)
|
||
{
|
||
if (LuneUi.Visible(pair.Value))
|
||
rects[pair.Key] = _hud.WorldToLocal(pair.Value.worldBound);
|
||
}
|
||
return rects;
|
||
}
|
||
|
||
// ---------------------------------------------------------------- per frame
|
||
/// <summary>Main-thread cost of the last frame's Tick (milliseconds; benchmark).</summary>
|
||
public double LastTickMs { get; private set; }
|
||
readonly System.Diagnostics.Stopwatch _tickClock = new System.Diagnostics.Stopwatch();
|
||
|
||
void Update()
|
||
{
|
||
if (Match == null)
|
||
return;
|
||
_tickClock.Restart();
|
||
Tick(Time.unscaledDeltaTime);
|
||
LastTickMs = _tickClock.Elapsed.TotalMilliseconds;
|
||
}
|
||
|
||
/// <summary>One HUD frame (Godot _process); tests call it with simulated time.</summary>
|
||
public void Tick(float delta)
|
||
{
|
||
Tooltip.Tick(delta);
|
||
_gold.Tick(delta);
|
||
Feed.Tick(delta);
|
||
_toastLeft -= delta;
|
||
if (_toastLeft <= 0f && _toast.text.Length > 0)
|
||
{
|
||
_toast.text = "";
|
||
StatusVisibility();
|
||
}
|
||
foreach (AncientBlock block in _ancients)
|
||
{
|
||
if (block.Pulse <= 0f)
|
||
continue;
|
||
block.Pulse = Mathf.Max(0f, block.Pulse - delta);
|
||
block.Bar.style.opacity = block.Pulse > 0f ? .45f + .55f * Mathf.Abs(Mathf.Cos(block.Pulse * Mathf.PI * 2f)) : 1f;
|
||
}
|
||
_tick += delta;
|
||
_mapTick += delta;
|
||
_feedTick += delta;
|
||
bool fighting = Match.Phase == Phase.Combat;
|
||
if (fighting && _mapTick >= MapRate)
|
||
{
|
||
_mapTick = 0f;
|
||
Minimap.Refresh(Match.Announcements());
|
||
}
|
||
if (_feedTick >= FeedRate)
|
||
{
|
||
TrackEvents();
|
||
_feedTick = 0f;
|
||
}
|
||
if (_dirty || (int)Match.Phase != _lastPhase || _tick >= (fighting ? CombatRate : PlanningRate))
|
||
{
|
||
var watch = System.Diagnostics.Stopwatch.StartNew();
|
||
Sync(_tick);
|
||
_tick = 0f;
|
||
_dirty = false;
|
||
Syncs++;
|
||
SyncMs = SyncMs <= 0.0 ? watch.Elapsed.TotalMilliseconds : SyncMs + (watch.Elapsed.TotalMilliseconds - SyncMs) * .2;
|
||
}
|
||
}
|
||
|
||
public void MarkDirty()
|
||
{
|
||
_dirty = true;
|
||
}
|
||
|
||
/// <summary>True for one second after a combat ended: Bereit (click or Space) is ignored meanwhile.</summary>
|
||
public bool ReadyBlocked()
|
||
{
|
||
return Time.realtimeSinceStartup < _readyBlockUntil;
|
||
}
|
||
|
||
void Sync(float delta)
|
||
{
|
||
int phase = (int)Match.Phase;
|
||
if (phase != _lastPhase)
|
||
{
|
||
PhaseChanged(_lastPhase, phase);
|
||
_lastPhase = phase;
|
||
}
|
||
if (_layoutPhase != phase)
|
||
Layout();
|
||
bool planning = Model.Planning;
|
||
bool fighting = Model.Fighting;
|
||
SyncTop();
|
||
bool reduced = LunePreferences.ReducedMotion;
|
||
foreach (KeyValuePair<string, LuneLaneIndicator> pair in Lanes)
|
||
pair.Value.Apply(Model.Lane(pair.Key), Model.Team, delta, reduced);
|
||
_team.Blessing = Model.State.Extended ? Model.State.Boss.Blessing[Model.Team] : -1;
|
||
_team.Apply(Model.Members(Model.Team), Model.Team, planning, Model.Networked);
|
||
SyncRight(planning, fighting);
|
||
SyncBottom(planning, fighting);
|
||
SyncReady(planning, fighting);
|
||
SyncBanners();
|
||
if (!fighting)
|
||
{
|
||
Minimap.ViewerTeam = Model.Team;
|
||
Minimap.HumanSlot = HumanSlot;
|
||
Minimap.Refresh(Match.Announcements());
|
||
}
|
||
if (_modal.Open)
|
||
{
|
||
HeroDetail.Apply(Model);
|
||
_modal.Title.text = HeroDetail.TitleText(Model);
|
||
}
|
||
LuneUi.Show(_over, Model.Over);
|
||
if (Model.Over)
|
||
{
|
||
int own = Model.Team;
|
||
_overTitle.text = Match.Winner == LuneMatch.Draw ? "UNENTSCHIEDEN" : Match.Winner == own ? "SIEG" : "NIEDERLAGE";
|
||
_overBody.text = Match.EndNote + "\nNach " + Match.RoundNumber + " Wellen.";
|
||
}
|
||
}
|
||
|
||
void SyncTop()
|
||
{
|
||
for (int side = 0; side < 2; side++)
|
||
{
|
||
int team = side == 0 ? Model.Team : Model.Enemy;
|
||
AncientBlock block = _ancients[side];
|
||
LuneHudModel.BuildingInfo ancient = Model.Ancient(team);
|
||
block.Data = ancient;
|
||
Color tint = LuneUiTheme.TeamColor(team);
|
||
block.Emblem.Glyph = "emblem_" + team;
|
||
block.Emblem.Color = tint;
|
||
block.Caption.text = (side == 0 ? "DEIN ANCIENT · " : "GEGNER · ") + LuneUi.Upper(LuneUiTheme.TeamName(team));
|
||
block.Bar.Fraction = (float)ancient.Fraction;
|
||
block.Bar.Fill = LuneHpBar.HealthColor(block.Bar.Fraction, tint);
|
||
block.Value.text = LuneUiTheme.Number(ancient.Hp) + " / " + LuneUiTheme.Number(ancient.MaxHp);
|
||
for (int index = 0; index < 2; index++)
|
||
{
|
||
LuneHudModel.BuildingInfo baseTower = ancient.Base[index];
|
||
block.Towers[index].Team = team;
|
||
block.Towers[index].SetState(baseTower.Alive, (float)baseTower.Fraction, baseTower.Attackable);
|
||
}
|
||
int level = block.Bar.Fraction < .3f ? 2 : block.Bar.Fraction < .5f ? 1 : 0;
|
||
bool show = level > 0 || !ancient.Alive;
|
||
if (!ancient.Alive)
|
||
block.Badge.SetState("ZERSTÖRT", LuneUiTheme.Danger, "close", true);
|
||
else if (level == 2)
|
||
block.Badge.SetState("KRITISCH", LuneUiTheme.Danger, "warning", true);
|
||
else if (level == 1)
|
||
block.Badge.SetState("BESCHÄDIGT", LuneUiTheme.Warn);
|
||
else if (ancient.Attackable)
|
||
{
|
||
show = true;
|
||
block.Badge.SetState("ANGREIFBAR", LuneUiTheme.Warn);
|
||
}
|
||
LuneUi.Show(block.Badge, show);
|
||
}
|
||
_wave.text = "WELLE " + Match.RoundNumber;
|
||
if (Model.Over)
|
||
_phaseBadge.SetState("PARTIE BEENDET", LuneUiTheme.Muted);
|
||
else if (Model.Fighting)
|
||
{
|
||
int elapsed = (int)Match.Combat.Elapsed;
|
||
_phaseBadge.SetState(string.Format("KAMPF · {0:00}:{1:00}", elapsed / 60, elapsed % 60), LuneUiTheme.Lightened(LuneUiTheme.Danger, .1f), "melee");
|
||
}
|
||
else if (Model.Locked)
|
||
_phaseBadge.SetState("PLANUNG · BEREIT", LuneUiTheme.Good, "check");
|
||
else
|
||
_phaseBadge.SetState("PLANUNG", LuneUiTheme.BronzeLight, "clock");
|
||
}
|
||
|
||
void SyncRight(bool planning, bool fighting)
|
||
{
|
||
LuneHudModel.BossInfo boss = Model.Boss();
|
||
BossCard.Apply(boss, Model.Team, Match.RoundNumber, planning);
|
||
if (boss.HasHp)
|
||
BossCard.SetBossHp((float)(boss.Hp / Math.Max(1.0, boss.MaxHp)));
|
||
RosterEntry entry = SelectedEntry();
|
||
if (entry == null && SelectedUid.Length > 0)
|
||
SelectedUid = "";
|
||
LuneUi.Show(UnitDetail, entry != null);
|
||
BossCard.SetCompact(entry != null);
|
||
if (entry != null)
|
||
UnitDetail.Apply(entry, Model, Model.LiveUnit(SelectedUid));
|
||
bool wave = entry == null && !Model.Over;
|
||
LuneUi.Show(NextWave, wave);
|
||
if (wave)
|
||
NextWave.Apply(Model.NextWave(), Model.Team, fighting);
|
||
}
|
||
|
||
RosterEntry SelectedEntry()
|
||
{
|
||
if (SelectedUid.Length == 0)
|
||
return null;
|
||
return Model.OwnRoster.FirstOrDefault(e => e.Uid == SelectedUid);
|
||
}
|
||
|
||
void SyncBottom(bool planning, bool fighting)
|
||
{
|
||
_gold.SetValues(Model.OwnGold, Model.Income, !Model.Over, Model.IncomeBonus);
|
||
List<RosterEntry> roster = Model.OwnRoster;
|
||
bool locked = Model.Locked;
|
||
bool canAct = planning && !locked;
|
||
LuneUi.Show(_purchase, planning);
|
||
LuneUi.Show(_army, !planning || !LuneUi.Visible(ChampionPanel));
|
||
LuneUi.Show(_undo, planning);
|
||
_undo.Disabled = !canAct || Model.Me.Undo.Count == 0;
|
||
LuneUi.Show(_starter, planning && roster.Count == 0);
|
||
_starter.Disabled = !canAct;
|
||
if (planning)
|
||
{
|
||
List<string> ids = Match.BaseIds(Model.Me.Bund);
|
||
int gold = Model.OwnGold;
|
||
bool full = roster.Count >= Model.SupplyCap;
|
||
if (!ShopCards.ContainsKey(ids[0]))
|
||
{
|
||
ShopCards.Clear();
|
||
for (int index = 0; index < ids.Count; index++)
|
||
{
|
||
var card = (LuneUnitCard)_troopRow[index];
|
||
string id = ids[index];
|
||
UnitDef data = DataCatalog.GetUnit(id);
|
||
card.Setup(id, id, data.DisplayName, Model.RoleOf(id), data.Cost, (index + 1).ToString(), false, data);
|
||
if (card.userData == null)
|
||
{
|
||
card.userData = "wired";
|
||
card.Clicked += () => CommandRequested?.Invoke(Command.Buy(card.TypeId));
|
||
LuneTooltip.Attach(card, () => LuneStats.TroopTip(card.TypeId, Model.RoleOf(card.TypeId), card.Reason));
|
||
}
|
||
ShopCards[id] = card;
|
||
}
|
||
}
|
||
foreach (LuneUnitCard card in ShopCards.Values)
|
||
{
|
||
string why = "";
|
||
if (locked)
|
||
why = "Bereit gemeldet – die Planung ist verbindlich.";
|
||
else if (full)
|
||
why = "Armee voll (" + roster.Count + "/" + Model.SupplyCap + ").";
|
||
else if (gold < card.Cost)
|
||
why = "Es fehlen " + (card.Cost - gold) + " Gold.";
|
||
card.SetAvailable(why.Length == 0, why, locked, gold < card.Cost);
|
||
}
|
||
if (LuneUi.Visible(ChampionPanel))
|
||
ChampionPanel.Apply(Model);
|
||
}
|
||
_armyTitle.text = "ARMEE " + roster.Count + "/" + Model.SupplyCap;
|
||
var perLane = new Dictionary<string, int>();
|
||
var aliveLane = new Dictionary<string, int>();
|
||
int value = 0;
|
||
foreach (RosterEntry entry in roster)
|
||
{
|
||
perLane[entry.Lane] = perLane.TryGetValue(entry.Lane, out int count) ? count + 1 : 1;
|
||
value += entry.Invested;
|
||
if (fighting)
|
||
{
|
||
LuneUnit unit = Model.LiveUnit(entry.Uid);
|
||
if (unit != null && unit.Alive)
|
||
aliveLane[entry.Lane] = aliveLane.TryGetValue(entry.Lane, out int alive) ? alive + 1 : 1;
|
||
}
|
||
}
|
||
var parts = new List<string>();
|
||
foreach (string lane in LuneMap.Lanes.Concat(new[] { LuneMatch.Grotto }))
|
||
{
|
||
if (!perLane.ContainsKey(lane))
|
||
continue;
|
||
string shortName = lane == LuneMatch.Grotto ? "GROTTE" : LuneUi.Upper(LuneMap.LaneNames[lane]);
|
||
parts.Add(fighting ? shortName + " " + aliveLane.GetValueOrDefault(lane, 0) + "/" + perLane[lane] : shortName + " " + perLane[lane]);
|
||
}
|
||
_armyLanes.text = string.Join(" · ", parts);
|
||
_armyValue.text = roster.Count > 0 ? "Wert " + LuneUiTheme.Number(value) : "";
|
||
SyncChips(roster, fighting);
|
||
HeroState hero = Model.OwnHero;
|
||
HeroCard.Apply(hero, Model, hero != null ? Model.LiveUnit(hero.Uid) : null);
|
||
}
|
||
|
||
void SyncChips(List<RosterEntry> roster, bool fighting)
|
||
{
|
||
var order = LuneMap.Lanes.Concat(new[] { LuneMatch.Grotto }).ToList();
|
||
List<RosterEntry> ordered = roster.OrderBy(e => order.IndexOf(e.Lane)).ThenBy(e => e.Seq).ToList();
|
||
float edge = ChipEdge(ordered.Count, fighting);
|
||
string signature = string.Join(",", ordered.Select(e => e.Uid + ":" + e.TypeId + ":" + e.Lane)) + "|" + (int)edge;
|
||
if (signature != _chipSignature)
|
||
{
|
||
_chipSignature = signature;
|
||
_strip.Clear();
|
||
Chips.Clear();
|
||
foreach (RosterEntry entry in ordered)
|
||
Chips[entry.Uid] = MakeChip(entry, edge);
|
||
}
|
||
foreach (KeyValuePair<string, LuneArmyChip> pair in Chips)
|
||
{
|
||
pair.Value.On = pair.Key == SelectedUid;
|
||
if (fighting)
|
||
{
|
||
LuneUnit unit = Model.LiveUnit(pair.Key);
|
||
pair.Value.Fallen = unit == null || !unit.Alive;
|
||
}
|
||
else
|
||
{
|
||
pair.Value.Fallen = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>Largest chip edge (46 … 30 px) at which the whole army fits into the strip.</summary>
|
||
float ChipEdge(int count, bool fighting)
|
||
{
|
||
float width = !float.IsNaN(_army.layout.width) && _army.layout.width > 60f ? _army.layout.width : 300f;
|
||
float height = (fighting ? BarCombat : BarPlanning) - 20f - 26f;
|
||
foreach (float edge in new[] { 46f, 42f, 38f, 34f, 30f })
|
||
{
|
||
int perRow = Math.Max(1, (int)Math.Floor((width + 4f) / (edge + 4f)));
|
||
int rows = (int)Math.Ceiling(count / (double)perRow);
|
||
if (rows * (edge + LuneArmyChip.Band + 4f) <= height)
|
||
return edge;
|
||
}
|
||
return 30f;
|
||
}
|
||
|
||
LuneArmyChip MakeChip(RosterEntry entry, float edge)
|
||
{
|
||
var chip = new LuneArmyChip();
|
||
chip.style.marginRight = 4f;
|
||
chip.style.marginBottom = 4f;
|
||
_strip.Add(chip);
|
||
chip.Setup(entry, edge);
|
||
string uid = entry.Uid;
|
||
chip.Clicked += () =>
|
||
{
|
||
if (SelectedUid == uid)
|
||
ClearSelection();
|
||
else
|
||
SelectUnit(uid);
|
||
};
|
||
string type = entry.TypeId;
|
||
string lane = entry.Lane;
|
||
LuneTooltip.Attach(chip, () =>
|
||
{
|
||
LuneTip tip = LuneStats.TroopTip(type, Model.RoleOf(type));
|
||
tip.Note = "Einsatz: " + (lane == LuneMatch.Grotto ? "Grotte" : LuneMap.LaneNames[lane]) + " · Klick: Details, Aufwerten, Einsatz";
|
||
if (chip.Fallen)
|
||
tip.Warn = "In diesem Kampf gefallen; kehrt zur nächsten Planung zurück.";
|
||
return tip;
|
||
});
|
||
return chip;
|
||
}
|
||
|
||
void SyncReady(bool planning, bool fighting)
|
||
{
|
||
LuneUi.Show(_readyBox, !Model.Over);
|
||
LuneUi.Show(_readyButton, planning);
|
||
LuneUi.Show(_readyNote, planning);
|
||
LuneUi.Show(_combatRow, fighting);
|
||
LuneUi.Show(_pause, fighting);
|
||
LuneUi.Show(_speed, fighting);
|
||
if (planning)
|
||
{
|
||
bool locked = Model.Locked;
|
||
_readyButton.Disabled = locked || ReadyBlocked();
|
||
_readyButton.Text = "BEREIT";
|
||
LuneUi.Show(_readyCheck, locked);
|
||
if (locked)
|
||
{
|
||
Model.ReadyCount(out int ready, out int humans);
|
||
_readyNote.text = humans > ready ? "Warte auf " + Math.Max(0, humans - ready) + " Mitspieler" : "Kampf beginnt …";
|
||
}
|
||
else
|
||
{
|
||
_readyNote.text = "Leertaste · startet Welle " + Match.RoundNumber;
|
||
}
|
||
}
|
||
if (fighting)
|
||
{
|
||
int elapsed = (int)Match.Combat.Elapsed;
|
||
_clock.text = string.Format("KAMPF {0:00}:{1:00}", elapsed / 60, elapsed % 60);
|
||
_auto.text = "Tempo " + _speed.Text;
|
||
bool guest = Model.IsGuest;
|
||
_pause.Disabled = guest;
|
||
_speed.Disabled = guest;
|
||
}
|
||
}
|
||
|
||
void SyncBanners()
|
||
{
|
||
var lines = new List<(string text, string glyph, bool own, LuneFocusTarget target)>();
|
||
LuneHudModel.BossInfo boss = Model.Boss();
|
||
if (boss.State == "deploy_next" || boss.State == "deploying")
|
||
{
|
||
bool own = boss.Own;
|
||
string lane = LuneUi.Upper(LuneMap.LaneNames.TryGetValue(boss.Lane, out string name) ? name : "?");
|
||
string text = own
|
||
? "Deine SCHLUNDKRONE greift in Welle " + boss.DeployRound + " auf " + lane + " an · Kopfgeld für den Gegner " + LuneUiTheme.Number(boss.Bounty) + " Gold"
|
||
: "SCHLUNDKRONE (" + LuneUiTheme.TeamName(boss.Team) + ") greift in Welle " + boss.DeployRound + " auf " + lane + " an · Kopfgeld für euch " + LuneUiTheme.Number(boss.Bounty) + " Gold";
|
||
lines.Add((text, "crown", own, LuneFocusTarget.ForLane(boss.Lane)));
|
||
}
|
||
LuneHudModel.BuildingInfo core = Model.Ancient(Model.Team);
|
||
if (core.Alive && core.Fraction < .3)
|
||
lines.Add(("Dein Ancient ist kritisch: " + LuneUiTheme.Round(core.Fraction * 100.0) + " % Leben", "warning", false, LuneFocusTarget.ForCamera("base")));
|
||
string signature = string.Join("|", lines.Select(l => l.text + l.glyph + l.own));
|
||
if (signature == _bannerSignature)
|
||
return;
|
||
_bannerSignature = signature;
|
||
_banners.Clear();
|
||
foreach (var line in lines)
|
||
{
|
||
var panel = new LunePanel(line.own ? LunePanels.BannerOwn : LunePanels.BannerDanger);
|
||
panel.RegisterCallback<WheelEvent>(evt => evt.StopPropagation());
|
||
_banners.Add(panel);
|
||
VisualElement row = LuneUi.Box(false, 10f, panel);
|
||
row.style.justifyContent = Justify.Center;
|
||
LuneUi.Glyph(line.glyph, line.own ? LuneUiTheme.TeamColor(Model.Team) : LuneUiTheme.Danger, 20f, row);
|
||
Label label = LuneUi.Wrap(LuneUi.Label(line.text, "HudValue", row));
|
||
label.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||
label.style.flexGrow = 1f;
|
||
LuneFocusTarget target = line.target;
|
||
panel.RegisterCallback<PointerDownEvent>(evt =>
|
||
{
|
||
if (evt.button == 0)
|
||
FocusTarget(target);
|
||
});
|
||
}
|
||
}
|
||
|
||
void StatusVisibility()
|
||
{
|
||
LuneUi.Show(_statusWrap, _report.text.Length > 0 || _toast.text.Length > 0);
|
||
LuneUi.Show(_report, _report.text.Length > 0);
|
||
LuneUi.Show(_toast, _toast.text.Length > 0);
|
||
}
|
||
|
||
// ---------------------------------------------------------------- phase changes and events
|
||
void PhaseChanged(int previous, int phase)
|
||
{
|
||
if (phase == (int)Phase.Combat)
|
||
{
|
||
CloseModal();
|
||
_report.text = "";
|
||
StatusVisibility();
|
||
foreach (Announcement entry in Match.Announcements())
|
||
{
|
||
bool own = entry.Team == Model.Team;
|
||
string lane = LuneUi.Upper(LuneMap.LaneNames[entry.Lane]);
|
||
if (entry.Kind == "boss")
|
||
Feed.Push(lane + " – Schlundkrone (" + LuneUiTheme.TeamName(entry.Team) + ") greift an", own ? "good" : "danger", "crown", LuneFocusTarget.ForLane(entry.Lane));
|
||
else
|
||
Feed.Push(lane + " – Angriffschampion " + LuneContent.Champions[entry.ChampionId].Name + " (" + LuneUiTheme.TeamName(entry.Team) + ") erschienen", own ? "info" : "warn", "champion", LuneFocusTarget.ForLane(entry.Lane));
|
||
}
|
||
}
|
||
else if (phase == (int)Phase.Planning && previous == (int)Phase.Combat)
|
||
{
|
||
_readyBlockUntil = Time.realtimeSinceStartup + 1f;
|
||
ReportSettlement();
|
||
}
|
||
else if (phase == (int)Phase.Planning && previous < 0 && Match.LastSettlement != null)
|
||
{
|
||
ReportSettlement();
|
||
}
|
||
_bossSeen.Clear();
|
||
Layout();
|
||
}
|
||
|
||
void ReportSettlement()
|
||
{
|
||
Settlement s = Match.LastSettlement;
|
||
if (s == null)
|
||
return;
|
||
int kills = s.Gains[HumanSlot];
|
||
int gained = kills + s.Income;
|
||
_report.text = string.Format("Welle {0} abgeschlossen ({1}, {2}s) · dein Ertrag +{3} Gold (Kills {4}, Einkommen {5})", s.Round, Reason(s.EndReason), (int)s.CombatTime, gained, kills, s.Income);
|
||
StatusVisibility();
|
||
Feed.Push("Welle " + s.Round + " abgeschlossen · +" + gained + " Gold", "info", "coin");
|
||
BossClaim claim = Match.Boss.Claim;
|
||
if (s.Boss == "won" && claim != null)
|
||
{
|
||
bool own = claim.Team == Model.Team;
|
||
Feed.Push("Boss in Welle " + claim.DeployRound + " auf " + LuneUi.Upper(LuneMap.LaneNames[claim.Lane]) + " (" + LuneUiTheme.TeamName(claim.Team) + ")", own ? "good" : "danger", "crown", LuneFocusTarget.ForLane(claim.Lane));
|
||
}
|
||
int level = OwnHeroLevel();
|
||
if (level > _heroLevel && _heroLevel > 0)
|
||
Feed.Push(LuneContent.Heroes[Model.OwnHero.HeroId].Name + " erreicht Stufe " + level, "good", "star", LuneFocusTarget.ForCamera("hero"));
|
||
_heroLevel = level;
|
||
}
|
||
|
||
int OwnHeroLevel()
|
||
{
|
||
HeroState hero = Model.OwnHero;
|
||
return hero == null ? 0 : LuneContent.LevelFor(hero.Xp);
|
||
}
|
||
|
||
void RememberBuildings()
|
||
{
|
||
_knownAlive.Clear();
|
||
foreach (BuildingDef definition in Match.Map.Buildings)
|
||
_knownAlive[definition.Id] = Model.Building(definition.Id).Alive;
|
||
}
|
||
|
||
/// <summary>Diffs public state for the event feed: fallen buildings, Ancient thresholds, the boss.</summary>
|
||
void TrackEvents()
|
||
{
|
||
if (Model.Over && _knownAlive.Count == 0)
|
||
return;
|
||
foreach (BuildingDef definition in Match.Map.Buildings)
|
||
{
|
||
string id = definition.Id;
|
||
bool alive = Model.Building(id).Alive;
|
||
if (_knownAlive.GetValueOrDefault(id, alive) && !alive)
|
||
{
|
||
bool own = definition.Team == Model.Team;
|
||
string place = definition.Lane == "base" ? "BASIS" : LuneUi.Upper(LuneMap.LaneNames.TryGetValue(definition.Lane, out string name) ? name : definition.Lane);
|
||
Feed.Push(place + " – " + LuneHudModel.BuildingTitle(id) + " (" + LuneUiTheme.TeamName(definition.Team) + ") zerstört", own ? "danger" : "good", "tower", LuneFocusTarget.ForPoint(definition.Position));
|
||
}
|
||
_knownAlive[id] = alive;
|
||
}
|
||
for (int side = 0; side < 2; side++)
|
||
{
|
||
int team = side == 0 ? Model.Team : Model.Enemy;
|
||
LuneHudModel.BuildingInfo core = Model.Ancient(team);
|
||
int level = core.Fraction < .3 ? 2 : core.Fraction < .5 ? 1 : 0;
|
||
if (level > _ancientLevel[side] && core.Alive && level == 2)
|
||
{
|
||
int percent = LuneUiTheme.Round(core.Fraction * 100.0);
|
||
Feed.Push(side == 0 ? "Dein Ancient kritisch (" + percent + " %)" : "Gegnerischer Ancient kritisch (" + percent + " %)", side == 0 ? "danger" : "good", "warning",
|
||
LuneFocusTarget.ForCamera(side == 0 ? "base" : "enemy_base"));
|
||
if (!LunePreferences.ReducedMotion)
|
||
_ancients[side].Pulse = 1.5f;
|
||
}
|
||
_ancientLevel[side] = level;
|
||
}
|
||
if (!Model.Fighting)
|
||
return;
|
||
foreach (LuneUnit unit in Match.Combat.Units)
|
||
{
|
||
if (unit.Kind != "boss")
|
||
continue;
|
||
string key = unit.Zone + "-" + unit.Id;
|
||
bool was = _bossSeen.GetValueOrDefault(key, unit.Alive);
|
||
if (was && !unit.Alive)
|
||
{
|
||
if (unit.Zone == "grotto")
|
||
{
|
||
LuneHudModel.BossInfo boss = Model.Boss();
|
||
if (boss.Team >= 0)
|
||
Feed.Push("Bossgrotte erobert – " + LuneUiTheme.TeamName(boss.Team), boss.Own ? "good" : "warn", "crown", LuneFocusTarget.ForCamera("grotto"));
|
||
}
|
||
else
|
||
{
|
||
Feed.Push("SCHLUNDKRONE besiegt (" + LuneUi.Upper(LuneMap.LaneNames.TryGetValue(unit.Lane, out string name) ? name : "") + ")", unit.Team != Model.Team ? "good" : "warn", "crown", LuneFocusTarget.ForLane(unit.Lane));
|
||
}
|
||
}
|
||
_bossSeen[key] = unit.Alive;
|
||
}
|
||
}
|
||
|
||
void FocusTarget(LuneFocusTarget target)
|
||
{
|
||
if (target == null)
|
||
return;
|
||
if (target.Lane.Length > 0 && Array.IndexOf(LuneMap.Lanes, target.Lane) >= 0)
|
||
LaneFocusRequested?.Invoke(target.Lane);
|
||
else if (target.Point.HasValue)
|
||
MapFocusRequested?.Invoke(target.Point.Value);
|
||
else if (target.Camera.Length > 0)
|
||
CameraRequested?.Invoke(target.Camera);
|
||
}
|
||
|
||
static string Reason(string reason)
|
||
{
|
||
return reason == "cleared" ? "alle Einheiten gefallen" : reason == "timeout" ? "Zeitfenster abgelaufen" : reason == "core" ? "Ancient zerstört" : reason;
|
||
}
|
||
|
||
// ---------------------------------------------------------------- public API
|
||
public void Notify(string message, bool error = false)
|
||
{
|
||
_toast.text = message ?? "";
|
||
LuneUi.SetColor(_toast, error ? LuneUiTheme.Danger : LuneUiTheme.Good);
|
||
_toastLeft = 5f;
|
||
StatusVisibility();
|
||
_dirty = true;
|
||
}
|
||
|
||
public string ToastText { get { return _toast.text; } }
|
||
|
||
/// <summary>The Ancient bar of a side (0 own, 1 enemy) pulses after turning critical (off with reduced motion).</summary>
|
||
public bool AncientPulsing(int side) { return _ancients[side].Pulse > 0f; }
|
||
public string ReportText { get { return _report.text; } }
|
||
|
||
public void SetSpeedLabel(int value, bool paused)
|
||
{
|
||
_speed.Text = value + "×";
|
||
_pauseGlyph.Glyph = paused ? "play" : "pause";
|
||
}
|
||
|
||
/// <summary>G2 compatibility: 0 = troops, 1 = hero, 2 = attack champions.</summary>
|
||
public void ShowPage(int index)
|
||
{
|
||
if (index == 1)
|
||
{
|
||
OpenHero();
|
||
return;
|
||
}
|
||
CloseModal();
|
||
if (index == 2)
|
||
OpenChampions();
|
||
else
|
||
OpenTroops();
|
||
}
|
||
|
||
public void OpenHero(int tab = LuneHeroDetail.TabValues)
|
||
{
|
||
HeroDetail.Tab = tab;
|
||
HeroDetail.ShowTab(tab);
|
||
HeroDetail.Apply(Model);
|
||
_modal.OpenWith(HeroDetail.TitleText(Model));
|
||
}
|
||
|
||
public void OpenItems() { OpenHero(LuneHeroDetail.TabItems); }
|
||
public void OpenTalents() { OpenHero(LuneHeroDetail.TabTalents); }
|
||
public void CloseModal() { _modal.Close(); }
|
||
public bool ModalOpen { get { return _modal.Open; } }
|
||
|
||
public void OpenChampions()
|
||
{
|
||
_purchaseTabs.Select(1);
|
||
ShowPurchase(1);
|
||
}
|
||
|
||
public void OpenTroops()
|
||
{
|
||
_purchaseTabs.Select(0);
|
||
ShowPurchase(0);
|
||
}
|
||
|
||
void ShowPurchase(int index)
|
||
{
|
||
LuneUi.Show(_troopRow, index == 0);
|
||
LuneUi.Show(ChampionPanel, index == 1);
|
||
LuneUi.Show(_army, index == 0);
|
||
_purchase.style.flexGrow = index == 1 ? 1f : 0f;
|
||
_dirty = true;
|
||
if (index == 1)
|
||
ChampionPanel.Apply(Model);
|
||
}
|
||
|
||
public void SelectUnit(string uid)
|
||
{
|
||
SelectedUid = uid ?? "";
|
||
_dirty = true;
|
||
}
|
||
|
||
public void ClearSelection()
|
||
{
|
||
SelectedUid = "";
|
||
_dirty = true;
|
||
}
|
||
|
||
/// <summary>Esc: closes the options, the hero window or the selection (in this order); false if nothing was open.</summary>
|
||
public bool HandleEscape()
|
||
{
|
||
if (Settings.Open)
|
||
{
|
||
Settings.Close();
|
||
return true;
|
||
}
|
||
if (_modal.Open)
|
||
{
|
||
CloseModal();
|
||
return true;
|
||
}
|
||
if (SelectedUid.Length > 0)
|
||
{
|
||
ClearSelection();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
public string PublicDigest() { return Model.PublicDigest(); }
|
||
|
||
/// <summary>
|
||
/// Whether a screen point (pixels, origin bottom left as Input.mousePosition) lies on a HUD
|
||
/// element that takes the mouse: then the world must not react (wheel zoom, field clicks).
|
||
/// </summary>
|
||
public bool IsPointerOverHud(Vector2 screen)
|
||
{
|
||
if (Root == null || Root.panel == null)
|
||
return false;
|
||
// Pixels of the render target (screen or texture), y flipped to the panel's top-left origin.
|
||
Vector2 panelPoint = PixelToPanel(new Vector2(screen.x, TargetSize.y - screen.y));
|
||
VisualElement picked = Root.panel.Pick(panelPoint);
|
||
return picked != null && picked != Root && picked != _hud;
|
||
}
|
||
|
||
/// <summary>Panel coordinates (logical) of a point given in target pixels with origin top left (tests).</summary>
|
||
public Vector2 PixelToPanel(Vector2 pixel)
|
||
{
|
||
return pixel / (Mathf.Min(TargetSize.x / Reference.x, TargetSize.y / Reference.y) * UiScale);
|
||
}
|
||
|
||
// ---------------------------------------------------------------- tooltips
|
||
LuneTip WaveTip()
|
||
{
|
||
LuneHudModel.WaveInfo wave = Model.NextWave();
|
||
return new LuneTip
|
||
{
|
||
Title = "Welle " + Match.RoundNumber, Glyph = "clock",
|
||
Text = "Wellen sind unbegrenzt: Die Partie endet nur, wenn ein Ancient fällt. Monster werden jede Runde stärker, alle drei Runden kommt eines je Lane hinzu (bis 7).",
|
||
Note = "Kommende Welle " + wave.Round + ": " + wave.Count + " Monster je Lane.",
|
||
};
|
||
}
|
||
|
||
LuneTip AncientTip(AncientBlock block)
|
||
{
|
||
LuneHudModel.BuildingInfo ancient = block.Data;
|
||
if (ancient == null)
|
||
return null;
|
||
int team = ancient.Team;
|
||
int standing = (ancient.Base[0].Alive ? 1 : 0) + (ancient.Base[1].Alive ? 1 : 0);
|
||
return new LuneTip
|
||
{
|
||
Title = "Ancient " + LuneUiTheme.TeamName(team) + " · " + LuneUiTheme.TeamEmblems[team], Glyph = "emblem_" + team, GlyphColor = LuneUiTheme.TeamColor(team),
|
||
Text = "Fällt ein Ancient, ist die Partie vorbei. Er ist erst angreifbar, wenn beide Basistürme (Kernwacht) gefallen sind; die fallen erst, wenn eine Lane ganz offen ist.",
|
||
Note = block.Own ? "B: Kamera zum eigenen Ancient" : "",
|
||
}.Stat("heart", "Leben", LuneUiTheme.Number(ancient.Hp) + " / " + LuneUiTheme.Number(ancient.MaxHp)).Stat("tower", "Basistürme", standing + " von 2 stehen");
|
||
}
|
||
|
||
LuneTip BaseTip(AncientBlock block, int index)
|
||
{
|
||
LuneHudModel.BuildingInfo ancient = block.Data;
|
||
if (ancient == null)
|
||
return null;
|
||
LuneHudModel.BuildingInfo baseTower = ancient.Base[index];
|
||
return new LuneTip
|
||
{
|
||
Title = "Kernwacht " + (index + 1) + " · " + LuneUiTheme.TeamName(baseTower.Team), Glyph = "tower", GlyphColor = LuneUiTheme.TeamColor(baseTower.Team),
|
||
Text = baseTower.Alive ? "Basisturm vor dem Ancient. Angreifbar, sobald auf einer Lane Grenzwacht und Bastion gefallen sind." : "Zerstört.",
|
||
}.Stat("heart", "Leben", LuneLaneIndicator.TowerValue(baseTower));
|
||
}
|
||
|
||
LuneTip GoldTip()
|
||
{
|
||
return new LuneTip
|
||
{
|
||
Title = "Dein Gold", Glyph = "coin", GlyphColor = LuneUiTheme.Gold,
|
||
Text = "Dazu kommt Gold für besiegte Monster, Champions, Gebäude und den Boss auf der eigenen Seite (Teile gehen ans Team).",
|
||
Note = "Das Gold der anderen Spieler bleibt privat.",
|
||
}.Stat("coin", "Jetzt", LuneUiTheme.Number(Model.OwnGold)).Stat("clock", "Einkommen", "+" + LuneUiTheme.Number(Model.Income) + " nach dieser Welle" + (Model.IncomeBonus > 0 ? " (davon " + Model.IncomeBonus + " Söldner-Einkommen aus Angriffschampions)" : ""));
|
||
}
|
||
|
||
LuneTip StarterTip()
|
||
{
|
||
List<string> ids = Match.BaseIds(Model.Me.Bund);
|
||
int cost = 0;
|
||
var names = new List<string>();
|
||
foreach (int index in new[] { 1, 0, 2 })
|
||
{
|
||
UnitDef data = DataCatalog.GetUnit(ids[index]);
|
||
cost += data.Cost;
|
||
names.Add(data.DisplayName);
|
||
}
|
||
return new LuneTip { Title = "Startaufstellung · " + cost + " Gold", Text = "Kauft " + string.Join(", ", names) + " für deine Lane." };
|
||
}
|
||
|
||
LuneTip ReadyTip()
|
||
{
|
||
if (Model.Locked)
|
||
return new LuneTip { Title = "Bereit gemeldet", Text = "Deine Planung ist verbindlich. Der Kampf beginnt, sobald alle bereit sind." };
|
||
return new LuneTip { Title = "Bereit", Text = "Übernimmt deine Planung verbindlich. Der Kampf läuft automatisch über alle drei Lanes.", Note = "Leertaste" };
|
||
}
|
||
}
|
||
}
|