champions-of-luneraria/Assets/Luneraria/UI/Model/LuneLaneThreat.cs
Martin Stickelbroeck fb5c92dd37 S3 Neutrallager und Fog of War mit Spähern (LoL, Dota 2)
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>
2026-10-11 16:30:57 +02:00

195 lines
8.5 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, ICollection<int> hidden = null)
{
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, hidden);
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>
/// <summary>Units on the lane in combat; S3: enemies in the fog (hidden) are not counted.</summary>
public static Battle Battlefield(LuneMatch state, int team, string lane, ICollection<int> hidden = null)
{
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;
if (hidden != null && hidden.Contains(unit.Id))
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;
}
}
}