Beginn des Ports des G3-HUD (Referenz luneraria/g3, 1b540f2). - Entscheidung UI Toolkit statt uGUI nach kurzer Bewertung und einer Probe: Panel rendert im Batchmodus in eine 1920x1080-Textur (Skalierung wie Godot canvas_items/expand, 1600x960), gefaste Verlaufsplatten, alle 50 Symbole, Schriften aus den variablen Fonts der Referenz. - Sichtmodell portiert (LuneHudModel, LuneLaneThreat, LuneStats) mit den Privatsphäre-Regeln der Referenz: Gold, Entwurf, Rückgängig und neue Buchungen nur für den eigenen Sitz; Lanes, Gebäude, Boss und Wellen aus öffentlichem Zustand. - Menschlicher Sitz in LuneMatchRunner/LuneMatchDriver: Bots planen die übrigen fünf Sitze, die Planung wartet auf Bereit; Tempo 1x/2x/4x und Pause nur im Kampf. Bot-Partien unverändert. - Theme-Grundlagen: Farben und Zahlenformate, Platten (forged_style.gd), Vektorsymbole (lune_glyphs.gd) mit Painter2D, statische SDF-Atlanten aus den benannten Instanzen von Cantarell und Cinzel (OFL). - Tests: EditMode 23/23 (12 neue: gleiches öffentliches Bild für alle Sitze, fremde Entwürfe privat, private Boss-Lane-Wahl, Lane-Bedrohung, Bosszustände, nächste Welle, Bereit-Sperre, Formate, menschlicher Sitz); PlayMode 3/3, Fingerabdrücke der Bot-Partien unverändert (DCE95C96556F3A1A, 7B8E43AEFC0D60C5). - Dokumentation: U3-UI (Bewertung, U3A), U5-VISUAL (Vorgaben des Projektinhabers: stilisierter Realismus für die ganze Welt, erst in U5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
192 lines
8.2 KiB
C#
192 lines
8.2 KiB
C#
using System.Collections.Generic;
|
||
using Luneraria.Core;
|
||
|
||
namespace Luneraria.UI
|
||
{
|
||
/// <summary>
|
||
/// Deterministic lane threat for one team (port of scripts/luneraria/ui/lune_lane_threat.gd),
|
||
/// read only from public state: building HP, the units on the battlefield (positions and HP,
|
||
/// which every client also sees) and the public announcements of this round. It never reads
|
||
/// drafts, gold, unannounced bookings or bot notes, so host and guest compute the same level.
|
||
/// Positions are projected onto the lane, because a network guest has positions but no lane
|
||
/// progress.
|
||
///
|
||
/// RUHIG nothing below applies
|
||
/// UMKÄMPFT a lane tower is below 85 %, an enemy attack champion is announced here, or
|
||
/// enemy units stand in the own half of the lane
|
||
/// UNTER DRUCK the Grenzwacht fell or is below 50 %, enemies attack the Grenzwacht, the enemy
|
||
/// has more than 1.5 times the own unit HP in the own half, or the enemy boss
|
||
/// is deployed on this lane
|
||
/// KRITISCH both lane towers fell (base open), the Bastion is below 50 % after the
|
||
/// Grenzwacht fell, enemies attack the Bastion, or the enemy boss comes while the
|
||
/// Grenzwacht is already down
|
||
/// </summary>
|
||
public static class LuneLaneThreat
|
||
{
|
||
public const int Calm = 0;
|
||
public const int Contested = 1;
|
||
public const int Pressure = 2;
|
||
public const int Critical = 3;
|
||
public static readonly string[] Names = { "RUHIG", "UMKÄMPFT", "UNTER DRUCK", "KRITISCH" };
|
||
public static readonly string[] Glyphs = { "check", "melee", "warning", "warning" };
|
||
|
||
public sealed class Result
|
||
{
|
||
public int Level;
|
||
public string Name = "";
|
||
public readonly List<string> Reasons = new List<string>();
|
||
}
|
||
|
||
public sealed class Battle
|
||
{
|
||
public double Length;
|
||
public int OwnCount;
|
||
public int EnemyCount;
|
||
public double OwnHp;
|
||
public double EnemyHp;
|
||
/// <summary>Deepest enemy position measured from the own base along the lane (infinity without enemies).</summary>
|
||
public double EnemyDepth = double.PositiveInfinity;
|
||
public double OwnFront;
|
||
}
|
||
|
||
static void Building(LuneMatch state, string id, out bool alive, out double fraction)
|
||
{
|
||
double maxHp = state.Map.BuildingDef(id)?.MaxHp ?? 1.0;
|
||
if (state.Phase == Phase.Combat && state.Combat != null && state.Combat.Buildings.TryGetValue(id, out LuneUnit unit))
|
||
{
|
||
alive = unit.Alive;
|
||
fraction = alive ? GdMath.Clamp(unit.Hp / GdMath.Max(1.0, maxHp), 0.0, 1.0) : 0.0;
|
||
return;
|
||
}
|
||
double hp = state.BuildingHp.GetValueOrDefault(id, 0.0);
|
||
alive = hp > 0.0;
|
||
fraction = GdMath.Clamp(hp / GdMath.Max(1.0, maxHp), 0.0, 1.0);
|
||
}
|
||
|
||
static string Percent(double value)
|
||
{
|
||
return LuneUiTheme.Round(value * 100.0) + " %";
|
||
}
|
||
|
||
public static Result Evaluate(LuneMatch state, int team, string lane)
|
||
{
|
||
var result = new Result();
|
||
int level = Calm;
|
||
string prefix = team == 0 ? "blau" : "rot";
|
||
int enemy = 1 - team;
|
||
Building(state, prefix + "_" + lane + "_outer", out bool outerAlive, out double outer);
|
||
Building(state, prefix + "_" + lane + "_inner", out bool innerAlive, out double inner);
|
||
if (!outerAlive && !innerAlive)
|
||
{
|
||
level = Critical;
|
||
result.Reasons.Add("Grenzwacht und Bastion gefallen – der Weg zur Basis ist offen");
|
||
}
|
||
else if (!outerAlive)
|
||
{
|
||
level = Pressure;
|
||
result.Reasons.Add("Grenzwacht gefallen");
|
||
if (inner < .5)
|
||
{
|
||
level = Critical;
|
||
result.Reasons.Add("Bastion bei " + Percent(inner));
|
||
}
|
||
else if (inner < .85)
|
||
{
|
||
result.Reasons.Add("Bastion bei " + Percent(inner));
|
||
}
|
||
}
|
||
else if (outer < .5)
|
||
{
|
||
level = Pressure;
|
||
result.Reasons.Add("Grenzwacht bei " + Percent(outer));
|
||
}
|
||
else if (outer < .85)
|
||
{
|
||
level = Contested;
|
||
result.Reasons.Add("Grenzwacht bei " + Percent(outer));
|
||
}
|
||
foreach (Announcement entry in state.Announcements())
|
||
{
|
||
if (entry.Team != enemy || entry.Lane != lane)
|
||
continue;
|
||
if (entry.Kind == "boss")
|
||
{
|
||
level = System.Math.Max(level, !outerAlive ? Critical : Pressure);
|
||
result.Reasons.Add("Gegnerische Schlundkrone greift hier an");
|
||
}
|
||
else
|
||
{
|
||
level = System.Math.Max(level, Contested);
|
||
result.Reasons.Add("Angriffschampion " + LuneContent.Champions[entry.ChampionId].Name + " angekündigt");
|
||
}
|
||
}
|
||
if (state.Phase == Phase.Combat && state.Combat != null)
|
||
{
|
||
Battle battle = Battlefield(state, team, lane);
|
||
double length = battle.Length;
|
||
if (battle.EnemyCount > 0)
|
||
{
|
||
double deepest = battle.EnemyDepth;
|
||
double innerAt = length * state.Balance.InnerTowerFraction;
|
||
double outerAt = length * state.Balance.OuterTowerFraction;
|
||
double reach = state.Balance.TowerRange * .8;
|
||
if (deepest <= innerAt + reach)
|
||
{
|
||
level = Critical;
|
||
result.Reasons.Add("Gegner greifen die Bastion oder die Basis an");
|
||
}
|
||
else if (deepest <= outerAt + reach && outerAlive)
|
||
{
|
||
level = System.Math.Max(level, Pressure);
|
||
result.Reasons.Add("Gegner greifen die Grenzwacht an");
|
||
}
|
||
else if (deepest <= length * .5)
|
||
{
|
||
level = System.Math.Max(level, Contested);
|
||
result.Reasons.Add("Gegner in der eigenen Lanehälfte");
|
||
}
|
||
if (battle.EnemyHp > battle.OwnHp * 1.5 && deepest <= length * .5)
|
||
{
|
||
level = System.Math.Max(level, Pressure);
|
||
result.Reasons.Add(string.Format("Übermacht: {0} gegen {1} Einheiten", battle.EnemyCount, battle.OwnCount));
|
||
}
|
||
}
|
||
}
|
||
result.Level = level;
|
||
result.Name = Names[level];
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Units of both sides on one lane: counts, HP sums and the deepest enemy position measured
|
||
/// from the own base along the lane (from positions only).
|
||
/// </summary>
|
||
public static Battle Battlefield(LuneMatch state, int team, string lane)
|
||
{
|
||
LanePath path = state.Map.LanePaths[lane];
|
||
var result = new Battle { Length = path.Length };
|
||
if (state.Combat == null)
|
||
return result;
|
||
foreach (LuneUnit unit in state.Combat.Units)
|
||
{
|
||
if (!unit.Alive || !unit.IsMobile() || unit.Zone != "lane" || unit.Lane != lane || unit.Team > 1)
|
||
continue;
|
||
double s = path.Project(unit.Position);
|
||
double fromOwnBase = team == 0 ? s : path.Length - s;
|
||
if (unit.Team == team)
|
||
{
|
||
result.OwnCount++;
|
||
result.OwnHp += unit.Hp;
|
||
result.OwnFront = GdMath.Max(result.OwnFront, fromOwnBase);
|
||
}
|
||
else
|
||
{
|
||
result.EnemyCount++;
|
||
result.EnemyHp += unit.Hp;
|
||
result.EnemyDepth = GdMath.Min(result.EnemyDepth, fromOwnBase);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
}
|
||
}
|