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