using System.Collections.Generic; using Luneraria.Core; namespace Luneraria.UI { /// /// 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 /// 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 Reasons = new List(); } public sealed class Battle { public double Length; public int OwnCount; public int EnemyCount; public double OwnHp; public double EnemyHp; /// Deepest enemy position measured from the own base along the lane (infinity without enemies). 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; } /// /// 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). /// 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; } } }