using System;
namespace codelanguage;
/// BYOND client.perspective values.
public enum PerspectiveMode
{
/// MOB_PERSPECTIVE (default): the view is centered on client.mob.
Mob,
/// EYE_PERSPECTIVE: the view is centered on client.eye (falls back to client.mob).
Eye,
/// EDGE_PERSPECTIVE: like mob-centered, but the view scrolls only at its edges.
Edge,
/// PIXEL_PERSPECTIVE: edge scrolling with pixel offsets honored.
Pixel
}
///
/// Pure client-camera policy for the game client: which atom a client's map
/// view centers on (client.perspective / client.eye / client.mob) and how the
/// view scrolls (always-centered vs. edge scrolling). Used by
/// CraftSeekerWindow.RenderWorld and covered by ClientCameraTests.
///
public static class ClientCamera
{
/// Sub-tile resolution used by PIXEL_PERSPECTIVE edge checks.
public const double TilePixels = 32.0;
///
/// Parses a perspective value: "MOB"/"EYE"/"EDGE"/"PIXEL" (with or without
/// the "_PERSPECTIVE" suffix) or 0..3 (Mob..Pixel).
///
public static PerspectiveMode ParsePerspective(CraftValue? value)
{
if (value == null) return PerspectiveMode.Mob;
string text = value.ToString().Trim();
if (text.EndsWith("_PERSPECTIVE", StringComparison.OrdinalIgnoreCase))
text = text[..^"_PERSPECTIVE".Length];
if (text.Equals("MOB", StringComparison.OrdinalIgnoreCase)) return PerspectiveMode.Mob;
if (text.Equals("EYE", StringComparison.OrdinalIgnoreCase)) return PerspectiveMode.Eye;
if (text.Equals("EDGE", StringComparison.OrdinalIgnoreCase)) return PerspectiveMode.Edge;
if (text.Equals("PIXEL", StringComparison.OrdinalIgnoreCase)) return PerspectiveMode.Pixel;
if (double.TryParse(text, out double numeric))
{
return (int)numeric switch
{
1 => PerspectiveMode.Eye,
2 => PerspectiveMode.Edge,
3 => PerspectiveMode.Pixel,
_ => PerspectiveMode.Mob,
};
}
return PerspectiveMode.Mob;
}
///
/// Resolves the atom the client's view centers on:
/// MOB_PERSPECTIVE uses client.mob; every other mode uses client.eye and
/// falls back to client.mob when eye is unset. Returns null when neither
/// resolves (the caller typically falls back to the local player mob).
///
public static CraftValue? ResolveCenter(CraftValue? client)
{
if (client == null) return null;
var mode = ParsePerspective(client.GetVar("perspective"));
var mob = NonEmpty(client.GetVar("mob"));
if (mode == PerspectiveMode.Mob) return mob;
return NonEmpty(client.GetVar("eye")) ?? mob;
}
/// True when the mode scrolls only at the view edges.
public static bool UsesEdgeScroll(PerspectiveMode mode)
=> mode is PerspectiveMode.Edge or PerspectiveMode.Pixel;
///
/// Computes the top-left tile of the view. Centered modes always recenter
/// (matching the historical RenderWorld behavior, clamped at
/// ); edge modes keep
/// while the camera stays inside that rect and
/// recenter when it leaves. PIXEL_PERSPECTIVE also considers pixel offsets.
///
public static (int X, int Y) ResolveViewOrigin(
PerspectiveMode mode,
int centerX,
int centerY,
int viewSize,
(int X, int Y)? previous,
double pixelOffsetX = 0,
double pixelOffsetY = 0,
int minCoord = 1)
{
if (viewSize < 1) viewSize = 1;
if (!UsesEdgeScroll(mode) || previous is not { } origin)
return Centered(centerX, centerY, viewSize, minCoord);
double cameraX = mode == PerspectiveMode.Pixel ? centerX + pixelOffsetX / TilePixels : centerX;
double cameraY = mode == PerspectiveMode.Pixel ? centerY + pixelOffsetY / TilePixels : centerY;
bool inside = cameraX >= origin.X && cameraX <= origin.X + viewSize - 1
&& cameraY >= origin.Y && cameraY <= origin.Y + viewSize - 1;
return inside ? origin : Centered(centerX, centerY, viewSize, minCoord);
}
private static (int X, int Y) Centered(int centerX, int centerY, int viewSize, int minCoord)
=> (Math.Max(minCoord, centerX - viewSize / 2), Math.Max(minCoord, centerY - viewSize / 2));
private static CraftValue? NonEmpty(CraftValue? value)
{
if (value == null) return null;
if (value.Type == "null") return null;
if (value.Type == "text" && string.IsNullOrEmpty(value.ToString())) return null;
return value;
}
}