champions-of-luneraria/Assets/Luneraria/Core/Simulation/LuneFog.cs
Martin Stickelbroeck 7b86973197 S4 Feinschliff: Betäubung sichtbar, Nebel im Gelände, Glyphe für Gäste
Betäubungs-Ring über dem Kopf (Gäste über Bit 18 der Rahmen-Flags),
Nebel-Gitter LuneFogOverlay mit Shader Luneraria/FogOverlay aus
LuneFog.Vision (10 Hz, weiche Kante), laufende Glyphe je Team als vier
Zusatzwerte nach den Gebäude-Lebenspunkten. Reine Darstellung.
Kern 627/627 (s4 8), EditMode 35/35, PlayMode 20/20, Verify 718/0,
Selbsttest 30/30, Golden 0, Fingerabdruck unverändert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:55:42 +02:00

163 lines
7 KiB
C#

using System.Collections.Generic;
namespace Luneraria.Core
{
/// <summary>
/// S3 fog of war (Dota 2 vision and wards, extended rules). It only decides what a viewer's
/// team sees; the simulation never reads it, so the rules and every replay stay the same.
/// In combat an enemy unit is visible within reach of an own living unit, an own living
/// building or one of the team's wards (a ward watches the middle of its lane). In planning,
/// enemy armies show only on scouted lanes: a ward there, or the enemy's outer tower of that
/// lane has fallen. Neutral units (camp guardians, the grotto boss) are always visible.
/// </summary>
public static class LuneFog
{
public const double UnitSight = 9.0;
public const double BuildingSight = 10.0;
public const double WardSight = 12.0;
struct Source
{
public Vec2 At;
public double Radius;
public string Zone;
}
/// <summary>The lane is scouted for the team in planning (ward, fallen enemy outer tower, or a ward in the own draft).</summary>
public static bool LaneScouted(LuneMatch match, int team, string lane, Draft ownDraft = null)
{
int index = LuneMap.LaneIndex(lane);
if (index < 0)
return false;
if (match.Boss.Wards[team * 3 + index] > 0)
return true;
if (ownDraft != null && ownDraft.WardLanes.Contains(lane))
return true;
foreach (BuildingDef definition in match.Map.Buildings)
{
if (definition.Team == 1 - team && definition.Lane == lane && definition.Tier == "outer")
return match.BuildingHp[definition.Id] <= 0.0;
}
return false;
}
/// <summary>
/// Planning (S3): the enemy's committed army on the lanes the team has scouted, as the
/// match will field it (troops and heroes, lane owner first). The planning preview shows
/// these; unscouted lanes show nothing of the enemy. Empty without the extended rules.
/// </summary>
public static List<LuneMatch.ArmyEntry> ScoutedEnemies(LuneMatch match, int team, Draft ownDraft = null)
{
var result = new List<LuneMatch.ArmyEntry>();
if (match == null || !match.Extended || team < 0 || team > 1)
return result;
int enemy = 1 - team;
foreach (string lane in LuneMap.Lanes)
{
if (!LaneScouted(match, team, lane, ownDraft))
continue;
for (int slot = enemy * 3; slot < enemy * 3 + 3; slot++)
{
foreach (LuneMatch.ArmyEntry entry in match.ArmyEntries(slot))
{
if (entry.Lane == lane)
result.Add(entry);
}
}
}
return result;
}
/// <summary>Middle of a lane, where a ward watches.</summary>
public static Vec2 WardPoint(LuneMatch match, string lane)
{
return match.Map.LanePoint(0, lane, match.Map.LanePaths[lane].Length * 0.5, 0.0);
}
/// <summary>
/// S4: everything that gives the team sight in the shown combat (or planning preview):
/// own living units, own standing buildings (from the match, so the planning preview has
/// them too) and the team's wards. Each as point, radius and zone.
/// </summary>
public static List<KeyValuePair<Vec2, double>> Vision(LuneMatch match, LuneCombat shown, int team, string zone = "lane")
{
var result = new List<KeyValuePair<Vec2, double>>();
if (match == null || team < 0 || team > 1)
return result;
if (shown != null)
{
foreach (LuneUnit unit in shown.Units)
{
if (unit.Team == team && unit.Alive && unit.IsMobile() && unit.Zone == zone)
result.Add(new KeyValuePair<Vec2, double>(unit.Position, UnitSight));
}
}
if (zone == "lane")
{
foreach (BuildingDef definition in match.Map.Buildings)
{
if (definition.Team == team && match.BuildingHp[definition.Id] > 0.0)
result.Add(new KeyValuePair<Vec2, double>(definition.Position, BuildingSight));
}
foreach (string lane in LuneMap.Lanes)
{
if (match.Boss.Wards[team * 3 + LuneMap.LaneIndex(lane)] > 0)
result.Add(new KeyValuePair<Vec2, double>(WardPoint(match, lane), WardSight));
}
}
return result;
}
/// <summary>
/// Ids of the enemy units the team cannot see in the shown combat (the live combat, or
/// the planning preview when planning). Empty without the extended rules or for team -1.
/// </summary>
public static HashSet<int> Hidden(LuneMatch match, LuneCombat shown, int team, bool planning, Draft ownDraft = null)
{
var hidden = new HashSet<int>();
if (match == null || shown == null || !match.Extended || team < 0 || team > 1)
return hidden;
if (planning)
{
foreach (LuneUnit unit in shown.Units)
{
if (unit.Team == 1 - team && unit.IsMobile() && (unit.Zone != "lane" || !LaneScouted(match, team, unit.Lane, ownDraft)))
hidden.Add(unit.Id);
}
return hidden;
}
var sources = new List<Source>();
foreach (LuneUnit unit in shown.Units)
{
if (unit.Team != team || !unit.Alive)
continue;
if (unit.IsMobile())
sources.Add(new Source { At = unit.Position, Radius = UnitSight, Zone = unit.Zone });
else if (unit.IsBuilding())
sources.Add(new Source { At = unit.Position, Radius = BuildingSight, Zone = "lane" });
}
foreach (string lane in LuneMap.Lanes)
{
if (match.Boss.Wards[team * 3 + LuneMap.LaneIndex(lane)] > 0)
sources.Add(new Source { At = WardPoint(match, lane), Radius = WardSight, Zone = "lane" });
}
foreach (LuneUnit unit in shown.Units)
{
if (unit.Team != 1 - team || !unit.IsMobile())
continue;
bool seen = false;
foreach (Source source in sources)
{
if (source.Zone == unit.Zone && unit.Position.DistanceTo(source.At) <= source.Radius)
{
seen = true;
break;
}
}
if (!seen)
hidden.Add(unit.Id);
}
return hidden;
}
}
}