using System.Collections.Generic;
namespace Luneraria.Core
{
/// Static data of one building (18 per match).
public sealed class BuildingDef
{
public string Id = "";
public int Team;
/// top, mid, bot, or base for base towers and Ancients.
public string Lane = "";
/// outer (Grenzwacht), inner (Bastion), base (Kernwacht) or core (Ancient).
public string Tier = "";
/// Lane progress from the own base; -1 for base buildings.
public double Progress;
public Vec2 Position;
public double MaxHp;
public double Damage;
public double Range;
public int Bounty;
}
///
/// Lane polyline with cumulative lengths. Points are float vectors; the cumulative table is
/// stored as float and the total length as double, as in the reference.
///
public sealed class LanePath
{
public readonly Vec2[] Points;
public readonly float[] Cumulative;
public readonly double Length;
public LanePath(List source)
{
Points = source.ToArray();
Cumulative = new float[Points.Length];
Cumulative[0] = 0f;
double length = 0.0;
for (int i = 1; i < Points.Length; i++)
{
length += Points[i].DistanceTo(Points[i - 1]);
Cumulative[i] = (float)length;
}
Length = length;
}
/// Index i of the segment [i - 1, i] that contains lane coordinate d.
int Segment(double d)
{
int low = 1;
int high = Points.Length - 1;
while (low < high)
{
int middle = (low + high) >> 1;
if (d <= Cumulative[middle])
high = middle;
else
low = middle + 1;
}
return low;
}
public Vec2 PointAt(double s)
{
double d = GdMath.Clamp(s, 0.0, Length);
int i = Segment(d);
double span = GdMath.Max(0.0001, (double)Cumulative[i] - (double)Cumulative[i - 1]);
return Points[i - 1].Lerp(Points[i], GdMath.Clamp((d - Cumulative[i - 1]) / span, 0.0, 1.0));
}
public Vec2 TangentAt(double s)
{
int i = Segment(GdMath.Clamp(s, 0.0, Length));
return (Points[i] - Points[i - 1]).Normalized();
}
/// Point and unnormalised tangent (b - a) / span at s in one pass.
public void Sample(double s, out Vec2 point, out Vec2 tangent)
{
double d = GdMath.Clamp(s, 0.0, Length);
int i = Segment(d);
Vec2 a = Points[i - 1];
Vec2 b = Points[i];
double span = GdMath.Max(0.0001, (double)Cumulative[i] - (double)Cumulative[i - 1]);
point = a.Lerp(b, GdMath.Clamp((d - Cumulative[i - 1]) / span, 0.0, 1.0));
tangent = (b - a) / span;
}
/// Lane coordinate of the closest point on the polyline.
public double Project(Vec2 position)
{
double best = double.PositiveInfinity;
double bestS = 0.0;
for (int i = 1; i < Points.Length; i++)
{
Vec2 a = Points[i - 1];
Vec2 b = Points[i];
Vec2 ab = b - a;
double t = GdMath.Clamp((double)(position - a).Dot(ab) / GdMath.Max(0.0001, (double)ab.LengthSquared()), 0.0, 1.0);
double d = position.DistanceSquaredTo(a + ab * t);
if (d < best)
{
best = d;
bestS = (double)Cumulative[i - 1] + (double)ab.Length() * t;
}
}
return bestS;
}
}
///
/// Map in ground coordinates (x, z): Blau (team 0) in the south-west, Rot (team 1) in the
/// north-east, Rot the mirror image of Blau across x = z. Every lane is built from its Blau
/// half and that half mirrored, so lanes, towers and spawns are equal by construction
/// (reference: lune_map.gd, G1 layout). Top and Bottom have exactly the same length.
///
public sealed class LuneMap
{
public static readonly string[] Lanes = { "top", "mid", "bot" };
public static readonly Dictionary LaneNames = new Dictionary { { "top", "Top" }, { "mid", "Mid" }, { "bot", "Bottom" } };
/// Catmull-Rom control points of each Blau half, base to symmetry axis.
public static readonly Vec2[] TopControl = { new Vec2(-27.4, 21.0), new Vec2(-26.4, 13.0), new Vec2(-28.4, 4.0), new Vec2(-26.2, -5.0),
new Vec2(-28.6, -13.5), new Vec2(-28.4, -21.2), new Vec2(-25.0, -25.0) };
public static readonly Vec2[] MidControl = { new Vec2(-21.6, 21.6), new Vec2(-16.4, 17.2), new Vec2(-10.9, 11.5), new Vec2(-5.6, 5.9), new Vec2(0.0, 0.0) };
public static readonly Vec2 CoreBlue = new Vec2(-30.0, 30.0);
/// Two base towers per Ancient, 6 m out and 40° either side of the Mid direction.
public static readonly Vec2[] BaseTowersBlue = { new Vec2(-29.477, 24.023), new Vec2(-24.023, 29.477) };
/// Neutral boss grotto on the symmetry axis between Top and Mid.
public static readonly Vec2 Grotto = new Vec2(-13.5, -13.5);
public const double HalfExtent = 36.0;
public static readonly Vec2 Axis = new Vec2(0.70710678, 0.70710678);
public static readonly Vec2 BlueSide = new Vec2(-0.70710678, 0.70710678);
public const double SampleSpacing = 2.0;
public const string GrottoZone = "grotto";
public readonly LuneBalance Balance;
public readonly Dictionary LanePaths = new Dictionary();
public readonly List Buildings = new List();
readonly Dictionary _index = new Dictionary();
public LuneMap(LuneBalance balance = null)
{
Balance = balance ?? LuneBalance.Default();
LanePaths["top"] = new LanePath(FullLane(SampleHalf(TopControl)));
LanePaths["mid"] = new LanePath(FullLane(SampleHalf(MidControl)));
LanePaths["bot"] = new LanePath(SolveBottom(LanePaths["top"].Length));
BuildBuildings();
}
public static int LaneIndex(string lane)
{
return System.Array.IndexOf(Lanes, lane);
}
public static Vec2 Mirror(Vec2 v) { return new Vec2(v.Y, v.X); }
public static Vec2 MirrorMid(Vec2 v) { return new Vec2(-v.Y, -v.X); }
public static List FullLane(List half)
{
var result = new List(half);
for (int i = half.Count - 2; i >= 0; i--)
result.Add(Mirror(half[i]));
return result;
}
///
/// Samples the Catmull-Rom curve through the control points of a Blau half. The phantom
/// point after the axis point mirrors the one before it, so the joined lane has no kink.
///
public static List SampleHalf(IList control)
{
var ext = new List { control[0] * 2.0 - control[1] };
ext.AddRange(control);
ext.Add(Mirror(control[control.Count - 2]));
var result = new List();
for (int i = 1; i < ext.Count - 2; i++)
{
Vec2 p0 = ext[i - 1];
Vec2 p1 = ext[i];
Vec2 p2 = ext[i + 1];
Vec2 p3 = ext[i + 2];
int steps = GdMath.Max(2, GdMath.CeilI(p1.DistanceTo(p2) / SampleSpacing));
for (int k = 0; k < steps; k++)
{
double t = (double)k / steps;
double t2 = t * t;
double t3 = t2 * t;
result.Add(0.5 * (2.0 * p1 + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (3.0 * p1 - p0 - 3.0 * p2 + p3) * t3));
}
}
result.Add(control[control.Count - 1]);
return result;
}
/// Bottom half through the south-eastern lowland; bend pulls the meander inwards.
public static Vec2[] BottomControl(double bend)
{
return new[] { new Vec2(-21.0, 27.4), new Vec2(-12.5, 27.0), new Vec2(-4.0, 27.6), new Vec2(4.0, 27.0 - bend),
new Vec2(12.0, 27.4 - 0.6 * bend), new Vec2(19.5, 27.0), new Vec2(22.2, 27.8), new Vec2(25.0, 25.0) };
}
/// Bisection on the meander so Bottom is exactly as long as Top.
public static List SolveBottom(double targetLength)
{
double low = 0.0;
double high = 8.0;
for (int i = 0; i < 60; i++)
{
double bend = (low + high) * 0.5;
if (new LanePath(FullLane(SampleHalf(BottomControl(bend)))).Length < targetLength)
low = bend;
else
high = bend;
}
return FullLane(SampleHalf(BottomControl((low + high) * 0.5)));
}
/// Lane coordinate s for a team's own marching progress (0 at its own base).
public double SFor(int team, string lane, double progress)
{
return team == 0 ? progress : LanePaths[lane].Length - progress;
}
public double ProgressFor(int team, string lane, double s)
{
return team == 0 ? s : LanePaths[lane].Length - s;
}
/// Blau frame point, mirrored for Rot so both sides are exact images.
public Vec2 LanePoint(int team, string lane, double progress, double lateral = 0.0)
{
Vec2 point;
Vec2 tangent;
LanePaths[lane].Sample(progress, out point, out tangent);
Vec2 blue = point + new Vec2(-tangent.Y, tangent.X) * lateral;
return team == 0 ? blue : new Vec2(blue.Y, blue.X);
}
public Vec2 LaneForward(int team, string lane, double progress)
{
LanePath path = LanePaths[lane];
Vec2 tangent = path.TangentAt(GdMath.Clamp(progress, 0.0, path.Length));
return team == 0 ? tangent : Mirror(tangent);
}
/// Grotto entrance of a team (mirrored); the boss rests slightly inside.
public Vec2 GrottoEntry(int team)
{
Vec2 blue = Grotto + new Vec2(1.2, 4.6);
return team == 0 ? blue : Mirror(blue);
}
public Vec2 GrottoBossPoint() { return Grotto + new Vec2(-1.5, -1.5); }
public Vec2 GrottoCenter() { return Grotto; }
/// Army formation area on a lane: progress from/to and lateral half width.
public static void DeploymentSpan(out double from, out double to, out double halfWidth)
{
from = 0.4;
to = 7.2;
halfWidth = 2.4;
}
public Vec2 CorePosition(int team) { return team == 0 ? CoreBlue : Mirror(CoreBlue); }
void BuildBuildings()
{
Buildings.Clear();
_index.Clear();
for (int team = 0; team < 2; team++)
{
string prefix = team == 0 ? "blau" : "rot";
foreach (string lane in Lanes)
{
LanePath path = LanePaths[lane];
foreach (string tier in new[] { "inner", "outer" })
{
double fraction = tier == "inner" ? Balance.InnerTowerFraction : Balance.OuterTowerFraction;
double progress = path.Length * fraction;
Add(new BuildingDef { Id = prefix + "_" + lane + "_" + tier, Team = team, Lane = lane, Tier = tier, Progress = progress,
Position = LanePoint(team, lane, progress, Balance.TowerSideOffset),
MaxHp = tier == "inner" ? Balance.TowerInnerHp : Balance.TowerOuterHp, Damage = Balance.TowerDamage, Range = Balance.TowerRange,
Bounty = tier == "inner" ? Balance.BountyInnerTower : Balance.BountyOuterTower });
}
}
for (int index = 0; index < 2; index++)
{
Vec2 point = BaseTowersBlue[index];
Add(new BuildingDef { Id = prefix + "_base_" + (index + 1), Team = team, Lane = "base", Tier = "base", Progress = -1.0,
Position = team == 0 ? point : Mirror(point), MaxHp = Balance.BaseTowerHp, Damage = Balance.BaseTowerDamage,
Range = Balance.TowerRange, Bounty = Balance.BountyBaseTower });
}
Add(new BuildingDef { Id = prefix + "_ancient", Team = team, Lane = "base", Tier = "core", Progress = -1.0,
Position = CorePosition(team), MaxHp = Balance.CoreHp, Damage = 0.0, Range = 0.0, Bounty = 0 });
}
}
void Add(BuildingDef definition)
{
Buildings.Add(definition);
_index[definition.Id] = definition;
}
public BuildingDef BuildingDef(string id)
{
BuildingDef definition;
return _index.TryGetValue(id, out definition) ? definition : null;
}
///
/// Gate rule shared by every damage source: outer first, inner after its outer, base
/// towers once any lane is fully open, the Ancient after both base towers.
/// alive maps building id to alive; missing ids count as destroyed.
///
public static bool IsAttackable(BuildingDef definition, IDictionary alive)
{
string prefix = definition.Team == 0 ? "blau" : "rot";
switch (definition.Tier)
{
case "outer":
return true;
case "inner":
return !Get(alive, prefix + "_" + definition.Lane + "_outer");
case "base":
foreach (string lane in Lanes)
{
if (!Get(alive, prefix + "_" + lane + "_outer") && !Get(alive, prefix + "_" + lane + "_inner"))
return true;
}
return false;
case "core":
return !Get(alive, prefix + "_base_1") && !Get(alive, prefix + "_base_2");
}
return false;
}
static bool Get(IDictionary alive, string id)
{
bool value;
return alive.TryGetValue(id, out value) && value;
}
}
}