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; } } }