using System.Numerics;
using System.Text.RegularExpressions;
using LScape = ACE.Server.Physics.Common.LScape;
using SortCell = ACE.Server.Physics.Common.SortCell;
namespace Aeshnidae.Expedition;
///
/// Copied from Aeshnidae.Invasions (2026-10-01, for Act 2's beacon sieges): cross-mod types are
/// unsafe here, so the engine is copied, not referenced. Where invaders stand. Towns are written in map coordinates ("42.0N 33.5E"), which is how
/// players and the wiki name a place; everything here works in ACE's global metres (one
/// landblock is 192 m, one map unit 240 m) and turns a point back into a landblock, a cell
/// and a ground height only at the end, so a ring round a town crosses landblock seams freely.
///
public static partial class Placement
{
[GeneratedRegex(@"^\s*(\d+(?:\.\d+)?)\s*([NS])[\s,]+(\d+(?:\.\d+)?)\s*([EW])\s*$", RegexOptions.IgnoreCase)]
private static partial Regex MapCoords();
/// "42.0N 33.5E" -> global metres (X east, Y north), or null.
public static Vector2? ParseMapCoords(string text)
{
var m = MapCoords().Match(text ?? "");
if (!m.Success)
return null;
var ns = float.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture);
var ew = float.Parse(m.Groups[3].Value, CultureInfo.InvariantCulture);
if (m.Groups[2].Value.Equals("S", StringComparison.OrdinalIgnoreCase)) ns = -ns;
if (m.Groups[4].Value.Equals("W", StringComparison.OrdinalIgnoreCase)) ew = -ew;
// The inverse of ACE's PositionExtensions.GetMapCoords: map = global / 240 - 102.
return new Vector2((ew + 102f) * 240f, (ns + 102f) * 240f);
}
/// Global metres of an outdoor position (indoor ones have no map place: null).
public static Vector2? Global(Position? p)
{
if (p is null || p.Indoors)
return null;
var g = p.ToGlobal();
return new Vector2(g.X, g.Y);
}
///
/// An outdoor position on the ground at global metres (x, y), or null when the spot is
/// no good: off the world, inside a building's cell, on a slope too steep to stand on.
/// Must run on the world thread - it reads the physics landscape.
///
public static Position? GroundAt(Vector2 global, float headingRadians = 0f)
{
try
{
if (global.X < 0 || global.Y < 0 || global.X >= 255 * Position.BlockLength || global.Y >= 255 * Position.BlockLength)
return null;
var pos = new Position().FromGlobal(new Vector3(global.X, global.Y, 0f));
// FromGlobal resolves the cell by the given height; at z 0 a spot under a building
// or a cave can land in an indoor cell. Only open ground will do.
if (pos.Indoors || pos.LandblockId.Raw == 0)
return null;
if (LScape.get_landcell(pos.Cell) is not SortCell cell || cell.has_building())
return null;
pos.PositionZ = pos.GetTerrainZ() + 0.05f;
if (!pos.IsWalkable())
return null;
// Face the given heading (radians, 0 = north, clockwise), as ACE's rotation does.
pos.Rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, -headingRadians);
return pos;
}
catch
{
return null;
}
}
///
/// A ground spot on a ring round the centre, tried up to `tries` times at random angles
/// and distances in [min, max]. The heading faces the centre.
///
public static Position? RingSpot(Vector2 center, double min, double max, int tries = 20, double? angle = null, double spread = Math.PI)
{
for (var i = 0; i < tries; i++)
{
var a = angle.HasValue
? angle.Value + (Random.Shared.NextDouble() * 2 - 1) * spread
: Random.Shared.NextDouble() * Math.PI * 2;
var r = min + Random.Shared.NextDouble() * Math.Max(0, max - min);
// a is measured from north, clockwise: x east = sin, y north = cos.
var spot = new Vector2(center.X + (float)(Math.Sin(a) * r), center.Y + (float)(Math.Cos(a) * r));
// Heading back toward the centre.
var heading = (float)Math.Atan2(center.X - spot.X, center.Y - spot.Y);
var pos = GroundAt(spot, heading);
if (pos is not null)
return pos;
}
return null;
}
/// Global 2D distance from an outdoor position to a point; +inf indoors.
public static double Distance(Position? p, Vector2 point)
{
var g = Global(p);
return g is null ? double.PositiveInfinity : Vector2.Distance(g.Value, point);
}
/// "42.0N 33.5E" for a global point, the way ACE prints map coordinates.
public static string MapText(Vector2 global)
{
var ew = global.X / 240f - 102f;
var ns = global.Y / 240f - 102f;
return $"{Math.Abs(ns):0.0}{(ns >= 0 ? "N" : "S")} {Math.Abs(ew):0.0}{(ew >= 0 ? "E" : "W")}";
}
}