champions-of-luneraria/Assets/Luneraria/Core/Simulation/LuneFog.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

129 lines
5.5 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>
/// 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;
}
}
}